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