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