Skip to main content

chisel/
args.rs

1use crate::{
2    opts::{Chisel, ChiselSubcommand},
3    prelude::{ChiselCommand, ChiselDispatcher, SolidityHelper},
4};
5use eyre::{Context, Result};
6use foundry_cli::utils::{self, LoadConfig};
7use foundry_common::fs;
8use foundry_config::Config;
9#[cfg(feature = "optimism")]
10use foundry_evm::core::evm::OpEvmNetwork;
11use foundry_evm::{
12    core::evm::{EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork},
13    opts::EvmOpts,
14};
15use rustyline::{Editor, config::Configurer, error::ReadlineError};
16use std::{ops::ControlFlow, path::PathBuf};
17use yansi::Paint;
18
19/// Run the `chisel` command line interface.
20pub fn run() -> Result<()> {
21    // Pre-parse discovery flags run before `setup()` so they cannot be blocked
22    // by panic-handler / tracing init failures and avoid that init's cost.
23    foundry_cli::machine::check_machine();
24    foundry_cli::opts::GlobalArgs::check_introspect::<Chisel>();
25    foundry_cli::opts::GlobalArgs::check_markdown_help::<Chisel>();
26
27    setup()?;
28
29    let args = foundry_cli::parse_or_exit::<Chisel>();
30    args.global.init()?;
31    args.global.tokio_runtime().block_on(run_command(args))
32}
33
34/// Setup the global logger and other utilities.
35pub fn setup() -> Result<()> {
36    utils::common_setup();
37    utils::subscriber();
38
39    Ok(())
40}
41
42macro_rules! try_cf {
43    ($e:expr) => {
44        match $e {
45            ControlFlow::Continue(()) => {}
46            ControlFlow::Break(()) => return Ok(()),
47        }
48    };
49}
50
51/// Run the subcommand.
52pub async fn run_command(args: Chisel) -> Result<()> {
53    // Load configuration
54    let (mut config, mut evm_opts) = args.load_config_and_evm_opts()?;
55
56    if let Some(chain) = config.chain {
57        evm_opts.networks = evm_opts.networks.with_chain_id(chain.id());
58    }
59    evm_opts.infer_network_from_fork().await;
60    config.networks = evm_opts.networks;
61
62    if evm_opts.networks.is_tempo() {
63        return run_command_with_network::<TempoEvmNetwork>(args, config, evm_opts).await;
64    }
65
66    #[cfg(feature = "optimism")]
67    if evm_opts.networks.is_optimism() {
68        return run_command_with_network::<OpEvmNetwork>(args, config, evm_opts).await;
69    }
70
71    run_command_with_network::<EthEvmNetwork>(args, config, evm_opts).await
72}
73
74async fn run_command_with_network<FEN: FoundryEvmNetwork>(
75    args: Chisel,
76    config: Config,
77    evm_opts: EvmOpts,
78) -> Result<()> {
79    // Create a new cli dispatcher
80    let mut dispatcher = ChiselDispatcher::<FEN>::new(crate::source::SessionSourceConfig {
81        // Enable traces if any level of verbosity was passed
82        traces: config.verbosity > 0,
83        foundry_config: config,
84        no_vm: args.no_vm,
85        evm_opts,
86        backend: None,
87        calldata: None,
88        ir_minimum: args.ir_minimum,
89    })?;
90
91    // Execute prelude Solidity source files
92    evaluate_prelude(&mut dispatcher, args.prelude).await?;
93
94    if let Some(cmd) = args.cmd {
95        try_cf!(handle_cli_command(&mut dispatcher, cmd).await?);
96        return Ok(());
97    }
98
99    let mut rl = Editor::<SolidityHelper, _>::new()?;
100    rl.set_helper(Some(dispatcher.helper.clone()));
101    rl.set_auto_add_history(true);
102    if let Some(path) = chisel_history_file() {
103        let _ = rl.load_history(&path);
104    }
105
106    sh_println!("Welcome to Chisel! Type `{}` to show available commands.", "!help".green())?;
107
108    // REPL loop.
109    let mut interrupt = false;
110    loop {
111        match rl.readline(&dispatcher.get_prompt()) {
112            Ok(line) => {
113                debug!("dispatching next line: {line}");
114                // Clear interrupt flag.
115                interrupt = false;
116
117                // Dispatch and match results.
118                let r = dispatcher.dispatch(&line).await;
119                dispatcher.helper.set_errored(r.is_err());
120                match r {
121                    Ok(ControlFlow::Continue(())) => {}
122                    Ok(ControlFlow::Break(())) => break,
123                    Err(e) => {
124                        sh_err!("{}", foundry_common::errors::display_chain(&e))?;
125                    }
126                }
127            }
128            Err(ReadlineError::Interrupted) => {
129                if interrupt {
130                    break;
131                }
132                sh_println!("(To exit, press Ctrl+C again)")?;
133                interrupt = true;
134            }
135            Err(ReadlineError::Eof) => break,
136            Err(err) => {
137                sh_err!("{err}")?;
138                break;
139            }
140        }
141    }
142
143    if let Some(path) = chisel_history_file() {
144        let _ = rl.save_history(&path);
145    }
146
147    Ok(())
148}
149
150/// Evaluate multiple Solidity source files contained within a
151/// Chisel prelude directory.
152async fn evaluate_prelude<FEN: FoundryEvmNetwork>(
153    dispatcher: &mut ChiselDispatcher<FEN>,
154    maybe_prelude: Option<PathBuf>,
155) -> Result<()> {
156    let Some(prelude_dir) = maybe_prelude else { return Ok(()) };
157    if prelude_dir.is_file() {
158        sh_println!("{} {}", "Loading prelude source file:".yellow(), prelude_dir.display())?;
159        try_cf!(load_prelude_file(dispatcher, prelude_dir).await?);
160        sh_println!("{}\n", "Prelude source file loaded successfully!".green())?;
161    } else {
162        let prelude_sources = fs::files_with_ext(&prelude_dir, "sol");
163        let mut print_success_msg = false;
164        for source_file in prelude_sources {
165            print_success_msg = true;
166            sh_println!("{} {}", "Loading prelude source file:".yellow(), source_file.display())?;
167            try_cf!(load_prelude_file(dispatcher, source_file).await?);
168        }
169
170        if print_success_msg {
171            sh_println!("{}\n", "All prelude source files loaded successfully!".green())?;
172        }
173    }
174    Ok(())
175}
176
177/// Loads a single Solidity file into the prelude.
178async fn load_prelude_file<FEN: FoundryEvmNetwork>(
179    dispatcher: &mut ChiselDispatcher<FEN>,
180    file: PathBuf,
181) -> Result<ControlFlow<()>> {
182    let prelude = fs::read_to_string(file)
183        .wrap_err("Could not load source file. Are you sure this path is correct?")?;
184    dispatcher.dispatch(&prelude).await
185}
186
187async fn handle_cli_command<FEN: FoundryEvmNetwork>(
188    d: &mut ChiselDispatcher<FEN>,
189    cmd: ChiselSubcommand,
190) -> Result<ControlFlow<()>> {
191    match cmd {
192        ChiselSubcommand::List => d.dispatch_command(ChiselCommand::ListSessions).await,
193        ChiselSubcommand::Load { id } => d.dispatch_command(ChiselCommand::Load { id }).await,
194        ChiselSubcommand::View { id } => {
195            let ControlFlow::Continue(()) = d.dispatch_command(ChiselCommand::Load { id }).await?
196            else {
197                return Ok(ControlFlow::Break(()));
198            };
199            d.dispatch_command(ChiselCommand::Source).await
200        }
201        ChiselSubcommand::ClearCache => d.dispatch_command(ChiselCommand::ClearCache).await,
202        ChiselSubcommand::Eval { command } => d.dispatch(&command).await,
203    }
204}
205
206fn chisel_history_file() -> Option<PathBuf> {
207    foundry_config::Config::foundry_dir().map(|p| p.join(".chisel_history"))
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use clap::CommandFactory;
214
215    #[test]
216    fn verify_cli() {
217        Chisel::command().debug_assert();
218    }
219
220    /// Every `command_id` exposed by `chisel --introspect` MUST be unique.
221    #[test]
222    fn introspect_command_ids_are_unique() {
223        use foundry_cli::introspect::{CommandRegistry, build_document, duplicate_command_ids};
224        let cmd = Chisel::command();
225        let doc = build_document(&cmd, &CommandRegistry::EMPTY);
226        let dups = duplicate_command_ids(&doc);
227        assert!(dups.is_empty(), "duplicate chisel command_ids: {dups:?}");
228    }
229
230    /// `chisel --introspect` must produce a JSON document that parses back into
231    /// the canonical `IntrospectDocument` shape.
232    #[test]
233    fn introspect_document_is_valid_json() {
234        use foundry_cli::introspect::{
235            CommandRegistry, INTROSPECT_SCHEMA_ID, IntrospectDocument, render_introspect_document,
236        };
237        let cmd = Chisel::command();
238        let json = render_introspect_document(&cmd, &CommandRegistry::EMPTY);
239        let doc: IntrospectDocument = serde_json::from_str(&json).expect("valid JSON");
240        assert_eq!(doc.schema_id, INTROSPECT_SCHEMA_ID);
241        assert_eq!(doc.binary.name, "chisel");
242    }
243
244    /// Capability self-consistency for every `chisel` command.
245    #[test]
246    fn introspect_capabilities_are_consistent() {
247        use foundry_cli::introspect::{CommandRegistry, build_document, capability_violations};
248        let cmd = Chisel::command();
249        let doc = build_document(&cmd, &CommandRegistry::EMPTY);
250        let v = capability_violations(&doc);
251        assert!(v.is_empty(), "chisel capability violations: {v:?}");
252    }
253}