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