1use crate::{
2 ScriptArgs, ScriptConfig,
3 broadcast::{BundledState, remaining_unsigned_transactions},
4 execute::LinkedState,
5 multi_sequence::MultiChainSequence,
6 sequence::ScriptSequenceKind,
7 session::{
8 RemainingScriptTransaction, SignerScope, script_session_expected_sender_if_configured,
9 },
10};
11use alloy_network::AnyNetwork;
12use alloy_primitives::{Address, B256, map::AddressHashSet};
13use alloy_provider::Provider;
14use eyre::{OptionExt, Result};
15use forge_script_sequence::ScriptSequence;
16use foundry_cheatcodes::Wallets;
17use foundry_cli::opts::TempoOpts;
18use foundry_common::{
19 ContractData, ContractsByArtifact, ContractsByArtifactBuilder, compile::ProjectCompiler,
20 provider::ProviderBuilder,
21};
22use foundry_compilers::{
23 ArtifactId, ProjectCompileOutput,
24 artifacts::{BytecodeObject, Libraries},
25 compilers::{Language, multi::MultiCompilerLanguage},
26 info::ContractInfo,
27 utils::source_files_iter,
28};
29use foundry_evm::{core::evm::FoundryEvmNetwork, traces::debug::ContractSources};
30use foundry_linking::Linker;
31use foundry_wallets::{MultiWalletOpts, wallet_browser::signer::BrowserSigner};
32use std::{path::PathBuf, str::FromStr, sync::Arc};
33
34#[derive(Clone, Debug)]
36pub struct BuildData {
37 pub project_root: PathBuf,
39 pub output: ProjectCompileOutput,
41 pub target: ArtifactId,
43}
44
45impl BuildData {
46 pub fn get_linker(&self) -> Linker<'_> {
47 Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
48 }
49
50 pub async fn link<FEN: FoundryEvmNetwork>(
53 self,
54 script_config: &ScriptConfig<FEN>,
55 ) -> Result<LinkedBuildData> {
56 let create2_deployer = script_config.evm_opts.create2_deployer;
57 let can_use_create2 = script_config
58 .evm_opts
59 .can_use_create2_deployer_resolved(script_config.resolved_fork()?)
60 .await?;
61
62 let known_libraries = script_config.config.libraries_with_remappings()?;
63
64 let maybe_create2_link_output = can_use_create2
65 .then(|| {
66 self.get_linker()
67 .link_with_create2_detailed(
68 known_libraries.clone(),
69 create2_deployer,
70 script_config.config.create2_library_salt,
71 [&self.target],
72 )
73 .ok()
74 })
75 .flatten();
76
77 let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
78 (
79 output.output.libraries,
80 ScriptPredeployLibraries::Create2 {
81 onchain: output.linked_libraries,
82 salt: script_config.config.create2_library_salt,
83 local: Vec::new(),
84 },
85 )
86 } else {
87 let output = self.get_linker().link_with_nonce_or_address_detailed(
88 known_libraries,
89 script_config.evm_opts.sender,
90 script_config.sender_nonce,
91 [&self.target],
92 )?;
93
94 (
95 output.output.libraries,
96 ScriptPredeployLibraries::Default {
97 onchain: output.linked_libraries,
98 local: Vec::new(),
99 },
100 )
101 };
102
103 LinkedBuildData::new(libraries, predeploy_libs, self)
104 }
105
106 pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
109 LinkedBuildData::new(
110 libraries,
111 ScriptPredeployLibraries::Default { onchain: Vec::new(), local: Vec::new() },
112 self,
113 )
114 }
115}
116
117#[derive(Clone, Debug)]
118pub enum ScriptPredeployLibraries {
119 Default {
120 onchain: Vec<foundry_linking::LinkedLibrary>,
121 local: Vec<foundry_linking::LinkedLibrary>,
122 },
123 Create2 {
124 onchain: Vec<foundry_linking::LinkedLibrary>,
125 salt: B256,
126 local: Vec<foundry_linking::LinkedLibrary>,
127 },
128}
129
130impl ScriptPredeployLibraries {
131 pub const fn libraries_count(&self) -> usize {
132 match self {
133 Self::Default { onchain, .. } => onchain.len(),
134 Self::Create2 { onchain, .. } => onchain.len(),
135 }
136 }
137}
138
139#[derive(Clone, Debug)]
141pub struct LinkedBuildData {
142 pub build_data: BuildData,
144 pub known_contracts: ContractsByArtifact,
146 pub libraries: Libraries,
148 pub predeploy_libraries: ScriptPredeployLibraries,
150 pub sources: ContractSources,
152}
153
154impl LinkedBuildData {
155 pub fn new(
156 libraries: Libraries,
157 predeploy_libraries: ScriptPredeployLibraries,
158 build_data: BuildData,
159 ) -> Result<Self> {
160 let sources = ContractSources::from_project_output(
161 &build_data.output,
162 &build_data.project_root,
163 Some(&libraries),
164 )?;
165
166 let linked_contracts = build_data.get_linker().get_linked_artifacts(&libraries)?;
167 let known_contracts = ContractsByArtifactBuilder::new(
168 linked_contracts.iter().map(|(id, artifact)| (id.clone(), artifact.into())),
169 )
170 .with_storage_layouts(build_data.output.artifact_ids().filter_map(|(id, artifact)| {
171 artifact.storage_layout.as_ref().map(|layout| (id, layout.clone()))
172 }))
173 .build();
174
175 Ok(Self { build_data, known_contracts, libraries, predeploy_libraries, sources })
176 }
177
178 pub fn get_target_contract(&self) -> Result<&ContractData> {
180 self.known_contracts
181 .get(&self.build_data.target)
182 .ok_or_eyre("target not found in linked artifacts")
183 }
184}
185
186pub struct PreprocessedState<FEN: FoundryEvmNetwork> {
188 pub args: ScriptArgs,
189 pub script_config: ScriptConfig<FEN>,
190 pub script_wallets: Wallets,
191 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
192}
193
194impl<FEN: FoundryEvmNetwork> PreprocessedState<FEN> {
195 pub fn compile(self) -> Result<CompiledState<FEN>> {
198 let Self { args, script_config, script_wallets, browser_wallet } = self;
199 let project = script_config.config.project()?;
200
201 let mut target_name = args.target_contract.clone();
202
203 let target_path = if let Ok(path) = dunce::canonicalize(&args.path) {
207 path
208 } else {
209 let contract = ContractInfo::from_str(&args.path)?;
210 target_name = Some(contract.name.clone());
211 if let Some(path) = contract.path {
212 dunce::canonicalize(path)?
213 } else {
214 project.find_contract_path(contract.name.as_str())?
215 }
216 };
217
218 let sources_to_compile = source_files_iter(
219 project.paths.sources.as_path(),
220 MultiCompilerLanguage::FILE_EXTENSIONS,
221 )
222 .chain([target_path.clone()]);
223
224 let output = ProjectCompiler::new()
225 .files(sources_to_compile)
226 .dynamic_test_linking(script_config.config.dynamic_test_linking)
227 .compile(&project)?;
228
229 let mut target_id: Option<ArtifactId> = None;
230
231 for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
233 if let Some(name) = &target_name {
234 if id.name != *name {
235 continue;
236 }
237 } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
238 || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
239 BytecodeObject::Bytecode(b) => b.is_empty(),
240 BytecodeObject::Unlinked(_) => false,
241 })
242 {
243 continue;
246 }
247
248 if let Some(target) = target_id {
249 let target_name = target.name.split('.').next().unwrap();
253 let id_name = id.name.split('.').next().unwrap();
254 if target_name != id_name {
255 eyre::bail!(
256 "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
257 );
258 }
259 }
260 target_id = Some(id);
261 }
262
263 let target = target_id.ok_or_eyre("Could not find target contract")?;
264
265 Ok(CompiledState {
266 args,
267 script_config,
268 script_wallets,
269 browser_wallet,
270 build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
271 })
272 }
273}
274
275pub struct CompiledState<FEN: FoundryEvmNetwork> {
277 pub args: ScriptArgs,
278 pub script_config: ScriptConfig<FEN>,
279 pub script_wallets: Wallets,
280 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
281 pub build_data: BuildData,
282}
283
284impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
285 pub async fn link(self) -> Result<LinkedState<FEN>> {
287 let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
288
289 let build_data = build_data.link(&script_config).await?;
290
291 Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
292 }
293
294 pub async fn resume(self) -> Result<BundledState<FEN>> {
296 let chain = if self.args.multi {
297 None
298 } else {
299 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")?;
300 let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
301 Some(provider.get_chain_id().await?)
302 };
303
304 let sequence = match self.try_load_sequence(chain, false) {
305 Ok(sequence) => sequence,
306 Err(_) => {
307 let mut sequence = self.try_load_sequence(chain, true)?;
310
311 sequence.update_paths_to_broadcasted(
314 &self.script_config.config,
315 &self.args.sig,
316 &self.build_data.target,
317 )?;
318
319 sequence.save(true, true)?;
320 sequence
321 }
322 };
323
324 let (args, build_data, script_wallets, browser_wallet, script_config) =
325 if self.args.unlocked {
326 (
327 self.args,
328 self.build_data,
329 self.script_wallets,
330 self.browser_wallet,
331 self.script_config,
332 )
333 } else {
334 let remaining_transactions =
335 remaining_unsigned_transactions(sequence.sequences()).collect::<Vec<_>>();
336 let remaining_froms =
337 remaining_transactions.iter().map(|tx| tx.from).collect::<AddressHashSet>();
338 let expected_session_sender = script_session_expected_sender_if_configured(
339 &self.script_config.tempo,
340 &remaining_froms,
341 )?;
342 let has_available_signers = has_available_script_signers(
343 &self.script_config.tempo,
344 &self.args.wallets,
345 &self.script_wallets,
346 expected_session_sender,
347 &remaining_transactions,
348 )?;
349
350 if has_available_signers {
351 (
352 self.args,
353 self.build_data,
354 self.script_wallets,
355 self.browser_wallet,
356 self.script_config,
357 )
358 } else {
359 let mut state = self;
362 state
363 .script_config
364 .update_tempo_session_sender(&state.args.wallets, state.args.evm.sender)
365 .await?;
366 let executed = state.link().await?.prepare_execution().await?.execute().await?;
367 (
368 executed.args,
369 executed.build_data.build_data,
370 executed.script_wallets,
371 executed.browser_wallet,
372 executed.script_config,
373 )
374 }
375 };
376
377 let libraries = match sequence {
379 ScriptSequenceKind::Single(ref seq) => Libraries::parse(&seq.libraries)?,
380 ScriptSequenceKind::Multi(_) => Libraries::default(),
382 };
383
384 let linked_build_data = build_data.link_with_libraries(libraries)?;
385
386 Ok(BundledState {
387 args,
388 script_config,
389 script_wallets,
390 browser_wallet,
391 build_data: linked_build_data,
392 sequence,
393 })
394 }
395
396 fn try_load_sequence(
397 &self,
398 chain: Option<u64>,
399 dry_run: bool,
400 ) -> Result<ScriptSequenceKind<FEN::Network>> {
401 if let Some(chain) = chain {
402 let sequence = ScriptSequence::load(
403 &self.script_config.config,
404 &self.args.sig,
405 &self.build_data.target,
406 chain,
407 dry_run,
408 )?;
409 Ok(ScriptSequenceKind::Single(sequence))
410 } else {
411 let sequence = MultiChainSequence::load(
412 &self.script_config.config,
413 &self.args.sig,
414 &self.build_data.target,
415 dry_run,
416 )?;
417 Ok(ScriptSequenceKind::Multi(sequence))
418 }
419 }
420}
421
422fn has_available_script_signers(
428 tempo: &TempoOpts,
429 wallets: &MultiWalletOpts,
430 script_wallets: &Wallets,
431 expected_sender: Option<Address>,
432 remaining: &[RemainingScriptTransaction],
433) -> Result<bool> {
434 let signers = script_wallets
435 .signers()
436 .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
437 if remaining.is_empty() {
438 return Ok(true);
439 }
440
441 let session_scope = tempo
442 .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
443 .map(|s| SignerScope::new(s.session.chain_id, s.access_key.account()));
444
445 Ok(remaining.iter().all(|tx| signers.contains(&tx.from) || session_scope == Some(tx.scope())))
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn has_available_script_signers_skips_session_resolution_when_remaining_empty() {
454 let has_available = has_available_script_signers(
455 &TempoOpts { session: Some(B256::repeat_byte(0x99)), ..Default::default() },
456 &MultiWalletOpts::default(),
457 &Wallets::new(Default::default(), None),
458 None,
459 &[],
460 )
461 .unwrap();
462
463 assert!(has_available);
464 }
465}