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