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