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,
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,
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::{core::evm::FoundryEvmNetwork, traces::debug::ContractSources};
32use foundry_linking::Linker;
33use foundry_wallets::{MultiWalletOpts, wallet_browser::signer::BrowserSigner};
34use std::{
35 path::{Path, PathBuf},
36 str::FromStr,
37 sync::Arc,
38};
39
40#[derive(Clone, Debug)]
42pub struct BuildData {
43 pub project_root: PathBuf,
45 pub output: ProjectCompileOutput,
47 pub target: ArtifactId,
49}
50
51impl BuildData {
52 pub fn get_linker(&self) -> Linker<'_> {
53 Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
54 }
55
56 pub async fn link<FEN: FoundryEvmNetwork>(
59 self,
60 script_config: &ScriptConfig<FEN>,
61 ) -> Result<LinkedBuildData> {
62 let create2_deployer = script_config.evm_opts.create2_deployer;
63 let can_use_create2 = script_config
64 .backend
65 .can_use_create2_deployer(script_config.evm_opts.create2_deployer)
66 .await?;
67
68 let known_libraries = script_config.config.libraries_with_remappings()?;
69
70 let maybe_create2_link_output = can_use_create2
71 .then(|| {
72 self.get_linker()
73 .link_with_create2_detailed(
74 known_libraries.clone(),
75 create2_deployer,
76 script_config.config.create2_library_salt,
77 [&self.target],
78 )
79 .ok()
80 })
81 .flatten();
82
83 let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
84 (
85 output.output.libraries,
86 ScriptPredeployLibraries::Create2 {
87 onchain: output.linked_libraries,
88 salt: script_config.config.create2_library_salt,
89 local: Vec::new(),
90 },
91 )
92 } else {
93 let output = self.get_linker().link_with_nonce_or_address_detailed(
94 known_libraries,
95 script_config.evm_opts.sender,
96 script_config.sender_nonce,
97 [&self.target],
98 )?;
99
100 (
101 output.output.libraries,
102 ScriptPredeployLibraries::Default {
103 onchain: output.linked_libraries,
104 local: Vec::new(),
105 },
106 )
107 };
108
109 LinkedBuildData::new(libraries, predeploy_libs, self)
110 }
111
112 pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
115 LinkedBuildData::new(
116 libraries,
117 ScriptPredeployLibraries::Default { onchain: Vec::new(), local: Vec::new() },
118 self,
119 )
120 }
121}
122
123#[derive(Clone, Debug)]
124pub enum ScriptPredeployLibraries {
125 Default {
126 onchain: Vec<foundry_linking::LinkedLibrary>,
127 local: Vec<foundry_linking::LinkedLibrary>,
128 },
129 Create2 {
130 onchain: Vec<foundry_linking::LinkedLibrary>,
131 salt: B256,
132 local: Vec<foundry_linking::LinkedLibrary>,
133 },
134}
135
136impl ScriptPredeployLibraries {
137 pub const fn libraries_count(&self) -> usize {
138 match self {
139 Self::Default { onchain, .. } => onchain.len(),
140 Self::Create2 { onchain, .. } => onchain.len(),
141 }
142 }
143}
144
145#[derive(Clone, Debug)]
147pub struct LinkedBuildData {
148 pub build_data: BuildData,
150 pub known_contracts: ContractsByArtifact,
152 pub libraries: Libraries,
154 pub predeploy_libraries: ScriptPredeployLibraries,
156 pub sources: ContractSources,
158}
159
160impl LinkedBuildData {
161 pub fn new(
162 libraries: Libraries,
163 predeploy_libraries: ScriptPredeployLibraries,
164 build_data: BuildData,
165 ) -> Result<Self> {
166 let sources = ContractSources::from_project_output(
167 &build_data.output,
168 &build_data.project_root,
169 Some(&libraries),
170 )?;
171
172 let linked_contracts = build_data.get_linker().get_linked_artifacts(&libraries)?;
173 let known_contracts = ContractsByArtifactBuilder::new(
174 linked_contracts.iter().map(|(id, artifact)| (id.clone(), artifact.into())),
175 )
176 .with_storage_layouts(build_data.output.artifact_ids().filter_map(|(id, artifact)| {
177 artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
178 }))
179 .build();
180
181 Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources })
182 }
183
184 pub fn get_target_contract(&self) -> Result<&ContractData> {
186 self.known_contracts
187 .get(&self.build_data.target)
188 .ok_or_eyre("target not found in linked artifacts")
189 }
190}
191
192pub struct PreprocessedState<FEN: FoundryEvmNetwork> {
194 pub args: ScriptArgs,
195 pub script_config: ScriptConfig<FEN>,
196 pub script_wallets: Wallets,
197 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
198}
199
200impl<FEN: FoundryEvmNetwork> PreprocessedState<FEN> {
201 pub fn compile(self) -> Result<CompiledState<FEN>> {
204 let Self { args, script_config, script_wallets, browser_wallet } = self;
205 let project = script_config.config.project()?;
206
207 let mut target_name = args.target_contract.clone();
208
209 let target_path = if let Ok(path) = canonicalize_path(&args.path) {
213 path
214 } else {
215 let contract = ContractInfo::from_str(&args.path)?;
216 target_name = Some(contract.name.clone());
217 if let Some(path) = contract.path {
218 canonicalize_path(path)?
219 } else {
220 project.find_contract_path(contract.name.as_str())?
221 }
222 };
223
224 let sources_to_compile = source_files_iter(
225 project.paths.sources.as_path(),
226 MultiCompilerLanguage::FILE_EXTENSIONS,
227 );
228
229 let compiler = ProjectCompiler::new()
230 .external_compilers(&script_config.config)
231 .files(sources_to_compile)
232 .dynamic_test_linking(script_config.config.dynamic_test_linking);
233 let compiler = if is_builtin_compiler_source(&target_path) {
234 compiler.files([target_path.clone()])
235 } else {
236 compiler.target_files([target_path.clone()])
237 };
238 let output = compiler.compile(&project)?;
239
240 let mut target_id: Option<ArtifactId> = None;
241
242 for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
244 if let Some(name) = &target_name {
245 if id.name != *name {
246 continue;
247 }
248 } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
249 || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
250 BytecodeObject::Bytecode(b) => b.is_empty(),
251 BytecodeObject::Unlinked(_) => false,
252 })
253 {
254 continue;
257 }
258
259 if let Some(target) = target_id {
260 let target_name = target.name.split('.').next().unwrap();
264 let id_name = id.name.split('.').next().unwrap();
265 if target_name != id_name {
266 eyre::bail!(
267 "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
268 );
269 }
270 }
271 target_id = Some(id);
272 }
273
274 let target = target_id.ok_or_eyre("Could not find target contract")?;
275
276 Ok(CompiledState {
277 args,
278 script_config,
279 script_wallets,
280 browser_wallet,
281 build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
282 })
283 }
284}
285
286pub struct CompiledState<FEN: FoundryEvmNetwork> {
288 pub args: ScriptArgs,
289 pub script_config: ScriptConfig<FEN>,
290 pub script_wallets: Wallets,
291 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
292 pub build_data: BuildData,
293}
294
295impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
296 pub async fn link(self) -> Result<LinkedState<FEN>> {
298 let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
299
300 let build_data = build_data.link(&script_config).await?;
301
302 Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
303 }
304
305 pub async fn resume(mut self) -> Result<BundledState<FEN>> {
307 let chain = if self.args.multi {
308 None
309 } else {
310 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")?;
311 let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
312 Some(provider.get_chain_id().await?)
313 };
314
315 let mut sequence = if self.sequence_exists(chain, false)? {
316 self.try_load_sequence(chain, false)?
317 } else {
318 let mut sequence = self.try_load_sequence(chain, true)?;
321
322 sequence.promote_to_broadcasted(
324 &self.script_config.config,
325 &self.args.sig,
326 &self.build_data.target,
327 )?;
328 sequence
329 };
330
331 let resolution = sequence.restore_delegated_pending(
332 self.args.resume_attempt,
333 self.args.resume_tx_hash,
334 self.args.resume_retry,
335 )?;
336 if !self.args.batch {
337 if let Some((sequence_index, _, attempt_id, hash)) = resolution {
338 let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
339 &self.script_config.config,
340 sequence.sequences()[sequence_index].rpc_url(),
341 )?
342 .build()?;
343 let transaction = provider
344 .get_transaction_by_hash(hash)
345 .await?
346 .context("resolved transaction is not available from the recovery endpoint")?;
347 sequence.resolve_delegated_hash(attempt_id, hash, &transaction)?;
348 }
349 let progress = ScriptProgress::default();
350 for index in 0..sequence.sequences().len() {
351 let provider = ProviderBuilder::<FEN::Network>::from_config_with_url(
352 &self.script_config.config,
353 sequence.sequences()[index].rpc_url(),
354 )?
355 .build()?;
356 for operation in 0..sequence.sequences()[index].transactions.len() {
359 let deployment = &sequence.sequences()[index];
360 if let Some(hash) = sequence.signed_payload(index, operation).map(|s| s.hash)
361 && !deployment.pending.contains(&hash)
362 && !deployment.receipts.iter().any(|r| r.transaction_hash() == hash)
363 && let Some(receipt) = provider.get_transaction_receipt(hash).await?
364 && is_mined_receipt_for(&receipt, hash)
365 {
366 sequence.sequences_mut()[index].add_pending(operation, hash);
367 }
368 }
369 if sequence.sequences()[index].pending.is_empty() {
370 continue;
371 }
372 let (durable_hashes, replayable_hashes) = sequence.submission_hashes(index);
373 let result = progress
374 .wait_for_pending(
375 index,
376 &mut sequence.sequences_mut()[index],
377 &provider,
378 self.script_config.config.transaction_timeout,
379 self.args.confirmations,
380 (&durable_hashes, &replayable_hashes),
381 )
382 .await;
383 sequence.save(true, false)?;
384 result?;
385 sequence.ensure_delegated_outcomes_known(index)?;
386 }
387 }
388
389 if !self.args.unlocked
390 && !remaining_unsigned_transactions_for_recovery(&sequence).is_empty()
391 {
392 self.script_wallets =
393 Wallets::new(self.args.wallets.get_multi_wallet().await?, self.args.evm.sender);
394 self.browser_wallet = self.args.wallets.browser_signer::<FEN::Network>().await?;
395
396 if self.args.evm.sender.is_none() {
397 let addresses = self.script_wallets.addresses();
398 let sender = self
399 .args
400 .maybe_load_private_key()?
401 .or_else(|| (addresses.len() == 1).then(|| addresses[0]))
402 .or_else(|| self.browser_wallet.as_ref().map(|wallet| wallet.address()));
403 if let Some(sender) = sender {
404 self.script_config.update_sender(sender).await?;
405 }
406 }
407 }
408
409 let (args, build_data, script_wallets, browser_wallet, script_config) =
410 if self.args.unlocked {
411 (
412 self.args,
413 self.build_data,
414 self.script_wallets,
415 self.browser_wallet,
416 self.script_config,
417 )
418 } else {
419 let remaining_transactions =
420 remaining_unsigned_transactions_for_recovery(&sequence);
421 let remaining_froms =
422 remaining_transactions.iter().map(|tx| tx.sender).collect::<AddressHashSet>();
423 let expected_session_sender = script_session_expected_sender_if_configured(
424 &self.script_config.tempo,
425 &remaining_froms,
426 )?;
427 let has_available_signers = has_available_script_signers(
428 &self.script_config.tempo,
429 &self.args.wallets,
430 &self.script_wallets,
431 expected_session_sender,
432 &remaining_transactions,
433 )?;
434
435 if has_available_signers {
436 (
437 self.args,
438 self.build_data,
439 self.script_wallets,
440 self.browser_wallet,
441 self.script_config,
442 )
443 } else {
444 let mut state = self;
447 state
448 .script_config
449 .update_tempo_session_sender(&state.args.wallets, state.args.evm.sender)
450 .await?;
451 let executed = state.link().await?.prepare_execution().await?.execute().await?;
452 (
453 executed.args,
454 executed.build_data.build_data,
455 executed.script_wallets,
456 executed.browser_wallet,
457 executed.script_config,
458 )
459 }
460 };
461
462 let libraries = if sequence.is_multi() {
464 Libraries::default()
466 } else {
467 Libraries::parse(&sequence.sequences()[0].libraries)?
468 };
469
470 let linked_build_data = build_data.link_with_libraries(libraries)?;
471
472 Ok(BundledState {
473 args,
474 script_config,
475 script_wallets,
476 browser_wallet,
477 build_data: linked_build_data,
478 sequence,
479 })
480 }
481
482 fn try_load_sequence(
483 &self,
484 chain: Option<u64>,
485 dry_run: bool,
486 ) -> Result<ScriptSequenceKind<FEN::Network>> {
487 if let Some(chain) = chain {
488 ScriptSequenceKind::load_single(
489 &self.script_config.config,
490 &self.args.sig,
491 &self.build_data.target,
492 chain,
493 dry_run,
494 self.args.batch,
495 )
496 } else {
497 ScriptSequenceKind::load_multi(
498 &self.script_config.config,
499 &self.args.sig,
500 &self.build_data.target,
501 dry_run,
502 self.args.batch,
503 )
504 }
505 }
506
507 fn sequence_exists(&self, chain: Option<u64>, dry_run: bool) -> Result<bool> {
508 let paths = if let Some(chain) = chain {
509 ScriptSequence::<FEN::Network>::get_paths(
510 &self.script_config.config,
511 &self.args.sig,
512 &self.build_data.target,
513 chain,
514 dry_run,
515 )?
516 } else {
517 MultiChainSequence::<FEN::Network>::get_paths(
518 &self.script_config.config,
519 &self.args.sig,
520 &self.build_data.target,
521 dry_run,
522 )?
523 };
524 Ok(compatibility_progress_exists(&paths) || recovery_exists(&paths)?)
525 }
526}
527
528fn compatibility_progress_exists(paths: &(PathBuf, PathBuf)) -> bool {
529 [&paths.0, &paths.1].into_iter().any(|path| progress_exists(path))
530}
531
532fn progress_exists(path: &Path) -> bool {
533 path.exists() || path.with_extension("previous").exists()
534}
535
536fn has_available_script_signers(
542 tempo: &TempoOpts,
543 wallets: &MultiWalletOpts,
544 script_wallets: &Wallets,
545 expected_sender: Option<Address>,
546 remaining: &[SignerScope],
547) -> Result<bool> {
548 let signers = script_wallets
549 .signers()
550 .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
551 if remaining.is_empty() {
552 return Ok(true);
553 }
554
555 let session_scope = tempo
556 .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
557 .map(|s| SignerScope::new(s.session.chain_id, s.access_key.account()));
558
559 Ok(remaining.iter().all(|tx| signers.contains(&tx.sender) || session_scope == Some(*tx)))
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn backup_counts_as_recoverable_progress() {
568 let dir = tempfile::tempdir().unwrap();
569 let path = dir.path().join("run-latest.json");
570 std::fs::write(path.with_extension("previous"), b"{}").unwrap();
571
572 assert!(progress_exists(&path));
573 }
574
575 #[test]
576 fn sensitive_export_counts_as_recoverable_progress() {
577 let dir = tempfile::tempdir().unwrap();
578 let paths = (dir.path().join("run-latest.json"), dir.path().join("run-latest-cache.json"));
579 std::fs::write(&paths.1, b"{}").unwrap();
580
581 assert!(compatibility_progress_exists(&paths));
582 }
583
584 #[test]
585 fn has_available_script_signers_skips_session_resolution_when_remaining_empty() {
586 let has_available = has_available_script_signers(
587 &TempoOpts { session: Some(B256::repeat_byte(0x99)), ..Default::default() },
588 &MultiWalletOpts::default(),
589 &Wallets::new(Default::default(), None),
590 None,
591 &[],
592 )
593 .unwrap();
594
595 assert!(has_available);
596 }
597}