1use super::{
2 auth::{confirm_auth_rpc_disclosure, confirm_auth_rpc_disclosure_before_network_resolution},
3 call_overrides::CallOverrideOpts,
4 fetch_code_via_rpc, print_raw_line,
5 run::{
6 block_num_hash, call_tracer_frame, fetch_contracts_bytecode_from_trace, trace_addresses,
7 },
8};
9use crate::{
10 debug::{ensure_remote_trace_context_unchanged, handle_traces, resolve_remote_trace_hardfork},
11 rpc_trace::{call_frame_to_arena, call_tracer_config},
12 traces::TraceKind,
13 tx::{CastTxBuilder, SenderKind, read_only_sender},
14};
15use alloy_consensus::BlockHeader;
16use alloy_dyn_abi::FunctionExt;
17use alloy_eips::BlockNumHash;
18use alloy_ens::NameOrAddress;
19use alloy_network::{
20 BlockResponse, Ethereum, NetworkTransactionBuilder, TransactionBuilder,
21 primitives::HeaderResponse,
22};
23use alloy_primitives::{B256, Bytes, TxKind, U256, hex, map::AddressHashMap};
24use alloy_provider::{Provider, ext::DebugApi};
25use alloy_rpc_types::{
26 BlockId, BlockNumberOrTag, TransactionInput, TransactionRequest,
27 trace::geth::{GethDebugTracingCallOptions, GethDebugTracingOptions},
28};
29use clap::Parser;
30use eyre::{Result, WrapErr};
31use foundry_cli::{
32 opts::{ChainValueParser, RpcOpts, TracingArgs, TransactionOpts},
33 utils::{TraceResult, load_config_from_provider, parse_ether_value},
34};
35use foundry_common::{
36 FoundryTransactionBuilder,
37 abi::{encode_function_args, get_func},
38 fmt::{format_token, serialize_value_as_json},
39 provider::{ProviderBuilder, curl_transport::generate_curl_command},
40 sh_println, shell,
41};
42use foundry_compilers::artifacts::EvmVersion;
43use foundry_config::{
44 Chain, Config, TracingConfig,
45 figment::{
46 self, Metadata, Profile,
47 value::{Dict, Map},
48 },
49};
50use foundry_evm::{
51 core::{
52 FoundryBlock, FoundryTransaction,
53 decode::RevertDecoder,
54 evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork},
55 },
56 executors::{ExecutorBuilder, TracingExecutor},
57 opts::EvmOpts,
58 traces::{InternalTraceMode, SparsedTraceArena, TraceContext, TraceRequirements},
59};
60use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
61use foundry_wallets::{BrowserWalletOpts, WalletOpts};
62use revm::context::Block;
63use std::str::FromStr;
64
65#[cfg(feature = "base")]
66use foundry_evm::core::evm::BaseEvmNetwork;
67
68#[cfg(feature = "monad")]
69use foundry_evm::core::evm::MonadEvmNetwork;
70
71#[cfg(feature = "optimism")]
72use foundry_evm::core::evm::OpEvmNetwork;
73
74#[derive(Debug, Parser)]
99pub struct CallArgs {
100 #[arg(value_parser = NameOrAddress::from_str)]
102 to: Option<NameOrAddress>,
103
104 sig: Option<String>,
106
107 #[arg(allow_negative_numbers = true)]
109 args: Vec<String>,
110
111 #[arg(
113 long,
114 conflicts_with_all = &["sig", "args"]
115 )]
116 data: Option<String>,
117
118 #[arg(long, default_value_t = false)]
120 trace: bool,
121
122 #[arg(long, requires = "from", conflicts_with = "browser")]
132 delegate: bool,
133
134 #[arg(
143 long = "debug-trace-call",
144 default_value_t = false,
145 conflicts_with_all = ["trace", "debug", "decode_internal", "evm_version"]
146 )]
147 debug_trace_call: bool,
148
149 #[arg(long, requires = "trace")]
152 debug: bool,
153
154 #[command(flatten)]
155 tracing: TracingArgs,
156
157 #[arg(long, requires = "trace")]
160 evm_version: Option<EvmVersion>,
161
162 #[arg(long, short)]
166 block: Option<BlockId>,
167
168 #[command(subcommand)]
169 command: Option<CallSubcommands>,
170
171 #[command(flatten)]
172 tx: TransactionOpts,
173
174 #[arg(long)]
176 force: bool,
177
178 #[command(flatten)]
179 rpc: RpcOpts,
180
181 #[command(flatten)]
182 wallet: WalletOpts,
183
184 #[command(flatten)]
185 browser: BrowserWalletOpts,
186
187 #[arg(
188 short,
189 long,
190 alias = "chain-id",
191 env = "CHAIN",
192 value_parser = ChainValueParser::default(),
193 )]
194 pub chain: Option<Chain>,
195
196 #[arg(long, visible_alias = "la")]
198 pub with_local_artifacts: bool,
199
200 #[command(flatten)]
201 pub overrides: CallOverrideOpts,
202}
203
204#[derive(Debug, Parser)]
205pub enum CallSubcommands {
206 #[command(name = "--create")]
208 Create {
209 code: String,
211
212 sig: Option<String>,
214
215 #[arg(allow_negative_numbers = true)]
217 args: Vec<String>,
218
219 #[arg(long, value_parser = parse_ether_value)]
225 value: Option<U256>,
226 },
227}
228
229fn call_tracer_options() -> GethDebugTracingCallOptions {
231 GethDebugTracingCallOptions::default()
232 .with_tracing_options(GethDebugTracingOptions::call_tracer(call_tracer_config()))
233}
234
235impl CallArgs {
236 fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
237 if self.debug_trace_call {
238 self.tracing.resolve_call_tracer(config, verbosity)
239 } else {
240 self.tracing.resolve(config, verbosity)
241 }
242 }
243
244 pub async fn run(mut self) -> Result<()> {
245 self.validate_trace_args()?;
246
247 if self.rpc.curl {
249 if self.trace {
250 eyre::bail!(
251 "--trace cannot be combined with --curl; use --debug-trace-call --curl instead"
252 );
253 }
254 if self.browser.browser {
255 eyre::bail!("--browser cannot be combined with --curl; use --from <ADDRESS>");
256 }
257 if self.delegate {
258 eyre::bail!("--delegate cannot be combined with --curl");
261 }
262 if !self.tx.auth.is_empty() {
263 eyre::bail!("--auth cannot be combined with --curl");
264 }
265 return self.run_curl().await;
266 }
267
268 let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
269 let (mut config, mut evm_opts) = super::load_cast_config_and_evm_opts(figment)?;
270 evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
271 let has_tempo_session = self.tx.tempo.session_id()?.is_some();
272 let requires_tempo = self.tx.tempo.is_tempo() || has_tempo_session;
273 super::validate_tempo_network(&config, requires_tempo)?;
274 if requires_tempo {
275 evm_opts.networks = NetworkConfigs::with_tempo();
276 } else if !evm_opts.networks.has_network_selection()
277 && let Some(chain) = config.chain
278 {
279 evm_opts.networks =
280 evm_opts.networks.try_with_chain_id(chain.id()).map_err(eyre::Report::msg)?;
281 }
282 if has_tempo_session && self.will_disclose_auth() {
283 eyre::bail!("Tempo sessions cannot be combined with EIP-7702 authorizations");
284 }
285 let Some(auth_preflight) = self.preflight_auth_disclosure().await? else {
286 return Ok(());
287 };
288 evm_opts.infer_network_from_fork().await?;
289 if self.chain.is_none()
290 && let Some(chain_id) = evm_opts.env.chain_id
291 {
292 let chain = Chain::from_id(chain_id);
293 self.chain = Some(chain);
294 config.chain = Some(chain);
295 }
296
297 match evm_opts.networks.execution_network() {
298 NetworkVariant::Tempo => {
299 self.run_with_network_and_opts::<TempoEvmNetwork>(
300 config,
301 evm_opts,
302 auth_preflight,
303 ExecutorBuilder::<TempoEvmNetwork>::new(),
304 )
305 .await
306 }
307 #[cfg(feature = "base")]
308 NetworkVariant::Base => {
309 super::validate_base_transaction_options(&self.tx)?;
310 self.run_with_network_and_opts::<BaseEvmNetwork>(
311 config,
312 evm_opts,
313 auth_preflight,
314 ExecutorBuilder::<BaseEvmNetwork>::new(),
315 )
316 .await
317 }
318 #[cfg(feature = "monad")]
319 NetworkVariant::Monad => {
320 self.run_with_network_and_opts::<MonadEvmNetwork>(
321 config,
322 evm_opts,
323 auth_preflight,
324 ExecutorBuilder::<MonadEvmNetwork>::new(),
325 )
326 .await
327 }
328 #[cfg(feature = "optimism")]
329 NetworkVariant::Optimism => {
330 self.run_with_network_and_opts::<OpEvmNetwork>(
331 config,
332 evm_opts,
333 auth_preflight,
334 ExecutorBuilder::<OpEvmNetwork>::new(),
335 )
336 .await
337 }
338 NetworkVariant::Ethereum => {
339 self.run_with_network_and_opts::<EthEvmNetwork>(
340 config,
341 evm_opts,
342 auth_preflight,
343 ExecutorBuilder::<EthEvmNetwork>::new(),
344 )
345 .await
346 }
347 }
348 }
349
350 const fn will_disclose_auth(&self) -> bool {
353 !self.tx.auth.is_empty() && (!self.trace || matches!(self.tx.access_list, Some(None)))
354 }
355
356 async fn preflight_auth_disclosure(
361 &self,
362 ) -> Result<Option<(bool, Option<SenderKind<'static>>)>> {
363 if !self.will_disclose_auth() {
364 return Ok(Some((false, None)));
365 }
366
367 let sender = if self.browser.browser {
368 None
369 } else {
370 Some(SenderKind::from_wallet_opts(self.wallet.clone(), &self.tx.auth).await?)
371 };
372 let browser_sender = SenderKind::from(self.wallet.from.unwrap_or_default());
373 let validation_sender = sender.as_ref().unwrap_or(&browser_sender);
374 if !confirm_auth_rpc_disclosure_before_network_resolution(
375 &self.tx.auth,
376 validation_sender,
377 self.force,
378 )? {
379 return Ok(None);
380 }
381
382 Ok(Some((true, sender)))
383 }
384
385 fn validate_trace_args(&self) -> Result<()> {
386 if !self.trace
387 && !self.debug_trace_call
388 && (self.tracing.disable_labels
389 || self.tracing.compact_labels
390 || !self.tracing.labels.is_empty()
391 || self.tracing.trace_depth.is_some())
392 {
393 eyre::bail!("trace rendering options require `--trace` or `--debug-trace-call`");
394 }
395
396 if self.tracing.decode_internal && !self.trace {
397 eyre::bail!("`--decode-internal` requires `--trace`");
398 }
399
400 Ok(())
401 }
402
403 async fn run_with_network_and_opts<FEN: FoundryEvmNetwork>(
404 self,
405 mut config: Box<Config>,
406 evm_opts: EvmOpts,
407 (auth_confirmed, auth_sender): (bool, Option<SenderKind<'static>>),
408 executor_builder: ExecutorBuilder<FEN>,
409 ) -> Result<()> {
410 config.networks = evm_opts.networks;
411 let mut state_overrides = self.overrides.get_state_overrides()?;
412 let block_overrides = self.overrides.get_block_overrides()?;
413 config.tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
414 let tracing = config.tracing.clone();
415
416 let Self {
417 mut to,
418 mut sig,
419 mut args,
420 mut tx,
421 command,
422 block,
423 trace,
424 debug_trace_call,
425 evm_version,
426 debug,
427 data,
428 with_local_artifacts,
429 wallet,
430 browser,
431 force,
432 delegate,
433 ..
434 } = self;
435
436 if let Some(data) = data {
437 sig = Some(data);
438 }
439
440 let provider = ProviderBuilder::<FEN::Network>::from_config(&config)?.build()?;
441 let endpoint_identity =
442 if debug_trace_call { Some(evm_opts.fork_endpoint_identity().await?) } else { None };
443 let sender = match auth_sender {
444 Some(sender) => sender,
445 None => {
446 let chain_id = match config.chain {
447 Some(chain) => chain.id(),
448 None => provider.get_chain_id().await?,
449 };
450 read_only_sender::<FEN::Network>(&browser, wallet, &tx, chain_id).await?.0
451 }
452 };
453 let from = sender.address();
454
455 if delegate {
461 if command.is_some() {
462 eyre::bail!("`--delegate` cannot be combined with `--create`");
463 }
464 let Some(target) = to else {
465 eyre::bail!("`--delegate` requires a destination address");
466 };
467 let target = target.resolve(&provider).await?;
468 let overrides = state_overrides.get_or_insert_with(Default::default);
469 if overrides.get(&from).is_some_and(|account| account.code.is_some()) {
470 eyre::bail!("`--delegate` conflicts with `--override-code` for the sender {from}");
471 }
472 let code = match overrides.get(&target).and_then(|account| account.code.clone()) {
475 Some(code) => code,
476 None => provider.get_code_at(target).block_id(block.unwrap_or_default()).await?,
477 };
478 if code.is_empty() {
479 eyre::bail!("`--delegate` destination {target} has no code to delegate to");
480 }
481 overrides.entry(from).or_default().code = Some(code);
482 to = Some(NameOrAddress::Address(target));
483 }
484
485 let code = if let Some(CallSubcommands::Create {
486 code,
487 sig: create_sig,
488 args: create_args,
489 value,
490 }) = command
491 {
492 sig = create_sig;
493 args = create_args;
494 if let Some(value) = value {
495 tx.value = Some(value);
496 }
497 Some(code)
498 } else {
499 None
500 };
501
502 let builder = CastTxBuilder::new(&provider, tx, &config)
503 .await?
504 .with_to(to)
505 .await?
506 .with_code_sig_and_args(code, sig, args)
507 .await?
508 .raw();
509 let will_disclose =
510 (!trace && builder.has_auth()) || builder.will_disclose_auth_during_build();
511 if will_disclose
512 && !auth_confirmed
513 && !confirm_auth_rpc_disclosure(&builder, &sender, force)?
514 {
515 return Ok(());
516 }
517 let (mut tx, func) = builder.build(sender).await?;
518
519 if delegate {
522 tx.set_to(from);
523 }
524
525 if debug_trace_call {
526 let endpoint_identity = endpoint_identity
527 .ok_or_else(|| eyre::eyre!("remote trace endpoint identity was not captured"))?;
528 let requested_block = block.unwrap_or(BlockId::latest());
529 let fetched_block = provider.get_block(requested_block).await?;
530 let resolved_canonical_block =
531 if matches!(requested_block, BlockId::Number(_)) && !requested_block.is_pending() {
532 fetched_block.as_ref().map(block_num_hash)
533 } else {
534 None
535 };
536 let block = pin_remote_trace_block(
537 requested_block,
538 fetched_block.as_ref().map(|block| block.header().hash()),
539 )?;
540 let block_time_override = block_overrides.as_ref().and_then(|overrides| overrides.time);
541 let mut call_options = call_tracer_options();
542 let mut override_bytecode = AddressHashMap::<Bytes>::default();
546 if with_local_artifacts && let Some(overrides) = &state_overrides {
547 for (address, account) in overrides {
548 if let Some(code) = &account.code {
549 override_bytecode.insert(*address, code.clone());
550 }
551 }
552 }
553
554 if let Some(state_overrides) = state_overrides {
556 call_options = call_options.with_state_overrides(state_overrides);
557 }
558 if let Some(block_overrides) = block_overrides {
559 call_options = call_options.with_block_overrides(block_overrides);
560 }
561
562 let frame = call_tracer_frame(
563 provider.debug_trace_call(tx, block, call_options).await,
564 "debug_traceCall",
565 "drop `--debug-trace-call` to run the call locally with `--trace`",
566 "the requested block; use an archive endpoint, or target a more recent block with `--block`",
567 )?;
568
569 let arena = SparsedTraceArena {
570 arena: call_frame_to_arena(&frame, None),
571 ignored: Default::default(),
572 diagnostics: Default::default(),
573 debug_steps: None,
574 };
575 let result = TraceResult {
576 success: frame.error.is_none() && frame.revert_reason.is_none(),
577 traces: Some(vec![(TraceKind::Execution, arena)]),
578 gas_used: frame.gas_used.saturating_to(),
579 };
580
581 let contracts_bytecode = if with_local_artifacts {
586 let mut contracts_bytecode =
587 fetch_code_via_rpc(&provider, trace_addresses(&result), block).await;
588 contracts_bytecode.extend(override_bytecode);
591 contracts_bytecode
592 } else {
593 Default::default()
594 };
595 let final_endpoint_identity = evm_opts.fork_endpoint_identity().await?;
596 ensure_remote_trace_context_unchanged(&endpoint_identity, &final_endpoint_identity)?;
597
598 let chain = alloy_chains::Chain::from_id(endpoint_identity.source_chain_id);
601 let block_timestamp = block_time_override
602 .or_else(|| fetched_block.as_ref().map(|block| block.header().timestamp()));
603 let resolved_hardfork =
604 resolve_remote_trace_hardfork(config.hardfork, &endpoint_identity, block_timestamp);
605 if let Some(resolved_block) = resolved_canonical_block {
606 let canonical_block =
607 provider.get_block_by_number(resolved_block.number.into()).await?;
608 ensure_remote_trace_block_is_canonical(
609 resolved_block,
610 canonical_block.as_ref().map(block_num_hash),
611 )?;
612 }
613 return handle_traces(
614 result,
615 &config,
616 {
617 let context = TraceContext::new(
618 chain,
619 endpoint_identity.network_profile,
620 resolved_hardfork,
621 );
622 context.with_hardfork(context.decoding_hardfork(&config))
623 },
624 &contracts_bytecode,
625 &tracing,
626 with_local_artifacts,
627 false,
628 )
629 .await;
630 }
631
632 if trace {
633 if let Some(BlockId::Number(BlockNumberOrTag::Number(block_number))) = block {
634 config.fork_block_number = Some(block_number);
636 }
637
638 let create2_deployer = evm_opts.create2_deployer;
639 let mut fork = TracingExecutor::<FEN>::get_fork(&mut config, evm_opts).await?;
640 fork.evm_env.cfg_env.disable_block_gas_limit = true;
642 fork.evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
643
644 if let Some(block_overrides) = block_overrides {
645 if let Some(number) = block_overrides.number {
646 fork.evm_env.block_env.set_number(number.to());
647 }
648 if let Some(time) = block_overrides.time {
649 fork.evm_env.block_env.set_timestamp(U256::from(time));
650 }
651 }
652 fork.resolve_spec(&config, evm_version);
653 fork.extend_precompile_labels(&mut config);
654 let context = fork.context();
655
656 let trace_requirements = TraceRequirements::none()
657 .with_calls(true)
658 .with_debug(debug)
659 .with_decode_internal(if tracing.decode_internal {
660 InternalTraceMode::Full
661 } else {
662 InternalTraceMode::None
663 })
664 .with_state_changes(tracing.verbosity > 4);
665 let mut executor = fork.into_executor(
666 executor_builder,
667 trace_requirements,
668 create2_deployer,
669 state_overrides,
670 )?;
671
672 let value = tx.value().unwrap_or_default();
673 let input = tx.input().cloned().unwrap_or_default();
674 let tx_kind = tx.kind().expect("set by builder");
675
676 if let Some(gas_limit) = tx.gas_limit() {
681 executor.set_gas_limit(gas_limit);
682 }
683
684 let (mut evm_env, mut env_tx) = executor.prepare_call_env(from, tx_kind, input, value);
689 let priority_fee = tx.max_priority_fee_per_gas().unwrap_or_default();
690 let gas_price = tx.max_fee_per_gas().or(tx.gas_price()).unwrap_or_default();
691 if gas_price != 0 || priority_fee != 0 {
692 eyre::ensure!(
693 !config.networks.execution_network().is_monad(),
694 "Nonzero fees are not supported for local Monad traces; use --debug-trace-call instead"
695 );
696 evm_env.cfg_env.disable_fee_charge = true;
697 evm_env.block_env.set_basefee(executor.evm_env().block_env.basefee());
698 env_tx.set_gas_price(gas_price);
699 env_tx.set_gas_priority_fee(Some(priority_fee));
700 }
701 if let Some(max_fee_per_blob_gas) = tx.max_fee_per_blob_gas() {
702 env_tx.set_max_fee_per_blob_gas(max_fee_per_blob_gas);
703 }
704 if let Some(nonce) = tx.nonce() {
705 env_tx.set_nonce(nonce);
706 }
707 env_tx.set_tx_type(tx.output_tx_type().into());
708 if let Some(access_list) = tx.access_list().cloned() {
709 env_tx.set_access_list(access_list);
710 }
711 if let Some(auth) = tx.authorization_list().cloned() {
712 env_tx.set_signed_authorization(auth);
713 }
714
715 let trace = match tx_kind {
716 TxKind::Create => {
717 let deploy_result = executor.deploy_with_env(evm_env, env_tx, None);
718 TraceResult::try_from(deploy_result)?
719 }
720 TxKind::Call(_) => TraceResult::from_raw(
721 executor.transact_with_env(evm_env, env_tx)?,
722 TraceKind::Execution,
723 ),
724 };
725
726 let contracts_bytecode = fetch_contracts_bytecode_from_trace(&executor, &trace)?;
727 return handle_traces(
728 trace,
729 &config,
730 context,
731 &contracts_bytecode,
732 &tracing,
733 with_local_artifacts,
734 debug,
735 )
736 .await;
737 }
738
739 let mut call = provider
740 .call(tx.clone())
741 .block(block.unwrap_or_default())
742 .with_block_overrides_opt(block_overrides);
743 if let Some(state_override) = state_overrides {
744 call = call.overrides(state_override)
745 }
746
747 let res = match call.await {
748 Ok(res) => res,
749 Err(err) => {
750 let data = err.as_error_resp().and_then(|payload| payload.as_revert_data());
751 if let Some(data) = data {
752 let decoded = match RevertDecoder::new().maybe_decode_known(&data) {
753 Some(decoded) => Some(decoded),
754 None => crate::tx::decode_custom_error(&data).await.ok().flatten(),
755 };
756 if let Some(decoded) = decoded {
757 return Err(err).wrap_err(format!("execution reverted: {decoded}"));
758 }
759 }
760 return Err(err.into());
761 }
762 };
763 let decoded = match func.as_ref() {
764 Some(func) => match func.abi_decode_output(res.as_ref()) {
765 Ok(decoded) => decoded,
766 Err(err) => {
767 if res.is_empty() {
769 let Some(addr) = tx.to() else {
770 eyre::bail!("tx req is a contract deployment");
771 };
772 if let Ok(code) =
773 provider.get_code_at(addr).block_id(block.unwrap_or_default()).await
774 && code.is_empty()
775 {
776 eyre::bail!("contract {addr:?} does not have any code");
777 }
778 }
779 return Err(err).wrap_err(
780 "could not decode output; did you specify the wrong function return data type?"
781 );
782 }
783 },
784 None => vec![],
785 };
786
787 let response = if decoded.is_empty() {
789 res.to_string()
790 } else if shell::is_json() {
791 let tokens = decoded
792 .into_iter()
793 .map(|value| serialize_value_as_json(value, None, true))
794 .collect::<eyre::Result<Vec<_>>>()?;
795 serde_json::to_string_pretty(&tokens).unwrap()
796 } else {
797 decoded.iter().map(format_token).collect::<Vec<_>>().join("\n")
799 };
800
801 if response == "0x"
804 && !delegate
805 && let Some(contract_address) = tx.to()
806 && provider.get_code_at(contract_address).await?.is_empty()
807 {
808 sh_warn!("Contract code is empty")?;
809 }
810
811 print_raw_line(response)
812 }
813
814 async fn run_curl(self) -> Result<()> {
816 let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
817 let config = load_config_from_provider(figment)?;
818 let has_tempo_session = self.tx.tempo.session_id()?.is_some();
819 super::validate_tempo_network(&config, self.tx.tempo.is_tempo() || has_tempo_session)?;
820 if has_tempo_session {
821 eyre::bail!("--tempo.session/TEMPO_SESSION_ID cannot be combined with --curl");
822 }
823 let url = config.get_rpc_url_or_localhost_http()?;
824 let jwt = config.get_rpc_jwt_secret()?;
825
826 let data = if let Some(data) = &self.data {
828 hex::decode(data)?
829 } else if let Some(sig) = &self.sig {
830 match hex::decode(sig) {
832 Ok(data) => data,
833 Err(_) => encode_function_args(&get_func(sig)?, &self.args)?,
834 }
835 } else {
836 Vec::new()
837 };
838
839 let to = self.to.as_ref().map(|n| match n {
841 NameOrAddress::Address(addr) => Ok(*addr),
842 NameOrAddress::Name(name) => {
843 eyre::bail!("ENS names are not supported with --curl. Please use a raw address instead of '{}'", name);
844 }
845 }).transpose()?;
846
847 let legacy = self.tx.legacy
850 || (config.chain.is_some_and(|chain| chain.is_legacy()) && self.tx.auth.is_empty());
851 let mut call_request = TransactionRequest {
852 to: Some(to.map_or(TxKind::Create, TxKind::Call)),
853 from: self.wallet.from,
854 input: TransactionInput::new(data.into()).normalized_data(),
855 ..Default::default()
856 };
857 let tx_opts = TransactionOpts { tempo: Default::default(), ..self.tx };
860 tx_opts.apply::<Ethereum>(&mut call_request, legacy);
861
862 let block_param = self
863 .block
864 .map(|b| serde_json::to_value(b).unwrap_or(serde_json::json!("latest")))
865 .unwrap_or(serde_json::json!("latest"));
866
867 let (method, params) = if self.debug_trace_call {
872 let mut call_options = call_tracer_options();
873 if let Some(state_overrides) = self.overrides.get_state_overrides()? {
874 call_options = call_options.with_state_overrides(state_overrides);
875 }
876 if let Some(block_overrides) = self.overrides.get_block_overrides()? {
877 call_options = call_options.with_block_overrides(block_overrides);
878 }
879 ("debug_traceCall", serde_json::json!([call_request, block_param, call_options]))
880 } else {
881 ("eth_call", serde_json::json!([call_request, block_param]))
882 };
883
884 let curl_cmd = generate_curl_command(
885 url.as_ref(),
886 method,
887 params,
888 config.eth_rpc_headers.as_deref(),
889 jwt.as_deref(),
890 )?;
891
892 sh_println!("{}", curl_cmd)?;
893 Ok(())
894 }
895}
896
897fn pin_remote_trace_block(requested: BlockId, fetched_hash: Option<B256>) -> Result<BlockId> {
898 if requested.is_pending() {
899 return Ok(requested);
900 }
901
902 let fetched_hash = fetched_hash.ok_or_else(|| {
903 eyre::eyre!("block {requested:?} was not found while preparing the remote trace")
904 })?;
905 if let BlockId::Hash(requested_hash) = requested {
906 if requested_hash.block_hash != fetched_hash {
907 eyre::bail!(
908 "the RPC endpoint returned block {fetched_hash} for requested block {}; retry the command",
909 requested_hash.block_hash
910 );
911 }
912 return Ok(requested);
914 }
915
916 Ok(BlockId::hash(fetched_hash))
917}
918
919fn ensure_remote_trace_block_is_canonical(
920 expected: BlockNumHash,
921 actual: Option<BlockNumHash>,
922) -> Result<()> {
923 let Some(actual) = actual else {
924 eyre::bail!(
925 "block {} at {} changed canonicality while collecting its remote trace: the canonical block lookup no longer reports that height; retry the command",
926 expected.hash,
927 expected.number,
928 );
929 };
930 if actual != expected {
931 eyre::bail!(
932 "block {} at {} changed canonicality while collecting its remote trace: the canonical block lookup reported block {} at {}; retry the command",
933 expected.hash,
934 expected.number,
935 actual.hash,
936 actual.number,
937 );
938 }
939
940 Ok(())
941}
942
943impl figment::Provider for CallArgs {
944 fn metadata(&self) -> Metadata {
945 Metadata::named("CallArgs")
946 }
947
948 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
949 let mut map = Map::new();
950
951 if let Some(evm_version) = self.evm_version {
952 map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
953 }
954 if let Some(chain) = self.chain {
955 map.insert("chain_id".into(), chain.id().into());
956 }
957
958 Ok(Map::from([(Config::selected_profile(), map)]))
959 }
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use alloy_eips::RpcBlockHash;
966
967 #[test]
968 fn remote_trace_block_pinning() {
969 let hash = B256::repeat_byte(0x11);
970
971 assert_eq!(pin_remote_trace_block(BlockId::pending(), None).unwrap(), BlockId::pending());
973 for requested in [
974 BlockId::number(42),
975 BlockId::earliest(),
976 BlockId::latest(),
977 BlockId::safe(),
978 BlockId::finalized(),
979 ] {
980 assert_eq!(pin_remote_trace_block(requested, Some(hash)).unwrap(), BlockId::hash(hash));
981 }
982 let err = pin_remote_trace_block(BlockId::number(42), None).unwrap_err();
983 assert!(err.to_string().contains("was not found while preparing the remote trace"));
984
985 for require_canonical in [None, Some(false), Some(true)] {
987 let requested = BlockId::Hash(RpcBlockHash { block_hash: hash, require_canonical });
988 assert_eq!(pin_remote_trace_block(requested, Some(hash)).unwrap(), requested);
989 }
990 let err = pin_remote_trace_block(
991 BlockId::hash_canonical(B256::repeat_byte(0x33)),
992 Some(B256::repeat_byte(0x44)),
993 )
994 .unwrap_err();
995 assert!(err.to_string().contains("returned block"));
996 }
997
998 #[test]
999 fn remote_trace_block_must_remain_canonical() {
1000 let expected = BlockNumHash::new(42, B256::repeat_byte(0x55));
1001 ensure_remote_trace_block_is_canonical(expected, Some(expected)).unwrap();
1002
1003 let err = ensure_remote_trace_block_is_canonical(expected, None).unwrap_err();
1004 assert!(err.to_string().contains("no longer reports that height"), "{err}");
1005
1006 let reorged = BlockNumHash::new(42, B256::repeat_byte(0x66));
1007 let err = ensure_remote_trace_block_is_canonical(expected, Some(reorged)).unwrap_err();
1008 assert!(err.to_string().contains("changed canonicality"), "{err}");
1009 }
1010
1011 #[test]
1012 fn chain_is_merged_into_config() {
1013 let args = CallArgs::parse_from(["foundry-cli", "--chain", "1"]);
1014 let config = Config::from_provider(Config::figment().merge(&args)).unwrap();
1015
1016 assert_eq!(config.chain, Some(Chain::mainnet()));
1017 }
1018
1019 #[test]
1022 #[cfg(all(not(feature = "base"), feature = "optimism"))]
1023 fn chain_id_without_base_still_resolves_to_optimism() {
1024 for chain_id in [8453, 84532] {
1025 let networks = NetworkConfigs::default()
1026 .try_with_chain_id(chain_id)
1027 .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
1028 assert!(networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
1029 }
1030 }
1031
1032 #[test]
1033 fn debug_trace_call_ignores_configured_internal_decoding() {
1034 let args = CallArgs::parse_from(["foundry-cli", "--debug-trace-call"]);
1035 let config = TracingConfig { decode_internal: true, ..Default::default() };
1036
1037 assert!(!args.resolve_tracing(&config, 0).decode_internal);
1038 }
1039}