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, Bytes, 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, compile::ProjectCompiler, provider::ProviderBuilder,
20};
21use foundry_compilers::{
22 ArtifactId, ProjectCompileOutput,
23 artifacts::{BytecodeObject, Libraries},
24 compilers::{Language, multi::MultiCompilerLanguage},
25 info::ContractInfo,
26 utils::source_files_iter,
27};
28use foundry_evm::{core::evm::FoundryEvmNetwork, traces::debug::ContractSources};
29use foundry_linking::Linker;
30use foundry_wallets::{MultiWalletOpts, wallet_browser::signer::BrowserSigner};
31use std::{path::PathBuf, str::FromStr, sync::Arc};
32
33fn has_available_script_signers(
39 tempo: &TempoOpts,
40 wallets: &MultiWalletOpts,
41 script_wallets: &Wallets,
42 expected_sender: Option<Address>,
43 remaining: &[RemainingScriptTransaction],
44) -> Result<bool> {
45 let signers = script_wallets
46 .signers()
47 .map_err(|e| eyre::eyre!("Failed to get available signers: {}", e))?;
48 if remaining.is_empty() {
49 return Ok(true);
50 }
51
52 let session_scope = tempo
53 .session_signer_for_multi_wallet_any_chain(wallets, expected_sender)?
54 .map(|s| SignerScope::new(s.session.chain_id, s.access_key.wallet_address));
55
56 Ok(remaining.iter().all(|tx| signers.contains(&tx.from) || session_scope == Some(tx.scope())))
57}
58
59#[derive(Debug)]
61pub struct BuildData {
62 pub project_root: PathBuf,
64 pub output: ProjectCompileOutput,
66 pub target: ArtifactId,
68}
69
70impl BuildData {
71 pub fn get_linker(&self) -> Linker<'_> {
72 Linker::new(self.project_root.clone(), self.output.artifact_ids().collect())
73 }
74
75 pub async fn link<FEN: FoundryEvmNetwork>(
78 self,
79 script_config: &ScriptConfig<FEN>,
80 ) -> Result<LinkedBuildData> {
81 let create2_deployer = script_config.evm_opts.create2_deployer;
82 let can_use_create2 = if let Some(fork_url) = &script_config.evm_opts.fork_url {
83 let provider = ProviderBuilder::<AnyNetwork>::new(fork_url).build()?;
84 let deployer_code = provider.get_code_at(create2_deployer).await?;
85
86 !deployer_code.is_empty()
87 } else {
88 true
90 };
91
92 let known_libraries = script_config.config.libraries_with_remappings()?;
93
94 let maybe_create2_link_output = can_use_create2
95 .then(|| {
96 self.get_linker()
97 .link_with_create2(
98 known_libraries.clone(),
99 create2_deployer,
100 script_config.config.create2_library_salt,
101 &self.target,
102 )
103 .ok()
104 })
105 .flatten();
106
107 let (libraries, predeploy_libs) = if let Some(output) = maybe_create2_link_output {
108 (
109 output.libraries,
110 ScriptPredeployLibraries::Create2(
111 output.libs_to_deploy,
112 script_config.config.create2_library_salt,
113 ),
114 )
115 } else {
116 let output = self.get_linker().link_with_nonce_or_address(
117 known_libraries,
118 script_config.evm_opts.sender,
119 script_config.sender_nonce,
120 [&self.target],
121 )?;
122
123 (output.libraries, ScriptPredeployLibraries::Default(output.libs_to_deploy))
124 };
125
126 LinkedBuildData::new(libraries, predeploy_libs, self)
127 }
128
129 pub fn link_with_libraries(self, libraries: Libraries) -> Result<LinkedBuildData> {
132 LinkedBuildData::new(libraries, ScriptPredeployLibraries::Default(Vec::new()), self)
133 }
134}
135
136#[derive(Debug)]
137pub enum ScriptPredeployLibraries {
138 Default(Vec<Bytes>),
139 Create2(Vec<Bytes>, B256),
140}
141
142impl ScriptPredeployLibraries {
143 pub const fn libraries_count(&self) -> usize {
144 match self {
145 Self::Default(libs) => libs.len(),
146 Self::Create2(libs, _) => libs.len(),
147 }
148 }
149}
150
151#[derive(Debug)]
153pub struct LinkedBuildData {
154 pub build_data: BuildData,
156 pub known_contracts: ContractsByArtifact,
158 pub libraries: Libraries,
160 pub predeploy_libraries: ScriptPredeployLibraries,
162 pub sources: ContractSources,
164}
165
166impl LinkedBuildData {
167 pub fn new(
168 libraries: Libraries,
169 predeploy_libraries: ScriptPredeployLibraries,
170 build_data: BuildData,
171 ) -> Result<Self> {
172 let sources = ContractSources::from_project_output(
173 &build_data.output,
174 &build_data.project_root,
175 Some(&libraries),
176 )?;
177
178 let known_contracts =
179 ContractsByArtifact::new(build_data.get_linker().get_linked_artifacts(&libraries)?);
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) = dunce::canonicalize(&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 dunce::canonicalize(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 .chain([target_path.clone()]);
229
230 let output = ProjectCompiler::new()
231 .files(sources_to_compile)
232 .dynamic_test_linking(script_config.config.dynamic_test_linking)
233 .compile(&project)?;
234
235 let mut target_id: Option<ArtifactId> = None;
236
237 for (id, contract) in output.artifact_ids().filter(|(id, _)| id.source == target_path) {
239 if let Some(name) = &target_name {
240 if id.name != *name {
241 continue;
242 }
243 } else if contract.abi.as_ref().is_none_or(|abi| abi.is_empty())
244 || contract.bytecode.as_ref().is_none_or(|b| match &b.object {
245 BytecodeObject::Bytecode(b) => b.is_empty(),
246 BytecodeObject::Unlinked(_) => false,
247 })
248 {
249 continue;
252 }
253
254 if let Some(target) = target_id {
255 let target_name = target.name.split('.').next().unwrap();
259 let id_name = id.name.split('.').next().unwrap();
260 if target_name != id_name {
261 eyre::bail!(
262 "Multiple contracts in the target path. Please specify the contract name with `--tc ContractName`"
263 )
264 }
265 }
266 target_id = Some(id);
267 }
268
269 let target = target_id.ok_or_eyre("Could not find target contract")?;
270
271 Ok(CompiledState {
272 args,
273 script_config,
274 script_wallets,
275 browser_wallet,
276 build_data: BuildData { output, target, project_root: project.root().to_path_buf() },
277 })
278 }
279}
280
281pub struct CompiledState<FEN: FoundryEvmNetwork> {
283 pub args: ScriptArgs,
284 pub script_config: ScriptConfig<FEN>,
285 pub script_wallets: Wallets,
286 pub browser_wallet: Option<BrowserSigner<FEN::Network>>,
287 pub build_data: BuildData,
288}
289
290impl<FEN: FoundryEvmNetwork> CompiledState<FEN> {
291 pub async fn link(self) -> Result<LinkedState<FEN>> {
293 let Self { args, script_config, script_wallets, browser_wallet, build_data } = self;
294
295 let build_data = build_data.link(&script_config).await?;
296
297 Ok(LinkedState { args, script_config, script_wallets, browser_wallet, build_data })
298 }
299
300 pub async fn resume(self) -> Result<BundledState<FEN>> {
302 let chain = if self.args.multi {
303 None
304 } else {
305 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")?;
306 let provider = Arc::new(ProviderBuilder::<AnyNetwork>::new(&fork_url).build()?);
307 Some(provider.get_chain_id().await?)
308 };
309
310 let sequence = match self.try_load_sequence(chain, false) {
311 Ok(sequence) => sequence,
312 Err(_) => {
313 let mut sequence = self.try_load_sequence(chain, true)?;
316
317 sequence.update_paths_to_broadcasted(
320 &self.script_config.config,
321 &self.args.sig,
322 &self.build_data.target,
323 )?;
324
325 sequence.save(true, true)?;
326 sequence
327 }
328 };
329
330 let (args, build_data, script_wallets, browser_wallet, script_config) =
331 if self.args.unlocked {
332 (
333 self.args,
334 self.build_data,
335 self.script_wallets,
336 self.browser_wallet,
337 self.script_config,
338 )
339 } else {
340 let remaining_transactions =
341 remaining_unsigned_transactions(sequence.sequences()).collect::<Vec<_>>();
342 let remaining_froms =
343 remaining_transactions.iter().map(|tx| tx.from).collect::<AddressHashSet>();
344 let expected_session_sender = script_session_expected_sender_if_configured(
345 &self.script_config.tempo,
346 &remaining_froms,
347 )?;
348 let has_available_signers = has_available_script_signers(
349 &self.script_config.tempo,
350 &self.args.wallets,
351 &self.script_wallets,
352 expected_session_sender,
353 &remaining_transactions,
354 )?;
355
356 if has_available_signers {
357 (
358 self.args,
359 self.build_data,
360 self.script_wallets,
361 self.browser_wallet,
362 self.script_config,
363 )
364 } else {
365 let mut state = self;
368 state
369 .script_config
370 .update_tempo_session_sender(&state.args.wallets, state.args.evm.sender)
371 .await?;
372 let executed = state.link().await?.prepare_execution().await?.execute().await?;
373 (
374 executed.args,
375 executed.build_data.build_data,
376 executed.script_wallets,
377 executed.browser_wallet,
378 executed.script_config,
379 )
380 }
381 };
382
383 let libraries = match sequence {
385 ScriptSequenceKind::Single(ref seq) => Libraries::parse(&seq.libraries)?,
386 ScriptSequenceKind::Multi(_) => Libraries::default(),
388 };
389
390 let linked_build_data = build_data.link_with_libraries(libraries)?;
391
392 Ok(BundledState {
393 args,
394 script_config,
395 script_wallets,
396 browser_wallet,
397 build_data: linked_build_data,
398 sequence,
399 })
400 }
401
402 fn try_load_sequence(
403 &self,
404 chain: Option<u64>,
405 dry_run: bool,
406 ) -> Result<ScriptSequenceKind<FEN::Network>> {
407 if let Some(chain) = chain {
408 let sequence = ScriptSequence::load(
409 &self.script_config.config,
410 &self.args.sig,
411 &self.build_data.target,
412 chain,
413 dry_run,
414 )?;
415 Ok(ScriptSequenceKind::Single(sequence))
416 } else {
417 let sequence = MultiChainSequence::load(
418 &self.script_config.config,
419 &self.args.sig,
420 &self.build_data.target,
421 dry_run,
422 )?;
423 Ok(ScriptSequenceKind::Multi(sequence))
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn has_available_script_signers_skips_session_resolution_when_remaining_empty() {
434 let has_available = has_available_script_signers(
435 &TempoOpts { session: Some(B256::repeat_byte(0x99)), ..Default::default() },
436 &MultiWalletOpts::default(),
437 &Wallets::new(Default::default(), None),
438 None,
439 &[],
440 )
441 .unwrap();
442
443 assert!(has_available);
444 }
445}