1use crate::{
2 ScriptArgs, ScriptConfig,
3 broadcast::{BundledState, remaining_unsigned_transactions_for_recovery},
4 execute::LinkedState,
5 multi_sequence::MultiChainSequence,
6 progress::ScriptProgress,
7 receipts::is_mined_receipt_for,
8 recovery::recovery_exists,
9 sequence::{ScriptSequenceKind, blocked_pending},
10 session::{SignerScope, script_session_expected_sender_if_configured},
11};
12use alloy_network::{AnyNetwork, ReceiptResponse};
13use alloy_primitives::{Address, B256, map::AddressHashSet};
14use alloy_provider::Provider;
15use eyre::{ContextCompat, OptionExt, Result};
16use forge_script_sequence::ScriptSequence;
17use foundry_cheatcodes::Wallets;
18use foundry_cli::opts::TempoOpts;
19use foundry_common::{
20 ContractData, ContractsByArtifact, ContractsByArtifactBuilder, compile::ProjectCompiler,
21 external_compiler::is_builtin_compiler_source, fs::canonicalize_path, namespaced_storage,
22 provider::ProviderBuilder,
23};
24use foundry_compilers::{
25 ArtifactId, ProjectCompileOutput,
26 artifacts::{BytecodeObject, Libraries},
27 compilers::{Language, multi::MultiCompilerLanguage},
28 info::ContractInfo,
29 utils::source_files_iter,
30};
31use foundry_evm::{
32 core::evm::FoundryEvmNetwork,
33 traces::{CallTraceDecoder, debug::ContractSources},
34};
35use foundry_linking::Linker;
36use foundry_wallets::{MultiWalletOpts, wallet_browser::signer::BrowserSigner};
37use itertools::Itertools;
38use std::{
39 path::{Path, PathBuf},
40 str::FromStr,
41 sync::Arc,
42};
43
44#[derive(Clone, Debug)]
46pub struct BuildData {
47 pub project_root: PathBuf,
49 pub output: ProjectCompileOutput,
51 pub target: ArtifactId,
53}
54
55impl BuildData {
56 pub fn get_linker(&self) -> Linker<'_> {
57 Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
58 }
59
60 pub async fn link<FEN: FoundryEvmNetwork>(
63 self,
64 script_config: &ScriptConfig<FEN>,
65 ) -> Result<LinkedBuildData> {
66 let create2_deployer = script_config.evm_opts.create2_deployer;
67 let can_use_create2 = script_config
68 .backend
69 .can_use_create2_deployer(script_config.evm_opts.create2_deployer)
70 .await?;
71
72 let known_libraries = script_config.config.libraries_with_remappings()?;
73
74 let maybe_create2_link_output = can_use_create2
75 .then(|| {
76 self.get_linker()
77 .link_with_create2_detailed(
78 known_libraries.clone(),
79 create2_deployer,
80 script_config.config.create2_library_salt,
81 [&self.target],
82 )
83 .ok()
84 })
85 .flatten();
86
87 let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
88 (
89 output.output.libraries,
90 ScriptPredeployLibraries::Create2 {
91 onchain: output.linked_libraries,
92 salt: script_config.config.create2_library_salt,
93 local: Vec::new(),
94 },
95 )
96 } else {
97 let output = self.get_linker().link_with_nonce_or_address_detailed(
98 known_libraries,
99 script_config.evm_opts.sender,
100 script_config.sender_nonce,
101 [&self.target],
102 )?;
103
104 (
105 output.output.libraries,
106 ScriptPredeployLibraries::Default {
107 onchain: output.linked_libraries,
108 local: Vec::new(),
109 },
110 )
111 };
112
113 LinkedBuildData::new(libraries, predeploy_libs, self)
114 }
115
116 pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
119 LinkedBuildData::new(
120 libraries,
121 ScriptPredeployLibraries::Default { onchain: Vec::new(), local: Vec::new() },
122 self,
123 )
124 }
125}
126
127#[derive(Clone, Debug)]
128pub enum ScriptPredeployLibraries {
129 Default {
130 onchain: Vec<foundry_linking::LinkedLibrary>,
131 local: Vec<foundry_linking::LinkedLibrary>,
132 },
133 Create2 {
134 onchain: Vec<foundry_linking::LinkedLibrary>,
135 salt: B256,
136 local: Vec<foundry_linking::LinkedLibrary>,
137 },
138}
139
140impl ScriptPredeployLibraries {
141 pub const fn libraries_count(&self) -> usize {
142 match self {
143 Self::Default { onchain, .. } => onchain.len(),
144 Self::Create2 { onchain, .. } => onchain.len(),
145 }
146 }
147}
148
149#[derive(Clone, Debug)]
151pub struct LinkedBuildData {
152 pub build_data: BuildData,
154 pub known_contracts: ContractsByArtifact,
156 pub libraries: Libraries,
158 pub predeploy_libraries: ScriptPredeployLibraries,
160 pub sources: ContractSources,
162}
163
164impl LinkedBuildData {
165 pub fn new(
166 libraries: Libraries,
167 predeploy_libraries: ScriptPredeployLibraries,
168 mut build_data: BuildData,
169 ) -> Result<Self> {
170 let sources = ContractSources::from_project_output(
171 &build_data.output,
172 &build_data.project_root,
173 Some(&libraries),
174 )?;
175
176 let storage_layouts = namespaced_storage::project_storage_layouts(
177 &mut build_data.output,
178 &build_data.project_root,
179 );
180 let linked_contracts = build_data.get_linker().get_linked_artifacts(&libraries)?;
181 let known_contracts = ContractsByArtifactBuilder::new(
182 linked_contracts.iter().map(|(id, artifact)| (id.clone(), artifact.into())),
183 )
184 .with_storage_layouts(storage_layouts)
185 .build();
186
187 Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources })
188 }
189
190 pub fn get_target_contract(&self) -> Result<&ContractData> {
192 self.known_contracts
193 .get(&self.build_data.target)
194 .ok_or_eyre("target not found in linked artifacts")
195 }
196}
197
198pub struct PreprocessedState<FEN: FoundryEvmNetwork> {
200 pub args: ScriptArgs,
201 pub script_config: ScriptConfig<FEN>,
202 pub script_wallets: Wallets,
203 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
204}
205
206impl<FEN: FoundryEvmNetwork> PreprocessedState<FEN> {
207 pub fn compile(self) -> Result<CompiledState<FEN>> {
210 let Self { args, script_config, script_wallets, browser_wallet } = self;
211 let project = script_config.config.project()?;
212
213 let mut target_name = args.target_contract.clone();
214
215 let target_path = if let Ok(path) = canonicalize_path(&args.path) {
219 path
220 } else {
221 let contract = ContractInfo::from_str(&args.path)?;
222 target_name = Some(contract.name.clone());
223 if let Some(path) = contract.path {
224 canonicalize_path(path)?
225 } else {
226 project.find_contract_path(contract.name.as_str())?
227 }
228 };
229
230 let sources_to_compile = source_files_iter(
231 project.paths.sources.as_path(),
232 MultiCompilerLanguage::FILE_EXTENSIONS,
233 );
234
235 let compiler = ProjectCompiler::new()
236 .allow_linked_libraries(script_config.config.allow_linked_libraries)
237 .external_compilers(&script_config.config)
238 .files(sources_to_compile)
239 .dynamic_test_linking(script_config.config.dynamic_test_linking);
240 let compiler = if is_builtin_compiler_source(&target_path) {
241 compiler.files([target_path.clone()])
242 } else {
243 compiler.target_files([target_path.clone()])
244 };
245 let output = compiler.compile(&project)?;
246
247 let mut target_id: Option<ArtifactId> = None;
248
249 for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
251 if let Some(name) = &target_name {
252 if id.name != *name {
253 continue;
254 }
255 } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
256 || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
257 BytecodeObject::Bytecode(b) => b.is_empty(),
258 BytecodeObject::Unlinked(_) => false,
259 })
260 {
261 continue;
264 }
265
266 if let Some(target) = target_id {
267 let target_name = target.name.split('.').next().unwrap();
271 let id_name = id.name.split('.').next().unwrap();
272 if target_name != id_name {
273 eyre::bail!(
274 "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
275 );
276 }
277 }
278 target_id = Some(id);
279 }
280
281 let target = target_id.ok_or_eyre("Could not find target contract")?;
282
283 Ok(CompiledState {
284 args,
285 script_config,
286 script_wallets,
287 browser_wallet,
288 build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
289 })
290 }
291}
292
293pub struct CompiledState<FEN: FoundryEvmNetwork> {
295 pub args: ScriptArgs,
296 pub script_config: ScriptConfig<FEN>,
297 pub script_wallets: Wallets,
298 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
299 pub build_data: BuildData,
300}
301
302impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
303 pub async fn link(self) -> Result<LinkedState<FEN>> {
305 let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
306
307 let build_data = build_data.link(&script_config).await?;
308
309 Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
310 }
311
312 pub async fn resume(mut self) -> Result<BundledState<FEN>> {
314 let endpoint = if self.args.multi {
315 if self.args.evm.rpc.rpc_url.is_some() {
316 sh_warn!(
317 "Multichain resume submits and checks transactions through the RPC endpoints saved by the original run; `--rpc-url` is not used for that."
318 )?;
319 }
320 None
321 } else {
322 let fork_url = self.script_config.evm_opts.fork_url.clone().ok_or_eyre("Missing --fork-url field, if you were trying to broadcast a multi-chain sequence, please use --multi flag")?;
323 let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
324 let chain = provider.get_chain_id().await?;
325 Some((fork_url, chain))
326 };
327 let chain = endpoint.as_ref().map(|(_, chain)| *chain);
328
329 let mut sequence = if self.sequence_exists(chain, false)? {
330 self.try_load_sequence(chain, false)?
331 } else {
332 let mut sequence = self.try_load_sequence(chain, true)?;
335
336 sequence.promote_to_broadcasted(
338 &self.script_config.config,
339 &self.args.sig,
340 &self.build_data.target,
341 )?;
342 sequence
343 };
344
345 if let Some((fork_url, chain)) = endpoint
348 && self.args.evm.rpc.rpc_url.is_some()
349 {
350 let saved = sequence.sequences()[0].chain;
351 if saved != chain {
352 eyre::bail!(
353 "saved sequence is for chain {saved}, but `--rpc-url` is on chain {chain}"
354 );
355 }
356 sequence.set_rpc_override(fork_url);
357 }
358
359 let resolution = sequence.restore_delegated_pending(
360 self.args.resume_attempt,
361 self.args.resume_tx_hash,
362 self.args.resume_retry,
363 )?;
364 if !self.args.batch {
365 if let Some((sequence_index, _, attempt_id, hash)) = resolution {
366 let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
367 &self.script_config.config,
368 sequence.rpc_url(sequence_index),
369 )?
370 .build()?;
371 let transaction = provider
372 .get_transaction_by_hash(hash)
373 .await?
374 .context("resolved transaction is not available from the recovery endpoint")?;
375 sequence.resolve_delegated_hash(attempt_id, hash, &transaction)?;
376 }
377 let progress = ScriptProgress::default();
378 let mut errors = Vec::new();
381 for index in 0..sequence.sequences().len() {
382 let waited = async {
384 let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
385 &self.script_config.config,
386 sequence.rpc_url(index),
387 )?
388 .build()?;
389 for operation in 0..sequence.sequences()[index].transactions.len() {
392 let deployment = &sequence.sequences()[index];
393 if let Some(hash) =
394 sequence.signed_payload(index, operation).map(|s| s.hash)
395 && !deployment.pending.contains(&hash)
396 && !deployment.reverted.contains(&hash)
397 && !deployment.receipts.iter().any(|r| r.transaction_hash() == hash)
398 && let Some(receipt) = provider.get_transaction_receipt(hash).await?
399 && is_mined_receipt_for(&receipt, hash)
400 {
401 sequence.sequences_mut()[index].add_pending(operation, hash);
402 }
403 }
404 if sequence.sequences()[index].pending.is_empty() {
405 return Ok(None);
406 }
407 let (durable_hashes, replayable_hashes) = sequence.submission_hashes(index);
408 let blocked = blocked_pending(sequence.sequences(), index);
409 Ok::<_, eyre::Report>(Some(
410 progress
411 .wait_for_pending(
412 index,
413 &mut sequence.sequences_mut()[index],
414 &provider,
415 self.script_config.config.transaction_timeout,
416 self.args.confirmations,
417 (&durable_hashes, &replayable_hashes),
418 &blocked,
419 )
420 .await,
421 ))
422 }
423 .await;
424 let result = match waited {
425 Ok(Some(result)) => {
426 sequence.save(true, false)?;
427 result.and_then(|()| sequence.ensure_delegated_outcomes_known(index))
428 }
429 Ok(None) => Ok(()),
430 Err(err) => Err(err),
431 };
432 if let Err(err) = result {
433 errors.push(sequence.label_error(index, err));
434 }
435 }
436 match errors.len() {
437 0 => {}
438 1 => return Err(errors.remove(0)),
439 _ => eyre::bail!("{}", errors.iter().format("\n")),
440 }
441 }
442 for deployment in sequence.sequences() {
443 for hash in &deployment.reverted {
444 sh_warn!(
445 "Transaction {hash} on chain {} reverted; resume will not resubmit it.",
446 deployment.chain
447 )?;
448 }
449 }
450
451 if !self.args.unlocked
452 && !remaining_unsigned_transactions_for_recovery(&sequence).is_empty()
453 {
454 self.script_wallets =
455 Wallets::new(self.args.wallets.get_multi_wallet().await?, self.args.evm.sender);
456 self.browser_wallet = self.args.wallets.browser_signer::<FEN::Network>().await?;
457
458 if self.args.evm.sender.is_none() {
459 let addresses = self.script_wallets.addresses();
460 let sender = self
461 .args
462 .maybe_load_private_key()?
463 .or_else(|| (addresses.len() == 1).then(|| addresses[0]))
464 .or_else(|| self.browser_wallet.as_ref().map(|wallet| wallet.address()));
465 if let Some(sender) = sender {
466 self.script_config.update_sender(sender).await?;
467 }
468 }
469 }
470
471 let (args, build_data, script_wallets, browser_wallet, script_config, resume_script_error) =
472 if self.args.unlocked {
473 (
474 self.args,
475 self.build_data,
476 self.script_wallets,
477 self.browser_wallet,
478 self.script_config,
479 None,
480 )
481 } else {
482 let remaining_transactions =
483 remaining_unsigned_transactions_for_recovery(&sequence);
484 let remaining_froms =
485 remaining_transactions.iter().map(|tx| tx.sender).collect::<AddressHashSet>();
486 let expected_session_sender = script_session_expected_sender_if_configured(
487 &self.script_config.tempo,
488 &remaining_froms,
489 )?;
490 let has_available_signers = has_available_script_signers(
491 &self.script_config.tempo,
492 &self.args.wallets,
493 &self.script_wallets,
494 expected_session_sender,
495 &remaining_transactions,
496 )?;
497
498 if has_available_signers {
499 (
500 self.args,
501 self.build_data,
502 self.script_wallets,
503 self.browser_wallet,
504 self.script_config,
505 None,
506 )
507 } else {
508 let mut state = self;
511 state
512 .script_config
513 .update_tempo_session_sender(&state.args.wallets, state.args.evm.sender)
514 .await?;
515 let executed = state.link().await?.prepare_execution().await?.execute().await?;
516 let resume_script_error = (!executed.execution_result.success).then(|| {
519 CallTraceDecoder::new()
520 .revert_decoder
521 .clone()
522 .with_abis(executed.build_data.known_contracts.values().map(|c| &c.abi))
523 .decode(
524 &executed.execution_result.returned,
525 executed.execution_result.exit_reason,
526 )
527 });
528 (
529 executed.args,
530 executed.build_data.build_data,
531 executed.script_wallets,
532 executed.browser_wallet,
533 executed.script_config,
534 resume_script_error,
535 )
536 }
537 };
538
539 let libraries = if sequence.is_multi() {
541 Libraries::default()
543 } else {
544 Libraries::parse(&sequence.sequences()[0].libraries)?
545 };
546
547 let linked_build_data = build_data.link_with_libraries(libraries)?;
548
549 Ok(BundledState {
550 args,
551 script_config,
552 script_wallets,
553 browser_wallet,
554 build_data: linked_build_data,
555 sequence,
556 resume_script_error,
557 })
558 }
559
560 fn try_load_sequence(
561 &self,
562 chain: Option<u64>,
563 dry_run: bool,
564 ) -> Result<ScriptSequenceKind<FEN::Network>> {
565 if let Some(chain) = chain {
566 ScriptSequenceKind::load_single(
567 &self.script_config.config,
568 &self.args.sig,
569 &self.build_data.target,
570 chain,
571 dry_run,
572 self.args.batch,
573 )
574 } else {
575 ScriptSequenceKind::load_multi(
576 &self.script_config.config,
577 &self.args.sig,
578 &self.build_data.target,
579 dry_run,
580 self.args.batch,
581 )
582 }
583 }
584
585 fn sequence_exists(&self, chain: Option<u64>, dry_run: bool) -> Result<bool> {
586 let paths = if let Some(chain) = chain {
587 ScriptSequence::<FEN::Network>::get_paths(
588 &self.script_config.config,
589 &self.args.sig,
590 &self.build_data.target,
591 chain,
592 dry_run,
593 )?
594 } else {
595 MultiChainSequence::<FEN::Network>::get_paths(
596 &self.script_config.config,
597 &self.args.sig,
598 &self.build_data.target,
599 dry_run,
600 )?
601 };
602 Ok(compatibility_progress_exists(&paths) || recovery_exists(&paths)?)
603 }
604}
605
606fn compatibility_progress_exists(paths: &(PathBuf, PathBuf)) -> bool {
607 [&paths.0, &paths.1].into_iter().any(|path| progress_exists(path))
608}
609
610fn progress_exists(path: &Path) -> bool {
611 path.exists() || path.with_extension("previous").exists()
612}
613
614fn has_available_script_signers(
620 tempo: &TempoOpts,
621 wallets: &MultiWalletOpts,
622 script_wallets: &Wallets,
623 expected_sender: Option<Address>,
624 remaining: &[SignerScope],
625) -> Result<bool> {
626 let signers = script_wallets
627 .signers()
628 .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
629 if remaining.is_empty() {
630 return Ok(true);
631 }
632
633 let session_scope = tempo
634 .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
635 .map(|s| SignerScope::new(s.session.chain_id, s.access_key.account()));
636
637 Ok(remaining.iter().all(|tx| signers.contains(&tx.sender) || session_scope == Some(*tx)))
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn backup_counts_as_recoverable_progress() {
646 let dir = tempfile::tempdir().unwrap();
647 let path = dir.path().join("run-latest.json");
648 std::fs::write(path.with_extension("previous"), b"{}").unwrap();
649
650 assert!(progress_exists(&path));
651 }
652
653 #[test]
654 fn sensitive_export_counts_as_recoverable_progress() {
655 let dir = tempfile::tempdir().unwrap();
656 let paths = (dir.path().join("run-latest.json"), dir.path().join("run-latest-cache.json"));
657 std::fs::write(&paths.1, b"{}").unwrap();
658
659 assert!(compatibility_progress_exists(&paths));
660 }
661
662 #[test]
663 fn has_available_script_signers_skips_session_resolution_when_remaining_empty() {
664 let has_available = has_available_script_signers(
665 &TempoOpts { session: Some(B256::repeat_byte(0x99)), ..Default::default() },
666 &MultiWalletOpts::default(),
667 &Wallets::new(Default::default(), None),
668 None,
669 &[],
670 )
671 .unwrap();
672
673 assert!(has_available);
674 }
675}