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::{Chain, 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 if config.chain.is_none()
79 && let Some(identity) = &evm_opts.fork_endpoint
80 {
81 config.chain = Some(Chain::from(identity.source_chain_id));
82 }
83
84 match evm_opts.networks.execution_network() {
85 NetworkVariant::Tempo => {
86 Box::pin(run_command_with_network::<TempoEvmNetwork>(
87 args,
88 config,
89 evm_opts,
90 ExecutorBuilder::<TempoEvmNetwork>::new(),
91 local_networks,
92 local_chain_id,
93 ))
94 .await
95 }
96 #[cfg(feature = "base")]
97 NetworkVariant::Base => {
98 Box::pin(run_command_with_network::<BaseEvmNetwork>(
99 args,
100 config,
101 evm_opts,
102 ExecutorBuilder::<BaseEvmNetwork>::new(),
103 local_networks,
104 local_chain_id,
105 ))
106 .await
107 }
108 #[cfg(feature = "monad")]
109 NetworkVariant::Monad => {
110 Box::pin(run_command_with_network::<MonadEvmNetwork>(
111 args,
112 config,
113 evm_opts,
114 ExecutorBuilder::<MonadEvmNetwork>::new(),
115 local_networks,
116 local_chain_id,
117 ))
118 .await
119 }
120 #[cfg(feature = "optimism")]
121 NetworkVariant::Optimism => {
122 Box::pin(run_command_with_network::<OpEvmNetwork>(
123 args,
124 config,
125 evm_opts,
126 ExecutorBuilder::<OpEvmNetwork>::new(),
127 local_networks,
128 local_chain_id,
129 ))
130 .await
131 }
132 NetworkVariant::Ethereum => {
133 Box::pin(run_command_with_network::<EthEvmNetwork>(
134 args,
135 config,
136 evm_opts,
137 ExecutorBuilder::<EthEvmNetwork>::new(),
138 local_networks,
139 local_chain_id,
140 ))
141 .await
142 }
143 }
144}
145
146fn infer_network_from_chain_id(
147 networks: NetworkConfigs,
148 chain_id: Option<u64>,
149) -> Result<NetworkConfigs> {
150 if let Some(chain_id) = chain_id {
151 networks.try_with_chain_id(chain_id).map_err(eyre::Report::msg)
152 } else {
153 Ok(networks)
154 }
155}
156
157async fn run_command_with_network<FEN: FoundryEvmNetwork>(
158 args: Chisel,
159 config: Config,
160 evm_opts: EvmOpts,
161 executor_builder: ExecutorBuilder<FEN>,
162 local_networks: NetworkConfigs,
163 local_chain_id: Option<u64>,
164) -> Result<()> {
165 let fork_network_is_inferred = evm_opts.fork_network_is_inferred;
166 let fork_chain_id_is_inferred = evm_opts.fork_chain_id_is_inferred;
167 let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
169 traces: config.verbosity > 0,
171 foundry_config: config,
172 no_vm: args.no_vm,
173 evm_opts,
174 executor_builder,
175 local_networks: Some(local_networks),
176 local_chain_id,
177 fork_network_is_inferred,
178 fork_chain_id_is_inferred,
179 resolved_hardfork: None,
180 source_chain_id: None,
181 cached_backend: None,
182 calldata: None,
183 ir_minimum: args.ir_minimum,
184 fork_url_required: false,
185 })?;
186
187 evaluate_prelude(&mut dispatcher, args.prelude).await?;
189
190 if let Some(cmd) = args.cmd {
191 try_cf!(handle_cli_command(&mut dispatcher, cmd).await?);
192 return Ok(());
193 }
194
195 let mut rl = Editor::<SolidityHelper, _>::new()?;
196 rl.set_helper(Some(dispatcher.helper.clone()));
197 rl.set_auto_add_history(true);
198 if let Some(path) = chisel_history_file() {
199 let _ = rl.load_history(&path);
200 }
201
202 sh_println!("Welcome to Chisel! Type `{}` to show available commands.", "!help".green())?;
203
204 let mut interrupt = false;
206 loop {
207 let prompt = dispatcher.get_prompt();
208 match rl.readline(prompt.as_ref()) {
209 Ok(line) => {
210 debug!("dispatching next line: {line}");
211 interrupt = false;
213
214 let r = dispatcher.dispatch(&line).await;
216 dispatcher.helper.set_errored(r.is_err());
217 match r {
218 Ok(ControlFlow::Continue(())) => {}
219 Ok(ControlFlow::Break(())) => break,
220 Err(e) => {
221 sh_err!("{}", foundry_common::errors::display_chain(&e))?;
222 }
223 }
224 }
225 Err(ReadlineError::Interrupted) => {
226 if interrupt {
227 break;
228 }
229 sh_println!("(To exit, press Ctrl+C again)")?;
230 interrupt = true;
231 }
232 Err(ReadlineError::Eof) => break,
233 Err(err) => {
234 sh_err!("{err}")?;
235 break;
236 }
237 }
238 }
239
240 if let Some(path) = chisel_history_file() {
241 let _ = rl.save_history(&path);
242 }
243
244 Ok(())
245}
246
247async fn evaluate_prelude<FEN: FoundryEvmNetwork>(
250 dispatcher: &mut ChiselDispatcher<FEN>,
251 maybe_prelude: Option<PathBuf>,
252) -> Result<()> {
253 let Some(prelude_dir) = maybe_prelude else { return Ok(()) };
254 if prelude_dir.is_file() {
255 sh_println!("{} {}", "Loading prelude source file:".yellow(), prelude_dir.display())?;
256 try_cf!(load_prelude_file(dispatcher, prelude_dir).await?);
257 sh_println!("{}\n", "Prelude source file loaded successfully!".green())?;
258 } else {
259 let prelude_sources = fs::files_with_ext(&prelude_dir, "sol");
260 let mut print_success_msg = false;
261 for source_file in prelude_sources {
262 print_success_msg = true;
263 sh_println!("{} {}", "Loading prelude source file:".yellow(), source_file.display())?;
264 try_cf!(load_prelude_file(dispatcher, source_file).await?);
265 }
266
267 if print_success_msg {
268 sh_println!("{}\n", "All prelude source files loaded successfully!".green())?;
269 }
270 }
271 Ok(())
272}
273
274async fn load_prelude_file<FEN: FoundryEvmNetwork>(
276 dispatcher: &mut ChiselDispatcher<FEN>,
277 file: PathBuf,
278) -> Result<ControlFlow<()>> {
279 let prelude = fs::read_to_string(file)
280 .wrap_err("Could not load source file. Are you sure this path is correct?")?;
281 dispatcher.dispatch_solidity(&prelude).await
282}
283
284async fn handle_cli_command<FEN: FoundryEvmNetwork>(
285 d: &mut ChiselDispatcher<FEN>,
286 cmd: ChiselSubcommand,
287) -> Result<ControlFlow<()>> {
288 match cmd {
289 ChiselSubcommand::List => d.dispatch_command(ChiselCommand::ListSessions).await,
290 ChiselSubcommand::Load { id } => d.dispatch_command(ChiselCommand::Load { id }).await,
291 ChiselSubcommand::View { id } => {
292 view_session(&id)?;
293 Ok(ControlFlow::Continue(()))
294 }
295 ChiselSubcommand::ClearCache => d.dispatch_command(ChiselCommand::ClearCache).await,
296 ChiselSubcommand::Eval { command } => d.dispatch(&command).await,
297 }
298}
299
300fn chisel_history_file() -> Option<PathBuf> {
301 foundry_config::Config::foundry_dir().map(|p| p.join(".chisel_history"))
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use clap::CommandFactory;
308
309 #[test]
310 fn verify_cli() {
311 Chisel::command().debug_assert();
312 }
313
314 #[test]
317 #[cfg(all(not(feature = "base"), feature = "optimism"))]
318 fn chain_id_without_base_still_resolves_to_optimism() {
319 for chain_id in [8453, 84532] {
320 let networks = infer_network_from_chain_id(NetworkConfigs::default(), Some(chain_id))
321 .unwrap_or_else(|error| panic!("chain ID {chain_id} must still resolve: {error}"));
322 assert!(networks.is_optimism(), "chain ID {chain_id} must resolve to Optimism");
323 }
324 }
325
326 #[test]
327 #[cfg(not(feature = "monad"))]
328 fn chain_id_rejects_disabled_monad_network() {
329 let error = infer_network_from_chain_id(NetworkConfigs::default(), Some(143)).unwrap_err();
330
331 assert_eq!(
332 error.to_string(),
333 "cannot infer execution network from chain ID 143: network family `monad` is not \
334 enabled in this build"
335 );
336 }
337
338 #[test]
339 fn explicit_ethereum_overrides_chain_id_inference() {
340 let ethereum = NetworkConfigs::with_ethereum();
341 for chain_id in [8453, 143] {
342 assert_eq!(infer_network_from_chain_id(ethereum, Some(chain_id)).unwrap(), ethereum);
343 }
344 }
345
346 #[tokio::test]
347 async fn prelude_does_not_dispatch_chisel_commands() {
348 let dir = tempfile::tempdir().unwrap();
349 let file = dir.path().join("prelude.sol");
350 std::fs::write(&file, "!calldata 0x00").unwrap();
351 let config = crate::source::SessionSourceConfig::<EthEvmNetwork> {
352 foundry_config: Config {
353 solc: Some(foundry_config::SolcReq::Version(semver::Version::new(0, 8, 29))),
354 ..Default::default()
355 },
356 ..Default::default()
357 };
358 let mut dispatcher = ChiselDispatcher::new(config).unwrap();
359
360 assert!(load_prelude_file(&mut dispatcher, file).await.is_err());
361 }
362}