1use eyre::Result;
8use foundry_compilers::{
9 Artifact, ProjectCompileOutput,
10 artifacts::{ConfigurableContractArtifact, Source, Sources},
11 project::ProjectCompiler,
12 solc::Solc,
13};
14use foundry_config::{
15 Config, EtherscanConfigs, FoundryHardfork, RpcEndpoints, SolcReq, cache::CachedEndpoints,
16};
17use foundry_evm::{
18 backend::Backend,
19 core::{bytecode::InstIter, evm::FoundryEvmNetwork},
20 executors::ExecutorBuilder,
21 opts::EvmOpts,
22};
23use foundry_evm_networks::NetworkConfigs;
24use semver::Version;
25use serde::{Deserialize, Deserializer, Serialize, Serializer};
26use solar::{
27 ast::{ItemKind, StmtKind as AstStmtKind, yul},
28 interface::{Span, diagnostics::EmittedDiagnostics},
29 sema::{
30 CompilerRef,
31 hir::{Block, Contract, EventId, ItemId, Stmt, StmtKind as HirStmtKind},
32 ty::Gcx,
33 },
34};
35use std::{cell::OnceCell, fmt};
36use walkdir::WalkDir;
37
38pub const MIN_VM_VERSION: Version = Version::new(0, 6, 2);
40
41static VM_SOURCE: &str = include_str!("../../../testdata/utils/Vm.sol");
43
44pub struct GeneratedOutput {
46 output: ProjectCompileOutput,
47}
48
49impl fmt::Debug for GeneratedOutput {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.debug_struct("GeneratedOutput").finish_non_exhaustive()
52 }
53}
54
55impl GeneratedOutput {
56 pub fn enter<R: Send>(
58 &self,
59 f: impl for<'a, 'b, 'gcx> FnOnce(GeneratedOutputRef<'a, 'b, 'gcx>) -> R + Send,
60 ) -> R {
61 self.output
62 .parser()
63 .solc()
64 .compiler()
65 .enter(|c| f(GeneratedOutputRef { output: &self.output, compiler: c }))
66 }
67}
68
69pub struct GeneratedOutputRef<'a, 'b, 'gcx> {
71 output: &'a ProjectCompileOutput,
72 pub(crate) compiler: &'b CompilerRef<'gcx>,
73}
74
75impl<'gcx> GeneratedOutputRef<'_, '_, 'gcx> {
76 pub fn gcx(&self) -> Gcx<'gcx> {
77 self.compiler.gcx()
78 }
79
80 pub fn repl_contract(&self) -> Option<&ConfigurableContractArtifact> {
81 self.output.find_first("REPL")
82 }
83
84 pub fn repl_contract_hir(&self) -> Option<&'gcx Contract<'gcx>> {
86 self.gcx().hir.contracts().find(|c| c.name.as_str() == "REPL")
87 }
88
89 pub fn run_func_body(&self) -> Block<'gcx> {
91 let hir = &self.gcx().hir;
92 let c = self.repl_contract_hir().expect("REPL contract not found in HIR");
93 let f = c
94 .functions()
95 .find(|&f| {
96 let f = hir.function(f);
97 f.name.is_some_and(|n| n.as_str() == "run") && f.parameters.is_empty()
98 })
99 .expect("`run()` function not found in REPL contract");
100 hir.function(f).body.expect("`run()` function does not have a body")
101 }
102
103 pub fn get_event(&self, input: &str) -> Option<EventId> {
105 let hir = &self.gcx().hir;
106 let c = self.repl_contract_hir()?;
107 c.items.iter().find_map(|id| {
108 if let ItemId::Event(eid) = id
109 && hir.event(*eid).name.as_str() == input
110 {
111 Some(*eid)
112 } else {
113 None
114 }
115 })
116 }
117
118 pub fn final_pc(&self, contract: &ConfigurableContractArtifact) -> Result<Option<usize>> {
119 let deployed_bytecode = contract
120 .get_deployed_bytecode()
121 .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
122 let deployed_bytecode_bytes = deployed_bytecode
123 .bytes()
124 .ok_or_else(|| eyre::eyre!("No deployed bytecode found for `REPL` contract"))?;
125
126 let run_body = self.run_func_body();
128
129 let last_yul_return_span: Option<Span> = self.first_yul_return_span();
137
138 let Some(last_stmt) = run_body.last() else { return Ok(None) };
141
142 let source_stmt = match &last_stmt.kind {
150 HirStmtKind::UncheckedBlock(stmts) | HirStmtKind::Block(stmts) => {
151 if let Some(stmt) = stmts.last() {
152 stmt
153 } else {
154 &run_body[run_body.len().saturating_sub(2)]
158 }
159 }
160 _ => last_stmt,
161 };
162 let mut source_span =
169 if matches!(last_stmt.kind, HirStmtKind::AssemblyBlock(_) | HirStmtKind::Err(_))
170 && let Some(span) = self.trailing_assembly_last_stmt_span()
171 {
172 span
173 } else {
174 self.stmt_span_without_semicolon(source_stmt)
175 };
176
177 if let Some(yul_return_span) = last_yul_return_span
179 && yul_return_span.hi() < source_span.lo()
180 {
181 source_span = yul_return_span;
182 }
183
184 let result = self
187 .compiler
188 .sess()
189 .source_map()
190 .span_to_source(source_span)
191 .map_err(|e| eyre::eyre!("failed to resolve span: {e:?}"))?;
192 let range = result.data;
193 let offset = range.start as u32;
194 let length = range.len() as u32;
195 trace!(%offset, %length, "find pc");
196 let final_pc = contract
197 .get_source_map_deployed()
198 .ok_or_else(|| eyre::eyre!("No source map found for `REPL` contract"))??
199 .into_iter()
200 .zip(InstIter::new(deployed_bytecode_bytes).with_pc().map(|(pc, _)| pc))
201 .filter(|(s, _)| s.offset() == offset && s.length() == length)
202 .map(|(_, pc)| pc)
203 .max();
204 trace!(?final_pc);
205 Ok(final_pc)
206 }
207
208 fn stmt_span_without_semicolon(&self, stmt: &Stmt<'_>) -> Span {
210 match stmt.kind {
211 HirStmtKind::DeclSingle(id) => {
212 let decl = self.gcx().hir.variable(id);
213 if let Some(expr) = decl.initializer {
214 stmt.span.with_hi(expr.span.hi())
215 } else {
216 stmt.span
217 }
218 }
219 HirStmtKind::DeclMulti(_, expr) => stmt.span.with_hi(expr.span.hi()),
220 HirStmtKind::Expr(expr) => expr.span,
221 _ => stmt.span,
222 }
223 }
224
225 fn repl_run_ast_body(&self) -> Option<&'gcx solar::ast::Block<'gcx>> {
230 let contract = self.repl_contract_hir()?;
231 let source = self.gcx().sources.get(contract.source)?;
232 let ast = source.ast.as_ref()?;
233
234 let contract_ast = ast.items.iter().find_map(|i| match &i.kind {
235 ItemKind::Contract(c) if c.name.as_str() == "REPL" => Some(c),
236 _ => None,
237 })?;
238 contract_ast.body.iter().find_map(|i| match &i.kind {
239 ItemKind::Function(f)
240 if f.header.name.is_some_and(|n| n.as_str() == "run")
241 && f.header.parameters.is_empty() =>
242 {
243 f.body.as_ref()
244 }
245 _ => None,
246 })
247 }
248
249 fn first_yul_return_span(&self) -> Option<Span> {
252 let run_body = self.repl_run_ast_body()?;
253 for stmt in run_body.stmts.iter() {
254 let AstStmtKind::Assembly(asm) = &stmt.kind else { continue };
255 for ystmt in asm.block.stmts.iter() {
256 if let yul::StmtKind::Expr(e) = &ystmt.kind
257 && let yul::ExprKind::Call(call) = &e.kind
258 && call.name.as_str() == "return"
259 {
260 return Some(ystmt.span);
261 }
262 }
263 }
264 None
265 }
266
267 fn trailing_assembly_last_stmt_span(&self) -> Option<Span> {
273 let run_body = self.repl_run_ast_body()?;
274 let AstStmtKind::Assembly(asm) = &run_body.stmts.last()?.kind else { return None };
275 asm.block
276 .stmts
277 .iter()
278 .rev()
279 .find(|s| !matches!(s.kind, yul::StmtKind::VarDecl(_, _)))
280 .map(|s| s.span)
281 }
282}
283
284#[derive(Clone, Debug, Default, Serialize, Deserialize)]
289#[serde(remote = "Self", bound = "")]
290pub struct SessionSourceConfig<FEN: FoundryEvmNetwork> {
291 #[serde(getter = "Self::persisted_foundry_config")]
293 pub foundry_config: Config,
294 #[serde(getter = "Self::persisted_evm_opts")]
296 pub evm_opts: EvmOpts,
297 #[serde(skip)]
299 pub executor_builder: ExecutorBuilder<FEN>,
300 #[serde(default)]
302 pub local_networks: Option<NetworkConfigs>,
303 #[serde(default)]
305 pub local_chain_id: Option<u64>,
306 #[serde(default)]
308 pub fork_network_is_inferred: bool,
309 #[serde(default)]
311 pub fork_chain_id_is_inferred: bool,
312 #[serde(skip)]
314 pub resolved_hardfork: Option<FoundryHardfork>,
315 #[serde(skip)]
317 pub source_chain_id: Option<u64>,
318 pub no_vm: bool,
320 #[serde(skip)]
322 pub(crate) cached_backend: Option<Backend<FEN>>,
323 pub traces: bool,
325 pub calldata: Option<Vec<u8>>,
327 pub ir_minimum: bool,
332 #[serde(default, getter = "Self::persisted_fork_url_required")]
334 pub(crate) fork_url_required: bool,
335}
336
337impl<FEN: FoundryEvmNetwork> SessionSourceConfig<FEN> {
338 pub fn initialize_local_context(&mut self) {
340 self.evm_opts.fork_network_is_inferred = self.fork_network_is_inferred;
341 self.evm_opts.fork_chain_id_is_inferred = self.fork_chain_id_is_inferred;
342 if self.local_networks.is_none() {
343 self.local_networks = Some(self.evm_opts.networks);
344 self.local_chain_id =
345 self.evm_opts.env.chain_id.or(self.foundry_config.chain.map(|chain| chain.id()));
346 }
347 }
348
349 pub fn detect_solc(&mut self) -> Result<()> {
351 if self.foundry_config.solc.is_none() {
352 let version = Solc::ensure_installed(&"*".parse().unwrap())?;
353 self.foundry_config.solc = Some(SolcReq::Version(version));
354 }
355 if !self.no_vm
356 && let Some(version) = self.foundry_config.solc_version()
357 && version < MIN_VM_VERSION
358 {
359 info!(%version, minimum=%MIN_VM_VERSION, "Disabling VM injection");
360 self.no_vm = true;
361 }
362 Ok(())
363 }
364
365 pub(crate) fn clear_credentials(&mut self) {
367 self.fork_url_required |= self.evm_opts.fork_url.is_some();
368 let none = InvocationRpc::default();
369 none.apply_config_credentials(&mut self.foundry_config);
370 none.apply_evm_credentials(&mut self.evm_opts);
371 }
372
373 pub(crate) fn restore_credentials(&mut self, current: &InvocationRpc) -> Result<()> {
375 let forked = self.persisted_fork_url_required();
376 if forked && current.fork_url.is_none() {
377 eyre::bail!(
378 "this saved Chisel session requires a fork endpoint; use !fork <url> or restart Chisel with --fork-url to load it"
379 );
380 }
381 current.apply_config_credentials(&mut self.foundry_config);
382 current.apply_evm_credentials(&mut self.evm_opts);
383 current.apply_transport(&mut self.foundry_config, &mut self.evm_opts);
384 if !forked {
385 self.evm_opts.fork_url = None;
386 }
387 self.fork_url_required = false;
388 self.evm_opts.fork_endpoint = None;
389 self.evm_opts.expected_fork_endpoint = None;
390 self.resolved_hardfork = None;
391 self.source_chain_id = None;
392 self.cached_backend = None;
393 Ok(())
394 }
395
396 fn persisted_foundry_config(&self) -> Config {
397 let mut config = self.foundry_config.clone();
398 InvocationRpc::default().apply_config_credentials(&mut config);
399 config
400 }
401
402 fn persisted_evm_opts(&self) -> EvmOpts {
403 let mut evm_opts = self.evm_opts.clone();
404 InvocationRpc::default().apply_evm_credentials(&mut evm_opts);
405 evm_opts
406 }
407
408 const fn persisted_fork_url_required(&self) -> bool {
410 self.fork_url_required || self.evm_opts.fork_url.is_some()
411 }
412}
413
414impl<FEN: FoundryEvmNetwork> Serialize for SessionSourceConfig<FEN> {
415 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
416 Self::serialize(self, serializer)
417 }
418}
419
420impl<'de, FEN: FoundryEvmNetwork> Deserialize<'de> for SessionSourceConfig<FEN> {
421 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
422 Self::deserialize(deserializer)
423 }
424}
425
426#[derive(Clone, Debug, Default)]
431pub(crate) struct InvocationRpc {
432 eth_rpc_url: Option<String>,
433 eth_rpc_jwt: Option<String>,
434 eth_rpc_headers: Option<Vec<String>>,
435 etherscan_api_key: Option<String>,
436 etherscan: EtherscanConfigs,
437 rpc_endpoints: RpcEndpoints,
438 cached_endpoints: CachedEndpoints,
439 eth_rpc_timeout: Option<u64>,
440 eth_rpc_accept_invalid_certs: bool,
441 eth_rpc_no_proxy: bool,
442 fork_url: Option<String>,
443 fork_headers: Option<Vec<String>>,
444 rpc_jwt: Option<String>,
445 rpc_headers: Option<Vec<String>>,
446 rpc_timeout: Option<u64>,
447 rpc_accept_invalid_certs: bool,
448 rpc_no_proxy: bool,
449}
450
451impl InvocationRpc {
452 pub(crate) fn capture(config: &Config, evm_opts: &EvmOpts) -> Self {
454 Self {
455 eth_rpc_url: config.eth_rpc_url.clone(),
456 eth_rpc_jwt: config.eth_rpc_jwt.clone(),
457 eth_rpc_headers: config.eth_rpc_headers.clone(),
458 etherscan_api_key: config.etherscan_api_key.clone(),
459 etherscan: config.etherscan.clone(),
460 rpc_endpoints: config.rpc_endpoints.clone(),
461 cached_endpoints: config.rpc_storage_caching.endpoints.clone(),
462 eth_rpc_timeout: config.eth_rpc_timeout,
463 eth_rpc_accept_invalid_certs: config.eth_rpc_accept_invalid_certs,
464 eth_rpc_no_proxy: config.eth_rpc_no_proxy,
465 fork_url: evm_opts.fork_url.clone(),
466 fork_headers: evm_opts.fork_headers.clone(),
467 rpc_jwt: evm_opts.rpc_jwt.clone(),
468 rpc_headers: evm_opts.rpc_headers.clone(),
469 rpc_timeout: evm_opts.rpc_timeout,
470 rpc_accept_invalid_certs: evm_opts.rpc_accept_invalid_certs,
471 rpc_no_proxy: evm_opts.rpc_no_proxy,
472 }
473 }
474
475 pub(crate) fn set_fork(&mut self, url: String, headers: Option<Vec<String>>) {
477 self.eth_rpc_url = Some(url.clone());
478 self.fork_url = Some(url);
479 self.fork_headers = headers;
480 }
481
482 fn apply_config_credentials(&self, config: &mut Config) {
483 config.eth_rpc_url.clone_from(&self.eth_rpc_url);
484 config.eth_rpc_jwt.clone_from(&self.eth_rpc_jwt);
485 config.eth_rpc_headers.clone_from(&self.eth_rpc_headers);
486 config.etherscan_api_key.clone_from(&self.etherscan_api_key);
487 config.etherscan.clone_from(&self.etherscan);
488 config.rpc_endpoints.clone_from(&self.rpc_endpoints);
489 config.rpc_storage_caching.endpoints.clone_from(&self.cached_endpoints);
490 }
491
492 fn apply_evm_credentials(&self, evm_opts: &mut EvmOpts) {
493 evm_opts.fork_url.clone_from(&self.fork_url);
494 evm_opts.fork_headers.clone_from(&self.fork_headers);
495 evm_opts.rpc_jwt.clone_from(&self.rpc_jwt);
496 evm_opts.rpc_headers.clone_from(&self.rpc_headers);
497 }
498
499 const fn apply_transport(&self, config: &mut Config, evm_opts: &mut EvmOpts) {
500 config.eth_rpc_timeout = self.eth_rpc_timeout;
501 config.eth_rpc_accept_invalid_certs = self.eth_rpc_accept_invalid_certs;
502 config.eth_rpc_no_proxy = self.eth_rpc_no_proxy;
503 evm_opts.rpc_timeout = self.rpc_timeout;
504 evm_opts.rpc_accept_invalid_certs = self.rpc_accept_invalid_certs;
505 evm_opts.rpc_no_proxy = self.rpc_no_proxy;
506 }
507}
508
509#[derive(Debug, Serialize, Deserialize)]
513#[serde(bound = "")]
514pub struct SessionSource<FEN: FoundryEvmNetwork> {
515 pub file_name: String,
517 pub contract_name: String,
519
520 pub config: SessionSourceConfig<FEN>,
522
523 pub global_code: String,
527 pub contract_code: String,
531 pub run_code: String,
533
534 #[serde(skip, default = "vm_source")]
536 vm_source: Source,
537 #[serde(skip)]
539 output: OnceCell<GeneratedOutput>,
540}
541
542fn vm_source() -> Source {
543 Source::new(VM_SOURCE)
544}
545
546impl<FEN: FoundryEvmNetwork> Clone for SessionSource<FEN> {
547 fn clone(&self) -> Self {
548 Self {
549 file_name: self.file_name.clone(),
550 contract_name: self.contract_name.clone(),
551 global_code: self.global_code.clone(),
552 contract_code: self.contract_code.clone(),
553 run_code: self.run_code.clone(),
554 config: self.config.clone(),
555 vm_source: self.vm_source.clone(),
556 output: Default::default(),
557 }
558 }
559}
560
561impl<FEN: FoundryEvmNetwork> SessionSource<FEN> {
562 pub fn new(mut config: SessionSourceConfig<FEN>) -> Result<Self> {
577 config.detect_solc()?;
578 Ok(Self {
579 file_name: "ReplContract.sol".to_string(),
580 contract_name: "REPL".to_string(),
581 config,
582 global_code: Default::default(),
583 contract_code: Default::default(),
584 run_code: Default::default(),
585 vm_source: vm_source(),
586 output: Default::default(),
587 })
588 }
589
590 pub fn clone_with_new_line(&self, mut content: String) -> Result<(Self, bool)> {
594 if let Some((new_source, fragment)) = self
595 .parse_fragment(&content)
596 .or_else(|| {
597 content.push(';');
598 self.parse_fragment(&content)
599 })
600 .or_else(|| {
601 content = content.trim_end().trim_end_matches(';').to_string();
602 self.parse_fragment(&content)
603 })
604 {
605 Ok((new_source, matches!(fragment, ParseTreeFragment::Function)))
606 } else {
607 eyre::bail!("\"{}\"", content.trim());
608 }
609 }
610
611 fn parse_fragment(&self, buffer: &str) -> Option<(Self, ParseTreeFragment)> {
614 #[track_caller]
615 fn debug_errors(errors: &EmittedDiagnostics) {
616 debug!("{errors}");
617 }
618
619 let mut this = self.clone();
620 match this.add_run_code(buffer).parse() {
621 Ok(()) => return Some((this, ParseTreeFragment::Function)),
622 Err(e) => debug_errors(&e),
623 }
624 this = self.clone();
625 match this.add_contract_code(buffer).parse() {
626 Ok(()) => return Some((this, ParseTreeFragment::Contract)),
627 Err(e) => debug_errors(&e),
628 }
629 this = self.clone();
630 match this.add_global_code(buffer).parse() {
631 Ok(()) => return Some((this, ParseTreeFragment::Source)),
632 Err(e) => debug_errors(&e),
633 }
634 None
635 }
636
637 pub fn add_global_code(&mut self, content: &str) -> &mut Self {
639 self.global_code.push_str(content.trim());
640 self.global_code.push('\n');
641 self.clear_output();
642 self
643 }
644
645 pub fn add_contract_code(&mut self, content: &str) -> &mut Self {
647 self.contract_code.push_str(content.trim());
648 self.contract_code.push('\n');
649 self.clear_output();
650 self
651 }
652
653 pub fn add_run_code(&mut self, content: &str) -> &mut Self {
655 self.run_code.push_str(content.trim());
656 self.run_code.push('\n');
657 self.clear_output();
658 self
659 }
660
661 pub fn clear(&mut self) {
663 String::clear(&mut self.global_code);
664 String::clear(&mut self.contract_code);
665 String::clear(&mut self.run_code);
666 self.clear_output();
667 }
668
669 pub fn clear_run(&mut self) -> &mut Self {
671 String::clear(&mut self.run_code);
672 self.clear_output();
673 self
674 }
675
676 fn clear_output(&mut self) {
677 self.output.take();
678 }
679
680 pub fn build(&self) -> Result<&GeneratedOutput> {
682 if let Some(output) = self.output.get() {
684 return Ok(output);
685 }
686 let output = self.compile()?;
687 let output = GeneratedOutput { output };
688 Ok(self.output.get_or_init(|| output))
689 }
690
691 #[cold]
693 fn compile(&self) -> Result<ProjectCompileOutput> {
694 let sources = self.get_sources();
695
696 let mut project = self.config.foundry_config.ephemeral_project()?;
697 self.config.foundry_config.disable_optimizations(&mut project, self.config.ir_minimum);
698 let mut output = ProjectCompiler::with_sources(&project, sources)?.compile()?;
699
700 if output.has_compiler_errors() {
701 eyre::bail!("{output}");
702 }
703
704 let compiler = output.parser_mut().solc_mut().compiler_mut();
707 compiler.enter_mut(|c| {
708 let _ = c.lower_asts();
709 let _ = c.analysis();
710 });
711
712 Ok(output)
713 }
714
715 fn get_sources(&self) -> Sources {
716 let mut sources = Sources::new();
717
718 let src = self.to_repl_source();
719 sources.insert(self.file_name.clone().into(), Source::new(src));
720
721 if !self.config.no_vm
723 && !self
724 .config
725 .foundry_config
726 .get_all_remappings()
727 .any(|r| r.name.starts_with("forge-std"))
728 {
729 sources.insert("forge-std/Vm.sol".into(), self.vm_source.clone());
730 }
731
732 sources
733 }
734
735 pub fn to_repl_source(&self) -> String {
737 let Self {
738 contract_name,
739 global_code,
740 contract_code: top_level_code,
741 run_code,
742 config,
743 ..
744 } = self;
745 let (mut vm_import, mut vm_constant) = (String::new(), String::new());
746 if !config.no_vm
749 && let Some(remapping) = config
750 .foundry_config
751 .remappings
752 .iter()
753 .find(|remapping| remapping.name == "forge-std/")
754 && let Some(vm_path) = WalkDir::new(&remapping.path.path)
755 .into_iter()
756 .filter_map(|e| e.ok())
757 .find(|e| e.file_name() == "Vm.sol")
758 {
759 vm_import = format!(
760 "import {{Vm}} from \"{}\";\n",
761 vm_path.path().to_string_lossy().replace('\\', "/")
762 );
763 vm_constant = "Vm internal constant vm = Vm(address(uint160(uint256(keccak256(\"hevm cheat code\")))));\n".to_string();
764 }
765
766 format!(
767 r#"
768// SPDX-License-Identifier: UNLICENSED
769pragma solidity 0;
770
771{vm_import}
772{global_code}
773
774contract {contract_name} {{
775 {vm_constant}
776 {top_level_code}
777
778 /// @notice REPL contract entry point
779 function run() public {{
780 {run_code}
781 }}
782}}"#,
783 )
784 }
785
786 pub(crate) fn parse(&self) -> Result<(), EmittedDiagnostics> {
788 let sess =
789 solar::interface::Session::builder().with_buffer_emitter(Default::default()).build();
790 let _ = sess.enter_sequential(|| -> solar::interface::Result<()> {
791 let arena = solar::ast::Arena::new();
792 let filename = self.file_name.clone().into();
793 let src = self.to_repl_source();
794 let mut parser = solar::parse::Parser::from_source_code(&sess, &arena, filename, src)?;
795 let _ast = parser.parse_file().map_err(|e| e.emit())?;
796 Ok(())
797 });
798 sess.dcx.emitted_errors().unwrap()
799 }
800}
801
802#[derive(Debug)]
807enum ParseTreeFragment {
808 Source,
810 Contract,
812 Function,
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819 use foundry_compilers::artifacts::remappings::{RelativeRemapping, RelativeRemappingPathBuf};
820 use foundry_evm::core::evm::EthEvmNetwork;
821 use std::fs;
822
823 #[test]
824 fn initialize_local_context_migrates_legacy_session() {
825 let mut config = SessionSourceConfig::<EthEvmNetwork>::default();
826 config.evm_opts.networks = NetworkConfigs::with_tempo();
827 config.evm_opts.env.chain_id = Some(4217);
828
829 config.initialize_local_context();
830
831 assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
832 assert_eq!(config.local_chain_id, Some(4217));
833
834 config.evm_opts.networks = NetworkConfigs::default();
835 config.evm_opts.env.chain_id = Some(1);
836 config.initialize_local_context();
837
838 assert_eq!(config.local_networks, Some(NetworkConfigs::with_tempo()));
839 assert_eq!(config.local_chain_id, Some(4217));
840 }
841
842 #[test]
843 fn serialized_session_restores_fork_inference_provenance() {
844 let config = SessionSourceConfig::<EthEvmNetwork> {
845 fork_network_is_inferred: true,
846 fork_chain_id_is_inferred: true,
847 ..Default::default()
848 };
849 let encoded = serde_json::to_string(&config).unwrap();
850 let mut decoded =
851 serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
852
853 assert!(!decoded.evm_opts.fork_network_is_inferred);
854 assert!(!decoded.evm_opts.fork_chain_id_is_inferred);
855 decoded.initialize_local_context();
856 assert!(decoded.evm_opts.fork_network_is_inferred);
857 assert!(decoded.evm_opts.fork_chain_id_is_inferred);
858 }
859
860 #[test]
861 fn legacy_session_without_rpc_transport_flags_deserializes() {
862 let config = SessionSourceConfig::<EthEvmNetwork>::default();
863 let mut legacy_session = serde_json::to_value(config).unwrap();
864 let evm_opts = legacy_session["evm_opts"].as_object_mut().expect("serialized EVM options");
865 assert!(evm_opts.remove("eth_rpc_accept_invalid_certs").is_some());
866 assert!(evm_opts.remove("eth_rpc_no_proxy").is_some());
867
868 let decoded =
869 serde_json::from_value::<SessionSourceConfig<EthEvmNetwork>>(legacy_session).unwrap();
870
871 assert!(!decoded.evm_opts.rpc_accept_invalid_certs);
872 assert!(!decoded.evm_opts.rpc_no_proxy);
873 }
874
875 #[test]
880 fn test_vm_import_path_uses_forward_slashes() {
881 let tmp = tempfile::tempdir().unwrap();
882 let vm_sol = tmp.path().join("Vm.sol");
883 fs::write(&vm_sol, "// dummy").unwrap();
884
885 let remapping = RelativeRemapping {
886 context: None,
887 name: "forge-std/".to_string(),
888 path: RelativeRemappingPathBuf { parent: None, path: tmp.path().to_path_buf() },
889 };
890
891 let mut config: SessionSourceConfig<EthEvmNetwork> = SessionSourceConfig {
892 foundry_config: Config {
893 solc: Some(SolcReq::Version(Version::new(0, 8, 29))),
894 remappings: vec![remapping],
895 ..Default::default()
896 },
897 ..Default::default()
898 };
899 config.detect_solc().unwrap();
901
902 let source = SessionSource {
903 file_name: "ReplContract.sol".to_string(),
904 contract_name: "REPL".to_string(),
905 config,
906 global_code: Default::default(),
907 contract_code: Default::default(),
908 run_code: Default::default(),
909 vm_source: vm_source(),
910 output: Default::default(),
911 };
912
913 let repl = source.to_repl_source();
914 let import_line = repl.lines().find(|l| l.contains("import {Vm}")).unwrap();
915 assert!(
916 !import_line.contains('\\'),
917 "Vm import path must not contain backslashes, got: {import_line}"
918 );
919 assert!(import_line.contains('/'), "Vm import path must use forward slashes");
920 }
921
922 #[test]
923 fn session_serialization_omits_credentials() {
924 let mut config = SessionSourceConfig::<EthEvmNetwork>::default();
925 config.foundry_config.eth_rpc_url =
926 Some("https://user:synthetic-password@rpc.invalid/key".into());
927 config.foundry_config.eth_rpc_jwt = Some("synthetic-jwt".into());
928 config.foundry_config.eth_rpc_headers =
929 Some(vec!["Authorization: synthetic-header".into()]);
930 config.foundry_config.etherscan_api_key = Some("synthetic-api-key".into());
931 config.foundry_config.etherscan = serde_json::from_value(serde_json::json!({
932 "mainnet": { "key": "synthetic-explorer-key", "chain": 1 }
933 }))
934 .unwrap();
935 config.foundry_config.rpc_endpoints = serde_json::from_value(serde_json::json!({
936 "mainnet": "https://rpc.invalid/synthetic-endpoint-key"
937 }))
938 .unwrap();
939 config.foundry_config.rpc_storage_caching.endpoints =
940 "synthetic-cache-endpoint-key".parse().unwrap();
941 config.evm_opts.fork_url = config.foundry_config.eth_rpc_url.clone();
942 config.evm_opts.rpc_jwt = config.foundry_config.eth_rpc_jwt.clone();
943 config.evm_opts.rpc_headers = config.foundry_config.eth_rpc_headers.clone();
944 config.evm_opts.fork_headers = Some(vec!["Authorization: synthetic-fork-header".into()]);
945 config.evm_opts.fork_block_number = Some(42);
946 config.calldata = Some(vec![0xde, 0xad, 0xbe, 0xef]);
947 config.traces = true;
948
949 let encoded = serde_json::to_string(&config).unwrap();
950 let decoded: SessionSourceConfig<EthEvmNetwork> = serde_json::from_str(&encoded).unwrap();
951
952 assert_eq!(decoded.foundry_config.eth_rpc_url, None);
953 assert_eq!(decoded.foundry_config.eth_rpc_jwt, None);
954 assert_eq!(decoded.foundry_config.eth_rpc_headers, None);
955 assert_eq!(decoded.foundry_config.etherscan_api_key, None);
956 assert!(decoded.foundry_config.etherscan.is_empty());
957 assert!(decoded.foundry_config.rpc_endpoints.is_empty());
958 assert_eq!(decoded.foundry_config.rpc_storage_caching.endpoints.to_string(), "all");
959 assert_eq!(decoded.evm_opts.fork_url, None);
960 assert_eq!(decoded.evm_opts.rpc_jwt, None);
961 assert_eq!(decoded.evm_opts.rpc_headers, None);
962 assert_eq!(decoded.evm_opts.fork_headers, None);
963 assert_eq!(decoded.evm_opts.fork_block_number, Some(42));
964 assert_eq!(decoded.calldata, Some(vec![0xde, 0xad, 0xbe, 0xef]));
965 assert!(decoded.traces);
966 assert!(decoded.fork_url_required);
967 assert_eq!(config.foundry_config.eth_rpc_jwt.as_deref(), Some("synthetic-jwt"));
968 assert_eq!(config.evm_opts.fork_url, config.foundry_config.eth_rpc_url);
969 }
970
971 #[test]
973 fn session_serialization_round_trips_persisted_fields() {
974 let config = SessionSourceConfig::<EthEvmNetwork> {
975 foundry_config: Config { optimizer_runs: Some(500), ..Default::default() },
976 evm_opts: EvmOpts { fork_block_number: Some(42), ..Default::default() },
977 local_networks: Some(NetworkConfigs::with_tempo()),
978 local_chain_id: Some(4217),
979 fork_network_is_inferred: true,
980 fork_chain_id_is_inferred: true,
981 no_vm: true,
982 traces: true,
983 calldata: Some(vec![1, 2, 3]),
984 ir_minimum: true,
985 fork_url_required: true,
986 source_chain_id: Some(1),
987 ..Default::default()
988 };
989
990 let encoded = serde_json::to_value(&config).unwrap();
991 let mut keys = encoded.as_object().unwrap().keys().map(String::as_str).collect::<Vec<_>>();
992 keys.sort_unstable();
993 assert_eq!(
994 keys,
995 [
996 "calldata",
997 "evm_opts",
998 "fork_chain_id_is_inferred",
999 "fork_network_is_inferred",
1000 "fork_url_required",
1001 "foundry_config",
1002 "ir_minimum",
1003 "local_chain_id",
1004 "local_networks",
1005 "no_vm",
1006 "traces",
1007 ]
1008 );
1009
1010 let decoded =
1011 serde_json::from_value::<SessionSourceConfig<EthEvmNetwork>>(encoded).unwrap();
1012 assert_eq!(decoded.foundry_config.optimizer_runs, Some(500));
1013 assert_eq!(decoded.evm_opts.fork_block_number, Some(42));
1014 assert!(decoded.local_networks.is_some_and(|networks| networks.is_tempo()));
1015 assert_eq!(decoded.local_chain_id, Some(4217));
1016 assert!(decoded.fork_network_is_inferred);
1017 assert!(decoded.fork_chain_id_is_inferred);
1018 assert!(decoded.no_vm);
1019 assert!(decoded.traces);
1020 assert_eq!(decoded.calldata, Some(vec![1, 2, 3]));
1021 assert!(decoded.ir_minimum);
1022 assert!(decoded.fork_url_required);
1023 assert_eq!(decoded.source_chain_id, None);
1024 }
1025
1026 #[test]
1027 fn saved_fork_restores_current_credentials_and_execution_settings() {
1028 let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1029 saved.evm_opts.fork_url = Some("https://rpc.invalid/old-token".into());
1030 saved.evm_opts.fork_block_number = Some(42);
1031 saved.evm_opts.env.chain_id = Some(1);
1032 saved.foundry_config.optimizer_runs = Some(500);
1033 saved.foundry_config.eth_rpc_accept_invalid_certs = true;
1034 saved.foundry_config.eth_rpc_no_proxy = true;
1035 saved.evm_opts.rpc_accept_invalid_certs = true;
1036 saved.evm_opts.rpc_no_proxy = true;
1037 saved.calldata = Some(vec![1, 2, 3]);
1038 saved.ir_minimum = true;
1039 let encoded = serde_json::to_string(&saved).unwrap();
1040 let mut saved =
1041 serde_json::from_str::<SessionSourceConfig<EthEvmNetwork>>(&encoded).unwrap();
1042 let mut current = SessionSourceConfig::<EthEvmNetwork>::default();
1043 current.foundry_config.eth_rpc_url = Some("https://rpc.invalid/new-token".into());
1044 current.foundry_config.eth_rpc_jwt = Some("current-jwt".into());
1045 current.foundry_config.eth_rpc_headers = Some(vec!["Authorization: current-header".into()]);
1046 current.foundry_config.etherscan_api_key = Some("current-api-key".into());
1047 current.foundry_config.rpc_endpoints = serde_json::from_value(serde_json::json!({
1048 "mainnet": "https://rpc.invalid/current-endpoint-token"
1049 }))
1050 .unwrap();
1051 current.foundry_config.etherscan = serde_json::from_value(serde_json::json!({
1052 "mainnet": { "key": "current-explorer-key", "chain": 1 }
1053 }))
1054 .unwrap();
1055 current.foundry_config.rpc_storage_caching.endpoints =
1056 "current-cache-endpoint-token".parse().unwrap();
1057 current.evm_opts.fork_url = current.foundry_config.eth_rpc_url.clone();
1058 current.evm_opts.rpc_jwt = current.foundry_config.eth_rpc_jwt.clone();
1059 current.evm_opts.rpc_headers = current.foundry_config.eth_rpc_headers.clone();
1060 current.evm_opts.fork_headers = Some(vec!["Authorization: current-fork-header".into()]);
1061 current.evm_opts.fork_block_number = Some(100);
1062 current.evm_opts.env.chain_id = Some(10);
1063 current.foundry_config.eth_rpc_timeout = Some(30);
1064 current.evm_opts.rpc_timeout = Some(30);
1065
1066 saved
1067 .restore_credentials(&InvocationRpc::capture(
1068 ¤t.foundry_config,
1069 ¤t.evm_opts,
1070 ))
1071 .unwrap();
1072
1073 assert_eq!(saved.foundry_config.eth_rpc_url, current.foundry_config.eth_rpc_url);
1074 assert_eq!(saved.foundry_config.eth_rpc_jwt, current.foundry_config.eth_rpc_jwt);
1075 assert_eq!(saved.foundry_config.eth_rpc_headers, current.foundry_config.eth_rpc_headers);
1076 assert_eq!(
1077 saved.foundry_config.etherscan_api_key,
1078 current.foundry_config.etherscan_api_key
1079 );
1080 assert_eq!(saved.foundry_config.etherscan, current.foundry_config.etherscan);
1081 assert_eq!(saved.foundry_config.rpc_endpoints, current.foundry_config.rpc_endpoints);
1082 assert_eq!(
1083 saved.foundry_config.rpc_storage_caching.endpoints,
1084 current.foundry_config.rpc_storage_caching.endpoints
1085 );
1086 assert_eq!(saved.evm_opts.fork_url, current.evm_opts.fork_url);
1087 assert_eq!(saved.evm_opts.rpc_jwt, current.evm_opts.rpc_jwt);
1088 assert_eq!(saved.evm_opts.rpc_headers, current.evm_opts.rpc_headers);
1089 assert_eq!(saved.evm_opts.fork_headers, current.evm_opts.fork_headers);
1090 assert!(!saved.foundry_config.eth_rpc_accept_invalid_certs);
1091 assert!(!saved.foundry_config.eth_rpc_no_proxy);
1092 assert!(!saved.evm_opts.rpc_accept_invalid_certs);
1093 assert!(!saved.evm_opts.rpc_no_proxy);
1094 assert_eq!(saved.foundry_config.eth_rpc_timeout, Some(30));
1095 assert_eq!(saved.evm_opts.rpc_timeout, Some(30));
1096 assert_eq!(saved.evm_opts.fork_block_number, Some(42));
1097 assert_eq!(saved.evm_opts.env.chain_id, Some(1));
1098 assert_eq!(saved.foundry_config.optimizer_runs, Some(500));
1099 assert_eq!(saved.calldata, Some(vec![1, 2, 3]));
1100 assert!(saved.ir_minimum);
1101 }
1102
1103 #[test]
1104 fn legacy_session_credentials_are_not_reused() {
1105 let mut legacy = SessionSourceConfig::<EthEvmNetwork>::default();
1106 legacy.foundry_config.eth_rpc_jwt = Some("legacy-jwt".into());
1107 legacy.foundry_config.etherscan_api_key = Some("legacy-key".into());
1108 legacy.evm_opts.rpc_headers = Some(vec!["Authorization: legacy-header".into()]);
1109 legacy.evm_opts.fork_headers = Some(vec!["Authorization: legacy-fork-header".into()]);
1110 legacy.restore_credentials(&InvocationRpc::default()).unwrap();
1111
1112 assert_eq!(legacy.foundry_config.eth_rpc_jwt, None);
1113 assert_eq!(legacy.foundry_config.etherscan_api_key, None);
1114 assert_eq!(legacy.evm_opts.rpc_headers, None);
1115 assert_eq!(legacy.evm_opts.fork_headers, None);
1116 }
1117
1118 #[test]
1119 fn saved_fork_requires_current_endpoint() {
1120 let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1121 saved.evm_opts.fork_url = Some("https://rpc.invalid/legacy-token".into());
1122 saved.clear_credentials();
1123 assert_eq!(saved.evm_opts.fork_url, None);
1124 assert!(saved.fork_url_required);
1125
1126 let error = saved.restore_credentials(&InvocationRpc::default()).unwrap_err();
1127
1128 assert_eq!(
1129 error.to_string(),
1130 "this saved Chisel session requires a fork endpoint; use !fork <url> or restart Chisel with --fork-url to load it"
1131 );
1132 }
1133
1134 #[test]
1135 fn saved_local_session_stays_local_with_current_fork_endpoint() {
1136 let mut saved = SessionSourceConfig::<EthEvmNetwork>::default();
1137 let mut current = SessionSourceConfig::<EthEvmNetwork>::default();
1138 current.evm_opts.fork_url = Some("https://rpc.invalid/current-token".into());
1139
1140 saved
1141 .restore_credentials(&InvocationRpc::capture(
1142 ¤t.foundry_config,
1143 ¤t.evm_opts,
1144 ))
1145 .unwrap();
1146
1147 assert_eq!(saved.evm_opts.fork_url, None);
1148 assert!(!saved.fork_url_required);
1149 }
1150}