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::{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
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    // Like `!fork`, use the fork's source chain for explorer lookups unless `--chain` was given.
78    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    // Create a new cli dispatcher
168    let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
169        // Enable traces if any level of verbosity was passed
170        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    // Execute prelude Solidity source files
188    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    // REPL loop.
205    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                // Clear interrupt flag.
212                interrupt = false;
213
214                // Dispatch and match results.
215                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
247/// Evaluate multiple Solidity source files contained within a
248/// Chisel prelude directory.
249async 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
274/// Loads a single Solidity file into the prelude.
275async 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    /// Base chain IDs resolved to Optimism before Base support existed, so a build without the
315    /// `base` feature — which is what release binaries ship — must keep resolving them that way.
316    #[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}