1use crate::{
2 dispatcher::view_session,
3 opts::{Chisel, ChiselSubcommand},
4 prelude::{ChiselCommand, ChiselDispatcher, SolidityHelper},
5};
6use clap::Parser;
7use eyre::{Context, Result};
8use foundry_cli::utils::{self, LoadConfig};
9use foundry_common::fs;
10use foundry_config::Config;
11use foundry_evm::{
12 core::evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork},
13 executors::ExecutorBuilder,
14 opts::EvmOpts,
15};
16use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
17use rustyline::{Editor, config::Configurer, error::ReadlineError};
18use std::{ops::ControlFlow, path::PathBuf};
19use yansi::Paint;
20
21#[cfg(feature = "base")]
22use foundry_evm::core::evm::BaseEvmNetwork;
23
24#[cfg(feature = "monad")]
25use foundry_evm::core::evm::MonadEvmNetwork;
26
27#[cfg(feature = "optimism")]
28use foundry_evm::core::evm::OpEvmNetwork;
29
30pub fn run() -> Result<()> {
32 foundry_cli::opts::GlobalArgs::check_markdown_help::<Chisel>();
33
34 let warnings = setup()?;
35
36 let args = Chisel::parse();
37 args.global.init()?;
38 for warning in warnings {
39 let _ = foundry_common::sh_warn!("{warning}");
40 }
41 args.global.tokio_runtime().block_on(run_command(args))
42}
43
44pub fn setup() -> Result<Vec<String>> {
46 let warnings = utils::common_setup();
47 utils::subscriber();
48
49 Ok(warnings)
50}
51
52macro_rules! try_cf {
53 ($e:expr) => {
54 match $e {
55 ControlFlow::Continue(()) => {}
56 ControlFlow::Break(()) => return Ok(()),
57 }
58 };
59}
60
61pub async fn run_command(args: Chisel) -> Result<()> {
63 if let Some(ChiselSubcommand::View { id }) = &args.cmd {
64 return view_session(id);
65 }
66
67 let (mut config, mut evm_opts) = args.load_config_and_evm_opts()?;
69
70 evm_opts.networks =
71 infer_network_from_chain_id(evm_opts.networks, config.chain.map(|chain| chain.id()))?;
72 evm_opts.infer_network_from_fork().await?;
73 evm_opts.pin_fork_block().await?;
74 config.networks = evm_opts.networks;
75 let local_networks = evm_opts.networks;
76 let local_chain_id = evm_opts.env.chain_id.or(config.chain.map(|chain| chain.id()));
77
78 match evm_opts.networks.execution_network() {
79 NetworkVariant::Tempo => {
80 Box::pin(run_command_with_network::<TempoEvmNetwork>(
81 args,
82 config,
83 evm_opts,
84 ExecutorBuilder::<TempoEvmNetwork>::new(),
85 local_networks,
86 local_chain_id,
87 ))
88 .await
89 }
90 #[cfg(feature = "base")]
91 NetworkVariant::Base => {
92 Box::pin(run_command_with_network::<BaseEvmNetwork>(
93 args,
94 config,
95 evm_opts,
96 ExecutorBuilder::<BaseEvmNetwork>::new(),
97 local_networks,
98 local_chain_id,
99 ))
100 .await
101 }
102 #[cfg(feature = "monad")]
103 NetworkVariant::Monad => {
104 Box::pin(run_command_with_network::<MonadEvmNetwork>(
105 args,
106 config,
107 evm_opts,
108 ExecutorBuilder::<MonadEvmNetwork>::new(),
109 local_networks,
110 local_chain_id,
111 ))
112 .await
113 }
114 #[cfg(feature = "optimism")]
115 NetworkVariant::Optimism => {
116 Box::pin(run_command_with_network::<OpEvmNetwork>(
117 args,
118 config,
119 evm_opts,
120 ExecutorBuilder::<OpEvmNetwork>::new(),
121 local_networks,
122 local_chain_id,
123 ))
124 .await
125 }
126 NetworkVariant::Ethereum => {
127 Box::pin(run_command_with_network::<EthEvmNetwork>(
128 args,
129 config,
130 evm_opts,
131 ExecutorBuilder::<EthEvmNetwork>::new(),
132 local_networks,
133 local_chain_id,
134 ))
135 .await
136 }
137 }
138}
139
140fn infer_network_from_chain_id(
141 networks: NetworkConfigs,
142 chain_id: Option<u64>,
143) -> Result<NetworkConfigs> {
144 if let Some(chain_id) = chain_id {
145 networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)
146 } else {
147 Ok(networks)
148 }
149}
150
151async fn run_command_with_network<FEN: FoundryEvmNetwork>(
152 args: Chisel,
153 config: Config,
154 evm_opts: EvmOpts,
155 executor_builder: ExecutorBuilder<FEN>,
156 local_networks: NetworkConfigs,
157 local_chain_id: Option<u64>,
158) -> Result<()> {
159 let fork_network_is_inferred = evm_opts.fork_network_is_inferred;
160 let fork_chain_id_is_inferred = evm_opts.fork_chain_id_is_inferred;
161 let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
163 traces: config.verbosity > 0,
165 foundry_config: config,
166 no_vm: args.no_vm,
167 evm_opts,
168 executor_builder,
169 local_networks: Some(local_networks),
170 local_chain_id,
171 fork_network_is_inferred,
172 fork_chain_id_is_inferred,
173 resolved_hardfork: None,
174 source_chain_id: None,
175 cached_backend: None,
176 calldata: None,
177 ir_minimum: args.ir_minimum,
178 fork_url_required: false,
179 })?;
180
181 evaluate_prelude(&mut dispatcher, args.prelude).await?;
183
184 if let Some(cmd) = args.cmd {
185 try_cf!(handle_cli_command(&mut dispatcher, cmd).await?);
186 return Ok(());
187 }
188
189 let mut rl = Editor::<SolidityHelper, _>::new()?;
190 rl.set_helper(Some(dispatcher.helper.clone()));
191 rl.set_auto_add_history(true);
192 if let Some(path) = chisel_history_file() {
193 let _ = rl.load_history(&path);
194 }
195
196 sh_println!("Welcome to Chisel! Type `{}` to show available commands.", "!help".green())?;
197
198 let mut interrupt = false;
200 loop {
201 let prompt = dispatcher.get_prompt();
202 match rl.readline(prompt.as_ref()) {
203 Ok(line) => {
204 debug!("dispatching next line: {line}");
205 interrupt = false;
207
208 let r = dispatcher.dispatch(&line).await;
210 dispatcher.helper.set_errored(r.is_err());
211 match r {
212 Ok(ControlFlow::Continue(())) => {}
213 Ok(ControlFlow::Break(())) => break,
214 Err(e) => {
215 sh_err!("{}", foundry_common::errors::display_chain(&e))?;
216 }
217 }
218 }
219 Err(ReadlineError::Interrupted) => {
220 if interrupt {
221 break;
222 }
223 sh_println!("(To exit, press Ctrl+C again)")?;
224 interrupt = true;
225 }
226 Err(ReadlineError::Eof) => break,
227 Err(err) => {
228 sh_err!("{err}")?;
229 break;
230 }
231 }
232 }
233
234 if let Some(path) = chisel_history_file() {
235 let _ = rl.save_history(&path);
236 }
237
238 Ok(())
239}
240
241async fn evaluate_prelude<FEN: FoundryEvmNetwork>(
244 dispatcher: &mut ChiselDispatcher<FEN>,
245 maybe_prelude: Option<PathBuf>,
246) -> Result<()> {
247 let Some(prelude_dir) = maybe_prelude else { return Ok(()) };
248 if prelude_dir.is_file() {
249 sh_println!("{} {}", "Loading prelude source file:".yellow(), prelude_dir.display())?;
250 try_cf!(load_prelude_file(dispatcher, prelude_dir).await?);
251 sh_println!("{}\n", "Prelude source file loaded successfully!".green())?;
252 } else {
253 let prelude_sources = fs::files_with_ext(&prelude_dir, "sol");
254 let mut print_success_msg = false;
255 for source_file in prelude_sources {
256 print_success_msg = true;
257 sh_println!("{} {}", "Loading prelude source file:".yellow(), source_file.display())?;
258 try_cf!(load_prelude_file(dispatcher, source_file).await?);
259 }
260
261 if print_success_msg {
262 sh_println!("{}\n", "All prelude source files loaded successfully!".green())?;
263 }
264 }
265 Ok(())
266}
267
268async fn load_prelude_file<FEN: FoundryEvmNetwork>(
270 dispatcher: &mut ChiselDispatcher<FEN>,
271 file: PathBuf,
272) -> Result<ControlFlow<()>> {
273 let prelude = fs::read_to_string(file)
274 .wrap_err("Could not load source file. Are you sure this path is correct?")?;
275 dispatcher.dispatch_solidity(&prelude).await
276}
277
278async fn handle_cli_command<FEN: FoundryEvmNetwork>(
279 d: &mut ChiselDispatcher<FEN>,
280 cmd: ChiselSubcommand,
281) -> Result<ControlFlow<()>> {
282 match cmd {
283 ChiselSubcommand::List => d.dispatch_command(ChiselCommand::ListSessions).await,
284 ChiselSubcommand::Load { id } => d.dispatch_command(ChiselCommand::Load { id }).await,
285 ChiselSubcommand::View { id } => {
286 view_session(&id)?;
287 Ok(ControlFlow::Continue(()))
288 }
289 ChiselSubcommand::ClearCache => d.dispatch_command(ChiselCommand::ClearCache).await,
290 ChiselSubcommand::Eval { command } => d.dispatch(&command).await,
291 }
292}
293
294fn chisel_history_file() -> Option<PathBuf> {
295 foundry_config::Config::foundry_dir().map(|p| p.join(".chisel_history"))
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use clap::CommandFactory;
302
303 #[test]
304 fn verify_cli() {
305 Chisel::command().debug_assert();
306 }
307
308 #[test]
311 #[cfg(all(not(feature = "base"), feature = "optimism"))]
312 fn chain_id_without_base_still_resolves_to_optimism() {
313 for chain_id in [8453, 84532] {
314 let networks = infer_network_from_chain_id(NetworkConfigs::default(), Some(chain_id))
315 .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
316 assert!(networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
317 }
318 }
319
320 #[test]
321 #[cfg(not(feature = "monad"))]
322 fn chain_id_rejects_disabled_monad_network() {
323 let error = infer_network_from_chain_id(NetworkConfigs::default(), Some(143)).unwrap_err();
324
325 assert_eq!(
326 error.to_string(),
327 "cannot infer execution network from chain ID 143: network family `monad` is not \
328 enabled in this build"
329 );
330 }
331
332 #[test]
333 fn explicit_ethereum_overrides_chain_id_inference() {
334 let ethereum = NetworkConfigs::with_ethereum();
335 for chain_id in [8453, 143] {
336 assert_eq!(infer_network_from_chain_id(ethereum, Some(chain_id)).unwrap(), ethereum);
337 }
338 }
339
340 #[tokio::test]
341 async fn prelude_does_not_dispatch_chisel_commands() {
342 let dir = tempfile::tempdir().unwrap();
343 let file = dir.path().join("prelude.sol");
344 std::fs::write(&file, "!calldata 0x00").unwrap();
345 let config = crate::source::SessionSourceConfig::<EthEvmNetwork> {
346 foundry_config: Config {
347 solc: Some(foundry_config::SolcReq::Version(semver::Version::new(0, 8, 29))),
348 ..Default::default()
349 },
350 ..Default::default()
351 };
352 let mut dispatcher = ChiselDispatcher::new(config).unwrap();
353
354 assert!(load_prelude_file(&mut dispatcher, file).await.is_err());
355 }
356}