Skip to main content

chisel/
dispatcher.rs

1//! Dispatcher
2//!
3//! This module contains the `ChiselDispatcher` struct, which handles the dispatching
4//! of both builtin commands and Solidity snippets.
5
6use crate::{
7    executor::InspectResult,
8    prelude::{ChiselCommand, ChiselResult, ChiselSession, SessionSourceConfig, SolidityHelper},
9    source::SessionSource,
10};
11use alloy_primitives::{Address, hex};
12use eyre::{Context, Result};
13use forge_fmt::FormatterConfig;
14use foundry_cli::utils::fetch_abi_from_etherscan;
15use foundry_config::{Chain, Config, RpcEndpointUrl};
16use foundry_evm::{
17    core::evm::FoundryEvmNetwork,
18    decode::decode_console_logs,
19    hardforks::{ExecutionSpec, TempoHardfork},
20    traces::{
21        CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, decode_trace_arena,
22        identifier::{SignaturesIdentifier, TraceIdentifiers},
23        render_trace_arena,
24    },
25};
26use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
27use reqwest::Url;
28use solar::{
29    parse::lexer::token::{RawLiteralKind, RawTokenKind},
30    sema::ast::Base,
31};
32use std::{
33    borrow::Cow,
34    io::Write,
35    ops::ControlFlow,
36    path::{Path, PathBuf},
37    process::Command,
38};
39use tempfile::Builder;
40use yansi::Paint;
41
42/// Prompt arrow character.
43pub const PROMPT_ARROW: char = '➜';
44/// Prompt arrow string.
45pub const PROMPT_ARROW_STR: &str = "➜";
46const DEFAULT_PROMPT: &str = "➜ ";
47
48/// Command leader character
49pub const COMMAND_LEADER: char = '!';
50/// Chisel character
51pub const CHISEL_CHAR: &str = "⚒️";
52
53/// Chisel input dispatcher
54#[derive(Debug)]
55pub struct ChiselDispatcher<FEN: FoundryEvmNetwork> {
56    pub session: ChiselSession<FEN>,
57    pub helper: SolidityHelper,
58    last_result: Option<String>,
59}
60
61/// Helper function that formats solidity source with the given [FormatterConfig]
62pub fn format_source(source: &str, config: FormatterConfig) -> eyre::Result<String> {
63    let formatted = forge_fmt::format(source, config).into_result()?;
64    Ok(formatted)
65}
66
67impl<FEN: FoundryEvmNetwork> ChiselDispatcher<FEN> {
68    /// Associated public function to create a new Dispatcher instance
69    pub fn new(config: SessionSourceConfig<FEN>) -> eyre::Result<Self> {
70        let session = ChiselSession::new(config)?;
71        Ok(Self { session, helper: Default::default(), last_result: None })
72    }
73
74    /// Returns the optional ID of the current session.
75    pub fn id(&self) -> Option<&str> {
76        self.session.id.as_deref()
77    }
78
79    /// Returns the [`SessionSource`].
80    pub const fn source(&self) -> &SessionSource<FEN> {
81        &self.session.source
82    }
83
84    /// Returns the [`SessionSource`].
85    pub const fn source_mut(&mut self) -> &mut SessionSource<FEN> {
86        &mut self.session.source
87    }
88
89    fn format_source(&self) -> eyre::Result<String> {
90        format_source(
91            &self.source().to_repl_source(),
92            self.source().config.foundry_config.fmt.clone(),
93        )
94    }
95
96    /// Returns the prompt based on the current status of the Dispatcher
97    pub fn get_prompt(&self) -> Cow<'static, str> {
98        match self.session.id.as_deref() {
99            // `(ID: {id}) ➜ `
100            Some(id) => {
101                let mut prompt = String::with_capacity(DEFAULT_PROMPT.len() + id.len() + 7);
102                prompt.push_str("(ID: ");
103                prompt.push_str(id);
104                prompt.push_str(") ");
105                prompt.push_str(DEFAULT_PROMPT);
106                Cow::Owned(prompt)
107            }
108            // `➜ `
109            None => Cow::Borrowed(DEFAULT_PROMPT),
110        }
111    }
112
113    /// Dispatches an input as a command via [Self::dispatch_command] or as a Solidity snippet.
114    pub async fn dispatch(&mut self, mut input: &str) -> Result<ControlFlow<()>> {
115        if let Some(command) = input.strip_prefix(COMMAND_LEADER) {
116            return match ChiselCommand::parse(command) {
117                Ok(cmd) => self.dispatch_command(cmd).await,
118                Err(e) => {
119                    eyre::bail!("unrecognized command: {e}");
120                }
121            };
122        }
123
124        input = input.trim();
125        let (only_trivia, new_input) = preprocess(input, self.last_result.as_deref())?;
126        input = &*new_input;
127
128        let source = self.source_mut();
129
130        // If the input is a comment, add it to the run code so we avoid running with empty input
131        if only_trivia {
132            debug!(?input, "matched trivia");
133            if !input.is_empty() {
134                source.add_run_code(input);
135            }
136            return Ok(ControlFlow::Continue(()));
137        }
138
139        // Create new source with exact input appended and parse
140        let (new_source, do_execute) = source.clone_with_new_line(input.to_string())?;
141
142        let InspectResult { control_flow, formatted_output, last_result, replay_input } =
143            source.inspect(input).await?;
144        let (new_source, do_execute) = if let Some(input) = replay_input {
145            source.clone_with_new_line(input)?
146        } else {
147            (new_source, do_execute)
148        };
149        if let Some(last_result) = last_result {
150            self.last_result = Some(last_result);
151        }
152        if let Some(res) = &formatted_output {
153            let _ = sh_println!("{res}");
154        }
155        if control_flow.is_break() {
156            debug!(%input, ?formatted_output, "inspect success");
157            return Ok(ControlFlow::Continue(()));
158        }
159
160        if do_execute {
161            self.execute_and_replace(new_source).await?;
162        } else {
163            let out = new_source.build()?;
164            debug!(%input, ?out, "skipped execute and rebuild source");
165            *self.source_mut() = new_source;
166        }
167        Ok(ControlFlow::Continue(()))
168    }
169
170    /// Decodes traces in the given [`ChiselResult`].
171    // TODO: Add `known_contracts` back in.
172    pub async fn decode_traces(
173        session_config: &SessionSourceConfig<FEN>,
174        result: &mut ChiselResult,
175        // known_contracts: &ContractsByArtifact,
176    ) -> eyre::Result<CallTraceDecoder> {
177        let chain_id = session_config.source_chain_id.map(Chain::from);
178        let resolved_hardfork = session_config.resolved_hardfork;
179
180        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
181        let mut builder = CallTraceDecoderBuilder::new()
182            .with_labels(result.labeled_addresses.clone())
183            .with_signature_identifier(SignaturesIdentifier::from_config(
184                &session_config.foundry_config,
185            )?)
186            .with_networks(session_config.foundry_config.networks)
187            .with_chain_id(chain_id.map(|c| c.id()))
188            .with_tempo_hardfork(resolved_hardfork.and_then(TempoHardfork::from_foundry_hardfork));
189        #[cfg(feature = "monad")]
190        {
191            builder = builder.with_monad_hardfork(
192                resolved_hardfork
193                    .and_then(foundry_evm::hardforks::MonadHardfork::from_foundry_hardfork),
194            );
195        }
196        let mut decoder = builder.build();
197
198        let mut identifier =
199            TraceIdentifiers::new().with_external(&session_config.foundry_config, chain_id)?;
200        if !identifier.is_empty() {
201            for (_, trace) in &mut result.traces {
202                decoder.identify(trace, &mut identifier);
203            }
204        }
205        Ok(decoder)
206    }
207
208    /// Display the gathered traces of a REPL execution.
209    pub async fn show_traces(
210        decoder: &CallTraceDecoder,
211        result: &mut ChiselResult,
212    ) -> eyre::Result<()> {
213        if result.traces.is_empty() {
214            return Ok(());
215        }
216
217        sh_println!("{}", "Traces:".green())?;
218        for (kind, trace) in &mut result.traces {
219            // Display all Setup + Execution traces.
220            if matches!(kind, TraceKind::Setup | TraceKind::Execution) {
221                decode_trace_arena(trace, decoder).await;
222                sh_println!("{}", render_trace_arena(trace))?;
223            }
224        }
225
226        Ok(())
227    }
228
229    async fn execute_and_replace(&mut self, mut new_source: SessionSource<FEN>) -> Result<()> {
230        let mut res = new_source.execute().await?;
231        let failed = !res.success;
232        if new_source.config.traces || failed {
233            if let Ok(decoder) = Self::decode_traces(&new_source.config, &mut res).await {
234                Self::show_traces(&decoder, &mut res).await?;
235
236                // Show console logs, if there are any
237                let decoded_logs = decode_console_logs(&res.logs);
238                if !decoded_logs.is_empty() {
239                    let _ = sh_println!("{}", "Logs:".green());
240                    for log in decoded_logs {
241                        let _ = sh_println!("  {log}");
242                    }
243                }
244            }
245
246            if failed {
247                // If the contract execution failed, continue on without
248                // updating the source.
249                eyre::bail!("Failed to execute edited contract!");
250            }
251        }
252
253        // the code could be compiled, save it
254        *self.source_mut() = new_source;
255
256        Ok(())
257    }
258}
259
260/// [`ChiselCommand`] implementations.
261impl<FEN: FoundryEvmNetwork> ChiselDispatcher<FEN> {
262    /// Dispatches a [`ChiselCommand`].
263    pub async fn dispatch_command(&mut self, cmd: ChiselCommand) -> Result<ControlFlow<()>> {
264        match cmd {
265            ChiselCommand::Quit => Ok(ControlFlow::Break(())),
266            cmd => self.dispatch_command_impl(cmd).await.map(ControlFlow::Continue),
267        }
268    }
269
270    async fn dispatch_command_impl(&mut self, cmd: ChiselCommand) -> Result<()> {
271        match cmd {
272            ChiselCommand::Help => self.show_help(),
273            ChiselCommand::Quit => unreachable!(),
274            ChiselCommand::Clear => self.clear_source(),
275            ChiselCommand::Save { id } => self.save_session(id),
276            ChiselCommand::Load { id } => self.load_session(&id),
277            ChiselCommand::ListSessions => self.list_sessions(),
278            ChiselCommand::Source => self.show_source(),
279            ChiselCommand::ClearCache => self.clear_cache(),
280            ChiselCommand::Fork { url } => self.set_fork(url).await,
281            ChiselCommand::Traces => self.toggle_traces(),
282            ChiselCommand::Calldata { data } => self.set_calldata(data.as_deref()),
283            ChiselCommand::MemDump => self.show_mem_dump().await,
284            ChiselCommand::StackDump => self.show_stack_dump().await,
285            ChiselCommand::Export => self.export(),
286            ChiselCommand::Fetch { addr, name } => self.fetch_interface(addr, name).await,
287            ChiselCommand::Exec { command, args } => self.exec_command(command, args),
288            ChiselCommand::Edit => self.edit_session().await,
289            ChiselCommand::RawStack { var } => self.show_raw_stack(var).await,
290        }
291    }
292
293    pub(crate) fn show_help(&self) -> Result<()> {
294        sh_println!("{}", ChiselCommand::format_help())
295    }
296
297    pub(crate) fn clear_source(&mut self) -> Result<()> {
298        self.source_mut().clear();
299        self.last_result = None;
300        sh_println!("Cleared session!")
301    }
302
303    pub(crate) fn save_session(&mut self, id: Option<String>) -> Result<()> {
304        let previous_id = self.session.id.clone();
305
306        // If a new name was supplied, overwrite the ID of the current session.
307        if let Some(id) = id {
308            self.session.id = Some(id);
309        }
310
311        let new_cache_file = match self.session.write() {
312            Ok(path) => path,
313            Err(error) => {
314                self.session.id = previous_id;
315                return Err(error);
316            }
317        };
318
319        if let (Some(previous_id), Some(current_id)) = (previous_id, self.session.id.as_deref())
320            && previous_id != current_id
321        {
322            let old_cache_file =
323                format!("{}chisel-{previous_id}.json", ChiselSession::<FEN>::cache_dir()?);
324            let same_cache_file = std::fs::canonicalize(&old_cache_file).ok()
325                == std::fs::canonicalize(&new_cache_file).ok();
326            if !same_cache_file {
327                ChiselSession::<FEN>::remove_cached_session(&previous_id)?;
328            }
329        }
330
331        sh_println!("Saved session to cache with ID = {}", self.session.id.as_ref().unwrap())
332    }
333
334    pub(crate) fn load_session(&mut self, id: &str) -> Result<()> {
335        // Try to save the current session before loading another.
336        // Don't save an empty session.
337        if !self.source().run_code.is_empty() {
338            self.session.write()?;
339            sh_println!("{}", "Saved current session!".green())?;
340        }
341
342        let mut new_session = match id {
343            "latest" => ChiselSession::<FEN>::latest(),
344            id => ChiselSession::<FEN>::load(id),
345        }
346        .wrap_err("failed to load session")?;
347
348        ensure_loaded_session_network_matches(
349            &self.session.source.config.foundry_config,
350            &new_session.source.config.foundry_config,
351            id,
352        )?;
353        new_session.source.config.initialize_local_context();
354        new_session.source.build()?;
355        self.session = new_session;
356        self.last_result = None;
357        sh_println!("Loaded Chisel session! (ID = {})", self.session.id.as_ref().unwrap())
358    }
359
360    pub(crate) fn list_sessions(&self) -> Result<()> {
361        let sessions = ChiselSession::<FEN>::get_sessions()?;
362        if sessions.is_empty() {
363            eyre::bail!("No sessions found. Use the `!save` command to save a session.");
364        }
365        sh_println!(
366            "{}\n{}",
367            format!("{CHISEL_CHAR} Chisel Sessions").cyan(),
368            sessions
369                .iter()
370                .map(|(time, name)| format!("{} - {}", format!("{time:?}").blue(), name))
371                .collect::<Vec<String>>()
372                .join("\n")
373        )
374    }
375
376    pub(crate) fn show_source(&self) -> Result<()> {
377        let formatted = self.format_source().wrap_err("failed to format session source")?;
378        let highlighted = self.helper.highlight(&formatted);
379        sh_println!("{highlighted}")
380    }
381
382    pub(crate) fn clear_cache(&mut self) -> Result<()> {
383        ChiselSession::<FEN>::clear_cache().wrap_err("failed to clear cache")?;
384        self.session.id = None;
385        sh_println!("Cleared chisel cache!")
386    }
387
388    pub(crate) async fn set_fork(&mut self, url: Option<String>) -> Result<()> {
389        self.source_mut().config.initialize_local_context();
390
391        let Some(url) = url else {
392            return self.clear_fork();
393        };
394
395        // If the argument is an RPC alias designated in the
396        // `[rpc_endpoints]` section of the `foundry.toml` within
397        // the pwd, use the URL matched to the key.
398        let endpoint = if let Some(endpoint) =
399            self.source_mut().config.foundry_config.rpc_endpoints.get(&url)
400        {
401            endpoint.clone()
402        } else {
403            RpcEndpointUrl::Env(url).into()
404        };
405        let fork_url = endpoint.resolve().url()?;
406
407        if let Err(e) = Url::parse(&fork_url) {
408            eyre::bail!("invalid fork URL: {e}");
409        }
410
411        let mut fork_opts = self.source().config.evm_opts.clone();
412        fork_opts.fork_url = Some(fork_url.clone());
413        fork_opts.fork_block_number = None;
414        fork_opts.fork_block_number_is_inferred = false;
415        let explicit_network =
416            fork_opts.networks.has_network_selection() && !fork_opts.fork_network_is_inferred;
417        let identity = fork_opts.discover_fork_endpoint().await?;
418        let target = identity.network;
419        let current_opts = &self.source().config.evm_opts;
420        let current = network_variant(current_opts.networks);
421        ensure_fork_network_matches(current, target)?;
422
423        let networks = if explicit_network {
424            current_opts.networks
425        } else {
426            current_opts.networks.with_rpc_profile(identity.network_profile)
427        };
428        if fork_opts.env.chain_id.is_none() || fork_opts.fork_chain_id_is_inferred {
429            fork_opts.env.chain_id = Some(identity.execution_chain_id);
430            fork_opts.fork_chain_id_is_inferred = true;
431        }
432        fork_opts.networks = networks;
433        fork_opts.fork_endpoint = Some(identity.clone());
434        fork_opts.fork_network_is_inferred = !explicit_network;
435        fork_opts.pin_fork_block().await?;
436        let chain_id_is_inferred = fork_opts.fork_chain_id_is_inferred;
437        let source = self.source_mut();
438        source.config.evm_opts = fork_opts;
439        source.config.fork_network_is_inferred = !explicit_network;
440        source.config.fork_chain_id_is_inferred = chain_id_is_inferred;
441        source.config.foundry_config.networks = networks;
442        source.config.foundry_config.chain = Some(Chain::from(identity.source_chain_id));
443        source.config.resolved_hardfork = None;
444        source.config.source_chain_id = None;
445        // Clear the backend so that it is re-instantiated with the new fork
446        // upon the next execution of the session source.
447        source.config.cached_backend = None;
448
449        sh_println!("Set fork URL to {}", fork_url.yellow())?;
450
451        Ok(())
452    }
453
454    fn clear_fork(&mut self) -> Result<()> {
455        let current = network_variant(self.source().config.evm_opts.networks);
456        let local_networks =
457            self.source().config.local_networks.unwrap_or(self.source().config.evm_opts.networks);
458        let target = network_variant(local_networks);
459        ensure_fork_network_matches(current, target)?;
460
461        let source = self.source_mut();
462        source.config.evm_opts.fork_url = None;
463        source.config.evm_opts.fork_block_number = None;
464        source.config.evm_opts.fork_block_number_is_inferred = false;
465        source.config.evm_opts.networks = local_networks;
466        source.config.evm_opts.env.chain_id = source.config.local_chain_id;
467        source.config.evm_opts.fork_network_is_inferred = false;
468        source.config.evm_opts.fork_chain_id_is_inferred = false;
469        source.config.fork_network_is_inferred = false;
470        source.config.fork_chain_id_is_inferred = false;
471        source.config.foundry_config.networks = local_networks;
472        source.config.foundry_config.chain = source.config.local_chain_id.map(Chain::from);
473        source.config.resolved_hardfork = None;
474        source.config.source_chain_id = None;
475        source.config.cached_backend = None;
476        sh_println!("Now using local environment.")
477    }
478
479    pub(crate) fn toggle_traces(&mut self) -> Result<()> {
480        let t = &mut self.source_mut().config.traces;
481        *t = !*t;
482        sh_println!("{} traces!", if *t { "Enabled" } else { "Disabled" })
483    }
484
485    pub(crate) fn set_calldata(&mut self, data: Option<&str>) -> Result<()> {
486        // remove empty space, double quotes, and 0x prefix
487        let arg = data
488            .map(|s| s.trim_matches(|c: char| c.is_whitespace() || c == '"' || c == '\''))
489            .map(|s| s.strip_prefix("0x").unwrap_or(s))
490            .unwrap_or("");
491
492        if arg.is_empty() {
493            self.source_mut().config.calldata = None;
494            sh_println!("Calldata cleared.")?;
495            return Ok(());
496        }
497
498        let calldata = hex::decode(arg);
499        match calldata {
500            Ok(calldata) => {
501                self.source_mut().config.calldata = Some(calldata);
502                sh_println!("Set calldata to '{}'", arg.yellow())
503            }
504            Err(e) => {
505                eyre::bail!("Invalid calldata: {e}");
506            }
507        }
508    }
509
510    pub(crate) async fn show_mem_dump(&mut self) -> Result<()> {
511        let res = self.source_mut().execute().await?;
512        let Some((_, mem)) = res.state.as_ref() else {
513            eyre::bail!("Run function is empty.");
514        };
515        for i in (0..mem.len()).step_by(32) {
516            let _ = sh_println!(
517                "{}: {}",
518                format!("[0x{:02x}:0x{:02x}]", i, i + 32).yellow(),
519                hex::encode_prefixed(&mem[i..i + 32]).cyan()
520            );
521        }
522        Ok(())
523    }
524
525    pub(crate) async fn show_stack_dump(&mut self) -> Result<()> {
526        let res = self.source_mut().execute().await?;
527        let Some((stack, _)) = res.state.as_ref() else {
528            eyre::bail!("Run function is empty.");
529        };
530        for i in (0..stack.len()).rev() {
531            let _ = sh_println!(
532                "{}: {}",
533                format!("[{}]", stack.len() - i - 1).yellow(),
534                format!("0x{:02x}", stack[i]).cyan()
535            );
536        }
537        Ok(())
538    }
539
540    pub(crate) fn export(&self) -> Result<()> {
541        // Check if the pwd is a foundry project
542        if !Path::new("foundry.toml").exists() {
543            eyre::bail!("Must be in a foundry project to export source to script.");
544        }
545
546        // Create "script" dir if it does not already exist.
547        if !Path::new("script").exists() {
548            std::fs::create_dir_all("script")?;
549        }
550
551        let formatted_source = self.format_source()?;
552        std::fs::write(PathBuf::from("script/REPL.s.sol"), formatted_source)?;
553        sh_println!("Exported session source to script/REPL.s.sol!")
554    }
555
556    /// Fetches an interface from Etherscan
557    pub(crate) async fn fetch_interface(&mut self, address: Address, name: String) -> Result<()> {
558        let abis = fetch_abi_from_etherscan(address, &self.source().config.foundry_config)
559            .await
560            .wrap_err("Failed to fetch ABI from Etherscan")?;
561        let (abi, _) = abis
562            .into_iter()
563            .next()
564            .ok_or_else(|| eyre::eyre!("No ABI found for address {address} on Etherscan"))?;
565        let code = forge_fmt::format(&abi.to_sol(&name, None), FormatterConfig::default())
566            .into_result()?;
567        self.source_mut().add_global_code(&code);
568        sh_println!("Added {address}'s interface to source as `{name}`")
569    }
570
571    pub(crate) fn exec_command(&self, command: String, args: Vec<String>) -> Result<()> {
572        let mut cmd = Command::new(command);
573        cmd.args(args);
574        let _ = cmd.status()?;
575        Ok(())
576    }
577
578    pub(crate) async fn edit_session(&mut self) -> Result<()> {
579        // create a temp file with the content of the run code
580        let mut tmp = Builder::new()
581            .prefix("chisel-")
582            .suffix(".sol")
583            .tempfile()
584            .wrap_err("Could not create temporary file")?;
585        tmp.as_file_mut()
586            .write_all(self.source().run_code.as_bytes())
587            .wrap_err("Could not write to temporary file")?;
588
589        // open the temp file with the editor
590        let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string());
591        let mut cmd = Command::new(editor);
592        cmd.arg(tmp.path());
593        let st = cmd.status()?;
594        if !st.success() {
595            eyre::bail!("Editor exited with {st}");
596        }
597
598        let edited_code = std::fs::read_to_string(tmp.path())?;
599        let mut new_source = self.source().clone();
600        new_source.clear_run();
601        new_source.add_run_code(&edited_code);
602
603        // if the editor exited successfully, try to compile the new code
604        self.execute_and_replace(new_source).await?;
605        sh_println!("Successfully edited `run()` function's body!")
606    }
607
608    pub(crate) async fn show_raw_stack(&mut self, var: String) -> Result<()> {
609        let source = self.source_mut();
610        let line = format!("bytes32 __raw__; assembly {{ __raw__ := {var} }}");
611        if let Ok((new_source, _)) = source.clone_with_new_line(line)
612            && let InspectResult { formatted_output: Some(res), .. } =
613                new_source.inspect("__raw__").await?
614        {
615            sh_println!("{res}")?;
616            return Ok(());
617        }
618
619        eyre::bail!("Variable must exist within `run()` function.");
620    }
621}
622
623fn config_network_name(config: &Config) -> &'static str {
624    config.networks.active_network_name().unwrap_or("ethereum")
625}
626
627fn network_variant(networks: NetworkConfigs) -> NetworkVariant {
628    networks.resolved_network().unwrap_or_default()
629}
630
631fn ensure_fork_network_matches(current: NetworkVariant, target: NetworkVariant) -> Result<()> {
632    if current != target {
633        eyre::bail!(
634            "cannot switch this Chisel session from network `{current}` to `{target}`. Restart \
635             Chisel with `--network {target}` or a fork URL for that network.",
636        );
637    }
638    Ok(())
639}
640
641fn ensure_loaded_session_network_matches(
642    current: &Config,
643    loaded: &Config,
644    id: &str,
645) -> Result<()> {
646    let current_network = config_network_name(current);
647    let loaded_network = config_network_name(loaded);
648    if current_network != loaded_network {
649        eyre::bail!(
650            "Chisel session `{id}` was saved for network `{loaded_network}`, but the current \
651             network is `{current_network}`. Rerun with `--network {loaded_network}` to load it.",
652        );
653    }
654    Ok(())
655}
656
657/// Expands the previous result, checksums addresses, and returns whether the input only contained
658/// trivia (comments, whitespace).
659fn preprocess<'a>(input: &'a str, last_result: Option<&str>) -> Result<(bool, Cow<'a, str>)> {
660    let mut only_trivia = true;
661    let mut replacements = Vec::new();
662    for (pos, token) in solar::parse::Cursor::new(input).with_position() {
663        use RawTokenKind::{BlockComment, LineComment, Literal, Whitespace};
664
665        if matches!(token.kind, Whitespace | LineComment { .. } | BlockComment { .. }) {
666            continue;
667        }
668        only_trivia = false;
669
670        let range = pos..pos + token.len as usize;
671        if &input[range.clone()] == "$_" {
672            let last_result = last_result.ok_or_else(|| eyre::eyre!("no previous result"))?;
673            replacements.push((range, format!("({last_result})")));
674            continue;
675        }
676
677        // Ensure that addresses are correctly checksummed.
678        if let Literal { kind: RawLiteralKind::Int { base: Base::Hexadecimal, .. } } = token.kind
679            && token.len == 42
680            && let Ok(addr) = input[range.clone()].parse::<Address>()
681        {
682            replacements.push((range, addr.to_checksum_buffer(None).to_string()));
683        }
684    }
685
686    if replacements.is_empty() {
687        Ok((only_trivia, Cow::Borrowed(input)))
688    } else {
689        let mut new_input = input.to_string();
690        for (range, replacement) in replacements.into_iter().rev() {
691            new_input.replace_range(range, &replacement);
692        }
693        Ok((only_trivia, Cow::Owned(new_input)))
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use foundry_evm::{core::evm::EthEvmNetwork, opts::EvmOpts};
701
702    fn config_with_network(network: Option<&str>) -> Config {
703        let mut config = Config::default();
704        if let Some(network) = network {
705            config.networks = serde_json::from_value(serde_json::json!({
706                "network": network,
707                "celo": false,
708                "bypass_prevrandao": false,
709            }))
710            .unwrap();
711        }
712        config
713    }
714
715    #[test]
716    fn config_network_name_defaults_to_ethereum() {
717        assert_eq!(config_network_name(&Config::default()), "ethereum");
718    }
719
720    #[test]
721    fn ensure_fork_network_matches_accepts_same_family() {
722        ensure_fork_network_matches(NetworkVariant::Ethereum, NetworkVariant::Ethereum).unwrap();
723        ensure_fork_network_matches(NetworkVariant::Tempo, NetworkVariant::Tempo).unwrap();
724    }
725
726    #[tokio::test(flavor = "multi_thread")]
727    async fn setting_fork_preserves_explicit_celo_context() {
728        let (_api, handle) = anvil::spawn(anvil::NodeConfig::test()).await;
729        let networks = NetworkConfigs::with_celo();
730        let config = SessionSourceConfig::<EthEvmNetwork> {
731            foundry_config: Config { networks, ..Default::default() },
732            evm_opts: EvmOpts { networks, ..Default::default() },
733            local_networks: Some(networks),
734            ..Default::default()
735        };
736        let mut dispatcher = ChiselDispatcher::new(config).unwrap();
737
738        dispatcher.set_fork(Some(handle.http_endpoint())).await.unwrap();
739        assert!(dispatcher.source().config.evm_opts.networks.is_celo());
740        assert!(!dispatcher.source().config.evm_opts.fork_network_is_inferred);
741
742        dispatcher.clear_fork().unwrap();
743        assert!(dispatcher.source().config.evm_opts.networks.is_celo());
744    }
745
746    #[test]
747    #[cfg(feature = "monad")]
748    fn ensure_fork_network_matches_rejects_cross_family_change() {
749        let err = ensure_fork_network_matches(NetworkVariant::Ethereum, NetworkVariant::Monad)
750            .unwrap_err();
751        assert_eq!(
752            err.to_string(),
753            "cannot switch this Chisel session from network `ethereum` to `monad`. Restart Chisel \
754             with `--network monad` or a fork URL for that network."
755        );
756    }
757
758    #[test]
759    #[cfg(feature = "monad")]
760    fn clearing_startup_fork_preserves_inferred_monad_context() {
761        let networks = NetworkConfigs::with_monad();
762        let evm_opts = EvmOpts {
763            fork_url: Some("http://localhost:8545".to_string()),
764            networks,
765            env: foundry_evm::opts::Env { chain_id: Some(143), ..Default::default() },
766            ..Default::default()
767        };
768        let config = SessionSourceConfig::<foundry_evm::core::evm::MonadEvmNetwork> {
769            foundry_config: Config {
770                solc: Some(foundry_config::SolcReq::Version(semver::Version::new(0, 8, 29))),
771                networks,
772                chain: Some(Chain::from(143u64)),
773                ..Default::default()
774            },
775            evm_opts,
776            local_networks: Some(networks),
777            local_chain_id: Some(143),
778            ..Default::default()
779        };
780        let mut dispatcher = ChiselDispatcher::new(config).unwrap();
781
782        dispatcher.clear_fork().unwrap();
783
784        let config = &dispatcher.source().config;
785        assert!(config.evm_opts.fork_url.is_none());
786        assert!(config.evm_opts.networks.is_monad());
787        assert_eq!(config.evm_opts.env.chain_id, Some(143));
788        assert!(config.foundry_config.networks.is_monad());
789        assert_eq!(config.foundry_config.chain.map(|chain| chain.id()), Some(143));
790    }
791
792    #[test]
793    fn ensure_loaded_session_network_matches_rejects_different_network() {
794        let current = config_with_network(None);
795        let loaded = config_with_network(Some("tempo"));
796
797        let err = ensure_loaded_session_network_matches(&current, &loaded, "42").unwrap_err();
798        assert_eq!(
799            err.to_string(),
800            "Chisel session `42` was saved for network `tempo`, but the current network is \
801             `ethereum`. Rerun with `--network tempo` to load it."
802        );
803    }
804
805    #[test]
806    #[cfg(feature = "monad")]
807    fn ensure_loaded_session_network_matches_rejects_monad_on_default_network() {
808        let current = config_with_network(None);
809        let loaded = config_with_network(Some("monad"));
810
811        let err = ensure_loaded_session_network_matches(&current, &loaded, "43").unwrap_err();
812        assert_eq!(
813            err.to_string(),
814            "Chisel session `43` was saved for network `monad`, but the current network is \
815             `ethereum`. Rerun with `--network monad` to load it."
816        );
817    }
818
819    #[test]
820    fn ensure_loaded_session_network_matches_accepts_same_network() {
821        let current = config_with_network(Some("tempo"));
822        let loaded = config_with_network(Some("tempo"));
823
824        ensure_loaded_session_network_matches(&current, &loaded, "42").unwrap();
825    }
826
827    #[test]
828    fn test_trivia() {
829        fn only_trivia(s: &str) -> bool {
830            let (only_trivia, _new_input) = preprocess(s, None).unwrap();
831            only_trivia
832        }
833        assert!(only_trivia("// line comment"));
834        assert!(only_trivia("  \n// line \tcomment\n"));
835        assert!(!only_trivia("// line \ncomment"));
836
837        assert!(only_trivia("/* block comment */"));
838        assert!(only_trivia(" \t\n  /* block \n \t comment */\n"));
839        assert!(!only_trivia("/* block \n \t comment */\nwith \tother"));
840    }
841
842    #[test]
843    fn test_last_result_preprocessing() {
844        let result = "abi.decode(hex\"2a\", (uint256))";
845        let (_, input) = preprocess("uint256 answer = $_;", Some(result)).unwrap();
846        assert_eq!(input, format!("uint256 answer = ({result});"));
847
848        let literal = r#"string memory value = "$_"; // $_"#;
849        let (_, input) = preprocess(literal, Some(result)).unwrap();
850        assert_eq!(input, literal);
851
852        assert_eq!(preprocess("$_", None).unwrap_err().to_string(), "no previous result");
853    }
854}