Skip to main content

chisel/
args.rs

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
29/// Run the `chisel` command line interface.
30pub 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
40/// Setup the global logger and other utilities.
41pub 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
57/// Run the subcommand.
58pub async fn run_command(args: Chisel) -> Result<()> {
59    // Load configuration
60    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    // Create a new cli dispatcher
154    let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
155        // Enable traces if any level of verbosity was passed
156        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    // Execute prelude Solidity source files
173    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    // REPL loop.
190    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                // Clear interrupt flag.
197                interrupt = false;
198
199                // Dispatch and match results.
200                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
232/// Evaluate multiple Solidity source files contained within a
233/// Chisel prelude directory.
234async 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
259/// Loads a single Solidity file into the prelude.
260async 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    /// Base chain IDs resolved to Optimism before Base support existed, so a build without the
303    /// `base` feature — which is what release binaries ship — must keep resolving them that way.
304    #[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}