1use 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
43pub const PROMPT_ARROW: char = '➜';
45pub const PROMPT_ARROW_STR: &str = "➜";
47const DEFAULT_PROMPT: &str = "➜ ";
48
49pub const COMMAND_LEADER: char = '!';
51pub const CHISEL_CHAR: &str = "⚒️";
53
54#[derive(Debug)]
56pub struct ChiselDispatcher<FEN: FoundryEvmNetwork> {
57 pub session: ChiselSession<FEN>,
58 pub helper: SolidityHelper,
59 last_result: Option<String>,
60 invocation_rpc: InvocationRpc,
62}
63
64pub 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 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 pub fn id(&self) -> Option<&str> {
80 self.session.id.as_deref()
81 }
82
83 pub const fn source(&self) -> &SessionSource<FEN> {
85 &self.session.source
86 }
87
88 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 pub fn get_prompt(&self) -> Cow<'static, str> {
102 match self.session.id.as_deref() {
103 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 None => Cow::Borrowed(DEFAULT_PROMPT),
114 }
115 }
116
117 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 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 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 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 pub async fn decode_traces(
189 session_config: &SessionSourceConfig<FEN>,
190 result: &mut ChiselResult,
191 ) -> 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 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 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 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 eyre::bail!("Failed to execute edited contract!");
258 }
259 }
260
261 *self.source_mut() = new_source;
263
264 Ok(())
265 }
266}
267
268impl<FEN: FoundryEvmNetwork> ChiselDispatcher<FEN> {
270 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 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 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 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 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 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 if !Path::new("foundry.toml").exists() {
565 eyre::bail!("Must be in a foundry project to export source to script.");
566 }
567
568 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 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 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 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 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
679fn 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 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
724pub(crate) fn view_session(id: &str) -> Result<()> {
726 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(¤t, &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(¤t, &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(¤t, &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 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 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}