1use crate::{TestCommand, init_tracing, util::lossy_string};
2use alloy_primitives::{Address, address};
3use alloy_provider::Provider;
4use eyre::Result;
5use foundry_common::provider::{RetryProvider, get_http_provider};
6use std::{
7 collections::BTreeMap,
8 fs,
9 path::{Path, PathBuf},
10};
11
12const BROADCAST_TEST_PATH: &str = "src/Broadcast.t.sol";
13const TESTDATA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata");
14
15fn init_script_cmd(
16 cmd: &mut TestCommand,
17 project_root: &Path,
18 target_contract: &str,
19 endpoint: Option<&str>,
20) {
21 cmd.forge_fuse();
22 cmd.set_current_dir(project_root);
23
24 cmd.args(["script", target_contract, "--root", project_root.to_str().unwrap(), "-vvvvv"]);
25
26 if let Some(rpc_url) = endpoint {
27 cmd.args(["--fork-url", rpc_url]);
28 }
29}
30pub struct ScriptTester {
32 pub accounts_pub: Vec<Address>,
33 pub accounts_priv: Vec<String>,
34 pub provider: Option<RetryProvider>,
35 pub nonces: BTreeMap<u32, u64>,
36 pub address_nonces: BTreeMap<Address, u64>,
37 pub cmd: TestCommand,
38 pub project_root: PathBuf,
39 pub target_contract: String,
40 pub endpoint: Option<String>,
41}
42
43impl ScriptTester {
44 pub fn new(
46 mut cmd: TestCommand,
47 endpoint: Option<&str>,
48 project_root: &Path,
49 target_contract: &str,
50 ) -> Self {
51 init_tracing();
52 Self::copy_testdata(project_root).unwrap();
53 init_script_cmd(&mut cmd, project_root, target_contract, endpoint);
54
55 let mut provider = None;
56 if let Some(endpoint) = endpoint {
57 provider = Some(get_http_provider(endpoint))
58 }
59
60 Self {
61 accounts_pub: vec![
62 address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
63 address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"),
64 address!("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"),
65 ],
66 accounts_priv: vec![
67 "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".to_string(),
68 "59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d".to_string(),
69 "5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a".to_string(),
70 ],
71 provider,
72 nonces: BTreeMap::default(),
73 address_nonces: BTreeMap::default(),
74 cmd,
75 project_root: project_root.to_path_buf(),
76 target_contract: target_contract.to_string(),
77 endpoint: endpoint.map(|s| s.to_string()),
78 }
79 }
80
81 pub fn new_broadcast(cmd: TestCommand, endpoint: &str, project_root: &Path) -> Self {
84 let target_contract = project_root.join(BROADCAST_TEST_PATH).to_string_lossy().to_string();
85
86 fs::copy(
88 Self::testdata_path().join("default/cheats/Broadcast.t.sol"),
89 project_root.join(BROADCAST_TEST_PATH),
90 )
91 .expect("Failed to initialize broadcast contract");
92
93 Self::new(cmd, Some(endpoint), project_root, &target_contract)
94 }
95
96 pub fn new_broadcast_without_endpoint(cmd: TestCommand, project_root: &Path) -> Self {
99 let target_contract = project_root.join(BROADCAST_TEST_PATH).to_string_lossy().to_string();
100
101 let testdata = Self::testdata_path();
103 fs::copy(
104 testdata.join("default/cheats/Broadcast.t.sol"),
105 project_root.join(BROADCAST_TEST_PATH),
106 )
107 .expect("Failed to initialize broadcast contract");
108
109 Self::new(cmd, None, project_root, &target_contract)
110 }
111
112 fn testdata_path() -> &'static Path {
114 Path::new(TESTDATA)
115 }
116
117 fn copy_testdata(root: &Path) -> Result<()> {
119 let testdata = Self::testdata_path();
120 let from_dir = testdata.join("utils");
121 let to_dir = root.join("utils");
122 fs::create_dir_all(&to_dir)?;
123 for entry in fs::read_dir(&from_dir)? {
124 let file = &entry?.path();
125 let name = file.file_name().unwrap();
126 fs::copy(file, to_dir.join(name))?;
127 }
128 Ok(())
129 }
130
131 pub async fn load_private_keys(&mut self, keys_indexes: &[u32]) -> &mut Self {
132 for &index in keys_indexes {
133 self.cmd.args(["--private-keys", &self.accounts_priv[index as usize]]);
134
135 if let Some(provider) = &self.provider {
136 let nonce = provider
137 .get_transaction_count(self.accounts_pub[index as usize])
138 .await
139 .unwrap();
140 self.nonces.insert(index, nonce);
141 }
142 }
143 self
144 }
145
146 pub async fn load_addresses(&mut self, addresses: &[Address]) -> &mut Self {
147 for &address in addresses {
148 let nonce =
149 self.provider.as_ref().unwrap().get_transaction_count(address).await.unwrap();
150 self.address_nonces.insert(address, nonce);
151 }
152 self
153 }
154
155 pub fn add_deployer(&mut self, index: u32) -> &mut Self {
156 self.sender(self.accounts_pub[index as usize])
157 }
158
159 pub fn sender(&mut self, addr: Address) -> &mut Self {
161 self.args(&["--sender", &addr.to_string()])
162 }
163
164 pub fn add_sig(&mut self, contract_name: &str, sig: &str) -> &mut Self {
165 self.args(&["--tc", contract_name, "--sig", sig])
166 }
167
168 pub fn add_create2_deployer(&mut self, create2_deployer: Address) -> &mut Self {
169 self.args(&["--create2-deployer", &create2_deployer.to_string()])
170 }
171
172 pub fn unlocked(&mut self) -> &mut Self {
174 self.arg("--unlocked")
175 }
176
177 pub fn simulate(&mut self, expected: ScriptOutcome) -> &mut Self {
178 self.run(expected)
179 }
180
181 pub fn broadcast(&mut self, expected: ScriptOutcome) -> &mut Self {
182 self.arg("--broadcast").run(expected)
183 }
184
185 pub fn resume(&mut self, expected: ScriptOutcome) -> &mut Self {
186 self.arg("--resume").run(expected)
187 }
188
189 pub async fn assert_nonce_increment(&mut self, keys_indexes: &[(u32, u32)]) -> &mut Self {
191 for &(private_key_slot, expected_increment) in keys_indexes {
192 let addr = self.accounts_pub[private_key_slot as usize];
193 let nonce = self.provider.as_ref().unwrap().get_transaction_count(addr).await.unwrap();
194 let prev_nonce = self.nonces.get(&private_key_slot).unwrap();
195
196 assert_eq!(
197 nonce,
198 (*prev_nonce + expected_increment as u64),
199 "nonce not incremented correctly for {addr}: \
200 {prev_nonce} + {expected_increment} != {nonce}"
201 );
202 }
203 self
204 }
205
206 pub async fn assert_nonce_increment_addresses(
208 &mut self,
209 address_indexes: &[(Address, u32)],
210 ) -> &mut Self {
211 for (address, expected_increment) in address_indexes {
212 let nonce =
213 self.provider.as_ref().unwrap().get_transaction_count(*address).await.unwrap();
214 let prev_nonce = self.address_nonces.get(address).unwrap();
215
216 assert_eq!(nonce, *prev_nonce + *expected_increment as u64);
217 }
218 self
219 }
220
221 pub fn run(&mut self, expected: ScriptOutcome) -> &mut Self {
222 let out = self.cmd.execute();
223 let (stdout, stderr) = (lossy_string(&out.stdout), lossy_string(&out.stderr));
224
225 trace!(target: "tests", "STDOUT\n{stdout}\n\nSTDERR\n{stderr}");
226
227 if !stdout.contains(expected.as_str()) && !stderr.contains(expected.as_str()) {
228 panic!(
229 "--STDOUT--\n{stdout}\n\n--STDERR--\n{stderr}\n\n--EXPECTED--\n{:?} not found in stdout or stderr",
230 expected.as_str()
231 );
232 }
233
234 self
235 }
236
237 pub fn slow(&mut self) -> &mut Self {
238 self.arg("--slow")
239 }
240
241 pub fn arg(&mut self, arg: &str) -> &mut Self {
242 self.cmd.arg(arg);
243 self
244 }
245
246 pub fn args(&mut self, args: &[&str]) -> &mut Self {
247 self.cmd.args(args);
248 self
249 }
250
251 pub fn clear(&mut self) {
252 init_script_cmd(
253 &mut self.cmd,
254 &self.project_root,
255 &self.target_contract,
256 self.endpoint.as_deref(),
257 );
258 self.nonces.clear();
259 self.address_nonces.clear();
260 }
261}
262
263#[derive(Debug)]
265pub enum ScriptOutcome {
266 OkNoEndpoint,
267 OkSimulation,
268 OkBroadcast,
269 WarnSpecifyDeployer,
270 MissingSender,
271 MissingWallet,
272 StaticCallNotAllowed,
273 ScriptFailed,
274 UnsupportedLibraries,
275 ErrorSelectForkOnBroadcast,
276 OkRun,
277}
278
279impl ScriptOutcome {
280 pub fn as_str(&self) -> &'static str {
281 match self {
282 Self::OkNoEndpoint => "If you wish to simulate on-chain transactions pass a RPC URL.",
283 Self::OkSimulation => "SIMULATION COMPLETE. To broadcast these",
284 Self::OkBroadcast => "ONCHAIN EXECUTION COMPLETE & SUCCESSFUL",
285 Self::WarnSpecifyDeployer => {
286 "Warning: You have more than one deployer who could predeploy libraries. Using `--sender` instead."
287 }
288 Self::MissingSender => {
289 "You seem to be using Foundry's default sender. Be sure to set your own --sender"
290 }
291 Self::MissingWallet => "No associated wallet",
292 Self::StaticCallNotAllowed => {
293 "staticcall`s are not allowed after `broadcast`; use `startBroadcast` instead"
294 }
295 Self::ScriptFailed => "script failed: ",
296 Self::UnsupportedLibraries => {
297 "Multi chain deployment does not support library linking at the moment."
298 }
299 Self::ErrorSelectForkOnBroadcast => "cannot select forks during a broadcast",
300 Self::OkRun => "Script ran successfully",
301 }
302 }
303
304 pub fn is_err(&self) -> bool {
305 match self {
306 Self::OkNoEndpoint
307 | Self::OkSimulation
308 | Self::OkBroadcast
309 | Self::WarnSpecifyDeployer
310 | Self::OkRun => false,
311 Self::MissingSender
312 | Self::MissingWallet
313 | Self::StaticCallNotAllowed
314 | Self::UnsupportedLibraries
315 | Self::ErrorSelectForkOnBroadcast
316 | Self::ScriptFailed => true,
317 }
318 }
319}