Skip to main content

chisel/
args.rs

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
30/// Run the `chisel` command line interface.
31pub 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
44/// Setup the global logger and other utilities.
45pub 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
61/// Run the subcommand.
62pub async fn run_command(args: Chisel) -> Result<()> {
63    if let Some(ChiselSubcommand::View { id }) = &args.cmd {
64        return view_session(id);
65    }
66
67    // Load configuration
68    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    // Create a new cli dispatcher
162    let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
163        // Enable traces if any level of verbosity was passed
164        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    // Execute prelude Solidity source files
182    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    // REPL loop.
199    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                // Clear interrupt flag.
206                interrupt = false;
207
208                // Dispatch and match results.
209                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
241/// Evaluate multiple Solidity source files contained within a
242/// Chisel prelude directory.
243async 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
268/// Loads a single Solidity file into the prelude.
269async 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    /// Base chain IDs resolved to Optimism before Base support existed, so a build without the
309    /// `base` feature — which is what release binaries ship — must keep resolving them that way.
310    #[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}