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