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