1use alloy_chains::Chain;
2#[cfg(test)]
3use alloy_primitives::B256;
4use alloy_primitives::{Bytes, map::AddressHashMap};
5use foundry_cli::utils::{TraceResult, print_traces};
6use foundry_common::{ContractsByArtifactBuilder, compile::ProjectCompiler};
7use foundry_compilers::artifacts::output_selection::ContractOutputSelection;
8use foundry_config::{Config, FoundryHardfork, TracingConfig};
9use foundry_debugger::Debugger;
10use foundry_evm::{
11 hardforks::{ExecutionSpec, TempoHardfork},
12 opts::ForkEndpointIdentity,
13 traces::{
14 CallTraceDecoderBuilder, DebugTraceIdentifier,
15 debug::ContractSources,
16 identifier::{SignaturesIdentifier, TraceIdentifiers},
17 },
18};
19use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
20
21pub(crate) fn select_remote_trace_hardfork(
22 configured: Option<FoundryHardfork>,
23 endpoint: Option<FoundryHardfork>,
24 network: NetworkVariant,
25) -> Option<FoundryHardfork> {
26 let namespace = network.hardfork_namespace();
27 configured
28 .filter(|hardfork| hardfork.namespace() == namespace)
29 .or_else(|| endpoint.filter(|hardfork| hardfork.namespace() == namespace))
30}
31
32pub(crate) fn ensure_remote_trace_context_unchanged(
33 before: &ForkEndpointIdentity,
34 after: &ForkEndpointIdentity,
35) -> eyre::Result<()> {
36 if before != after {
37 eyre::bail!(
38 "the RPC endpoint changed execution context while the remote trace was being \
39 collected; retry the command"
40 );
41 }
42 Ok(())
43}
44
45#[expect(clippy::too_many_arguments)]
47pub(crate) async fn handle_traces(
48 mut result: TraceResult,
49 config: &Config,
50 chain: Chain,
51 contracts_bytecode: &AddressHashMap<Bytes>,
52 tracing: &TracingConfig,
53 with_local_artifacts: bool,
54 debug: bool,
55 hardfork: Option<FoundryHardfork>,
56 networks: NetworkConfigs,
57) -> eyre::Result<()> {
58 let (known_contracts, mut sources) = if with_local_artifacts {
59 let _ = sh_status!("Compiling project to generate artifacts");
61 let mut config = config.clone();
62 if debug && !config.extra_output.contains(&ContractOutputSelection::StorageLayout) {
63 config.extra_output.push(ContractOutputSelection::StorageLayout);
64 }
65 let project = config.project()?;
66 let compiler = ProjectCompiler::new();
67 let output = compiler.compile(&project)?;
68 (
69 Some(
70 ContractsByArtifactBuilder::new(
71 output.artifact_ids().map(|(id, artifact)| (id, artifact.into())),
72 )
73 .with_storage_layouts(output.artifact_ids().filter_map(|(id, artifact)| {
74 artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
75 }))
76 .build(),
77 ),
78 ContractSources::from_project_output(&output, project.root(), None)?,
79 )
80 } else {
81 (None, ContractSources::default())
82 };
83
84 let resolved_hardfork = hardfork.or(config.hardfork);
85 let execution_network = networks.execution_network();
86 let tempo_hardfork = resolved_hardfork.and_then(TempoHardfork::from_foundry_hardfork);
87 let is_tempo = execution_network.is_tempo();
88 #[cfg(feature = "monad")]
89 let is_monad = execution_network.is_monad();
90 #[cfg(feature = "monad")]
91 let monad_hardfork =
92 resolved_hardfork.and_then(foundry_evm::hardforks::MonadHardfork::from_foundry_hardfork);
93 let mut builder = CallTraceDecoderBuilder::new()
94 .with_tracing_config(tracing)
95 .with_signature_identifier(SignaturesIdentifier::from_config(config)?)
96 .with_networks(networks)
97 .with_chain_id(Some(chain.id()))
98 .with_tempo_hardfork(
99 tempo_hardfork.or_else(|| is_tempo.then(|| config.evm_spec_id::<TempoHardfork>())),
100 );
101 #[cfg(feature = "monad")]
102 {
103 builder = builder.with_monad_hardfork(monad_hardfork.or_else(|| {
104 is_monad.then(|| config.evm_spec_id::<foundry_evm::hardforks::MonadHardfork>())
105 }));
106 }
107 let mut identifier = TraceIdentifiers::new().with_external(config, Some(chain))?;
108 if let Some(contracts) = &known_contracts {
109 builder = builder.with_known_contracts(contracts);
110 identifier = identifier.with_local_and_bytecodes(contracts, contracts_bytecode);
111 }
112
113 let mut decoder = builder.build();
114
115 for (_, trace) in result.traces.as_deref_mut().unwrap_or_default() {
116 decoder.identify(trace, &mut identifier);
117 }
118
119 if tracing.decode_internal || debug {
120 if let Some(ref etherscan_identifier) = identifier.external {
121 sources.merge(etherscan_identifier.get_compiled_contracts().await?);
122 }
123
124 if debug {
125 let mut builder = Debugger::builder()
126 .traces(result.traces.expect("missing traces"))
127 .decoder(&decoder)
128 .sources(sources);
129 if let Some(known_contracts) = &known_contracts {
130 builder = builder.known_contracts(known_contracts);
131 }
132 let mut debugger = builder.build();
133 debugger.try_run_tui()?;
134 return Ok(());
135 }
136
137 decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
138 }
139
140 print_traces(
141 &mut result,
142 &decoder,
143 tracing.verbosity > 0,
144 tracing.verbosity > 4,
145 tracing.trace_depth,
146 )
147 .await?;
148
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn remote_trace_context_rejects_same_url_reset() {
158 let before = ForkEndpointIdentity {
159 endpoint: "http://localhost:8545".to_string(),
160 execution_chain_id: 1,
161 source_chain_id: 1,
162 network: NetworkVariant::Ethereum,
163 network_profile: foundry_evm_networks::NetworkConfigs::default(),
164 reported_hardfork: None,
165 hardfork: None,
166 instance_id: Some(B256::with_last_byte(1)),
167 source_fork_block_number: None,
168 source_fork_block_hash: None,
169 };
170 assert!(ensure_remote_trace_context_unchanged(&before, &before).is_ok());
171
172 let mut after = before.clone();
173 after.instance_id = Some(B256::with_last_byte(2));
174 assert!(ensure_remote_trace_context_unchanged(&before, &after).is_err());
175
176 let mut before_unknown = before;
177 before_unknown.instance_id = None;
178 before_unknown.reported_hardfork = Some("FutureA".to_string());
179 let mut after_unknown = before_unknown.clone();
180 after_unknown.reported_hardfork = Some("FutureB".to_string());
181 assert!(ensure_remote_trace_context_unchanged(&before_unknown, &after_unknown).is_err());
182 }
183
184 #[test]
185 #[cfg(feature = "monad")]
186 fn remote_trace_hardfork_ignores_cross_network_override() {
187 let ethereum = FoundryHardfork::Ethereum(foundry_evm::hardforks::EthereumHardfork::Cancun);
188 let monad_eight = FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadEight);
189 let monad_nine = FoundryHardfork::Monad(foundry_evm::hardforks::MonadHardfork::MonadNine);
190
191 assert_eq!(
192 select_remote_trace_hardfork(Some(ethereum), Some(monad_nine), NetworkVariant::Monad),
193 Some(monad_nine)
194 );
195 assert_eq!(
196 select_remote_trace_hardfork(
197 Some(monad_eight),
198 Some(monad_nine),
199 NetworkVariant::Monad
200 ),
201 Some(monad_eight)
202 );
203 }
204}