1use crate::transaction::TransactionWithMetadata;
2use alloy_network::{Network, ReceiptResponse};
3use alloy_primitives::{TxHash, hex, map::HashMap};
4use eyre::{ContextCompat, Result, WrapErr};
5use foundry_common::{SELECTOR_LEN, TransactionMaybeSigned, fs, shell};
6use foundry_compilers::ArtifactId;
7use foundry_config::Config;
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::VecDeque,
11 path::PathBuf,
12 time::{Duration, SystemTime, UNIX_EPOCH},
13};
14
15pub const DRY_RUN_DIR: &str = "dry-run";
16
17#[derive(Clone, Serialize, Deserialize)]
18pub struct NestedValue {
19 pub internal_type: String,
20 pub value: String,
21}
22
23#[derive(Clone, Default, Serialize, Deserialize)]
25pub struct SensitiveTransactionMetadata {
26 pub rpc: String,
27}
28
29#[derive(Clone, Default, Serialize, Deserialize)]
31pub struct SensitiveScriptSequence {
32 pub transactions: VecDeque<SensitiveTransactionMetadata>,
33}
34
35#[derive(Clone, Serialize, Deserialize)]
38#[serde(bound(
39 serialize = "N::TransactionRequest: Serialize, N::TxEnvelope: Serialize",
40 deserialize = "N::TransactionRequest: for<'de2> Deserialize<'de2>, N::TxEnvelope: for<'de2> Deserialize<'de2>"
41))]
42pub struct ScriptSequence<N: Network> {
43 pub transactions: VecDeque<TransactionWithMetadata<N>>,
44 pub receipts: Vec<N::ReceiptResponse>,
45 pub libraries: Vec<String>,
46 pub pending: Vec<TxHash>,
47 #[serde(skip)]
48 pub paths: Option<(PathBuf, PathBuf)>,
51 pub returns: HashMap<String, NestedValue>,
52 pub timestamp: u128,
53 pub chain: u64,
54 pub commit: Option<String>,
55}
56
57impl<N: Network> Default for ScriptSequence<N> {
58 fn default() -> Self {
59 Self {
60 transactions: Default::default(),
61 receipts: Default::default(),
62 libraries: Default::default(),
63 pending: Default::default(),
64 paths: Default::default(),
65 returns: Default::default(),
66 timestamp: Default::default(),
67 chain: Default::default(),
68 commit: Default::default(),
69 }
70 }
71}
72
73impl<N: Network> From<&ScriptSequence<N>> for SensitiveScriptSequence {
74 fn from(sequence: &ScriptSequence<N>) -> Self {
75 Self {
76 transactions: sequence
77 .transactions
78 .iter()
79 .map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() })
80 .collect(),
81 }
82 }
83}
84
85impl<N: Network> ScriptSequence<N> {
86 pub fn load(
88 config: &Config,
89 sig: &str,
90 target: &ArtifactId,
91 chain_id: u64,
92 dry_run: bool,
93 ) -> Result<Self>
94 where
95 N::TxEnvelope: for<'d> Deserialize<'d>,
96 {
97 let (path, sensitive_path) = Self::get_paths(config, sig, target, chain_id, dry_run)?;
98
99 let mut script_sequence: Self = fs::read_json_file(&path)
100 .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?;
101
102 let sensitive_script_sequence: SensitiveScriptSequence = fs::read_json_file(
103 &sensitive_path,
104 )
105 .wrap_err(format!("Deployment's sensitive details not found for chain `{chain_id}`."))?;
106
107 script_sequence.fill_sensitive(&sensitive_script_sequence).wrap_err(format!(
108 "Deployment's sensitive details are out of sync with the broadcast file for chain `{chain_id}`; restore matching broadcast and sensitive-cache files before resuming."
109 ))?;
110
111 script_sequence.paths = Some((path, sensitive_path));
112
113 Ok(script_sequence)
114 }
115
116 pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()>
120 where
121 N::TxEnvelope: Serialize,
122 {
123 self.sort_receipts();
124
125 if self.transactions.is_empty() {
126 return Ok(());
127 }
128
129 self.timestamp = now().as_millis();
130 let ts_name = format!("run-{}.json", self.timestamp);
131
132 let sensitive_script_sequence = SensitiveScriptSequence::from(&*self);
133
134 let Some((path, sensitive_path)) = self.paths.as_ref() else { return Ok(()) };
135
136 fs::write_pretty_json_file(path, &self)?;
139 if save_ts {
140 fs::copy(path, path.with_file_name(&ts_name))?;
142 }
143
144 fs::write_sensitive_json_file(sensitive_path, &sensitive_script_sequence)?;
147 if save_ts {
148 fs::copy(sensitive_path, sensitive_path.with_file_name(&ts_name))?;
150 }
151
152 if !silent {
153 if shell::is_json() {
154 sh_println!(
155 "{}",
156 serde_json::json!({
157 "status": "success",
158 "transactions": path.display().to_string(),
159 "sensitive": sensitive_path.display().to_string(),
160 })
161 )?;
162 } else {
163 sh_println!("\nTransactions saved to: {}\n", path.display())?;
164 sh_println!("Sensitive values saved to: {}\n", sensitive_path.display())?;
165 }
166 }
167
168 Ok(())
169 }
170
171 pub fn add_receipt(&mut self, receipt: N::ReceiptResponse) {
172 self.receipts.push(receipt);
173 }
174
175 pub fn sort_receipts(&mut self) {
177 self.receipts.sort_by_key(|r| (r.block_number(), r.transaction_index()));
178 }
179
180 pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) {
181 if !self.pending.contains(&tx_hash) {
182 self.transactions[index].hash = Some(tx_hash);
183 self.pending.push(tx_hash);
184 }
185 }
186
187 pub fn remove_pending(&mut self, tx_hash: TxHash) {
188 self.pending.retain(|element| element != &tx_hash);
189 }
190
191 pub fn get_paths(
195 config: &Config,
196 sig: &str,
197 target: &ArtifactId,
198 chain_id: u64,
199 dry_run: bool,
200 ) -> Result<(PathBuf, PathBuf)> {
201 let mut broadcast = config.broadcast.clone();
202 let mut cache = config.cache_path.clone();
203 let mut common = PathBuf::new();
204
205 let target_fname = target.source.file_name().wrap_err("No filename.")?;
206 common.push(target_fname);
207 common.push(chain_id.to_string());
208 if dry_run {
209 common.push(DRY_RUN_DIR);
210 }
211
212 broadcast.push(common.clone());
213 cache.push(common);
214
215 fs::create_dir_all(&broadcast)?;
216 fs::create_dir_all(&cache)?;
217
218 let filename = sig_to_file_name(sig);
220 let filename_with_ext = format!("{filename}-latest.json");
221
222 broadcast.push(&filename_with_ext);
223 cache.push(&filename_with_ext);
224
225 Ok((broadcast, cache))
226 }
227
228 pub fn rpc_url(&self) -> &str {
230 self.transactions.front().expect("empty sequence").rpc.as_str()
231 }
232
233 pub fn transactions(&self) -> impl Iterator<Item = &TransactionMaybeSigned<N>> {
235 self.transactions.iter().map(|tx| tx.tx())
236 }
237
238 pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) -> Result<()> {
240 let transactions_len = self.transactions.len();
241 let sensitive_len = sensitive.transactions.len();
242 if transactions_len != sensitive_len {
243 eyre::bail!(
244 "sensitive-cache entry count ({sensitive_len}) does not match transaction count \
245 ({transactions_len}); the broadcast file and its sensitive-cache counterpart are \
246 out of sync"
247 );
248 }
249 for (i, tx) in self.transactions.iter_mut().enumerate() {
250 tx.rpc.clone_from(&sensitive.transactions[i].rpc);
251 }
252 Ok(())
253 }
254}
255
256pub fn sig_to_file_name(sig: &str) -> String {
260 if let Some((name, _)) = sig.split_once('(') {
261 return name.to_string();
263 }
264 if let Ok(calldata) = hex::decode(sig.strip_prefix("0x").unwrap_or(sig)) {
266 if let Some(selector) = calldata.get(..SELECTOR_LEN) {
268 return hex::encode(selector);
269 }
270 return sig.to_string();
272 }
273
274 sig.to_string()
275}
276
277pub fn now() -> Duration {
278 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use alloy_network::Ethereum;
285
286 fn sequence_with_two_transactions() -> ScriptSequence<Ethereum> {
287 let mut sequence = ScriptSequence::default();
288 for rpc in ["first", "second"] {
289 let mut tx = TransactionWithMetadata::from_tx_request(
290 TransactionMaybeSigned::Unsigned(Default::default()),
291 );
292 tx.rpc = rpc.to_string();
293 sequence.transactions.push_back(tx);
294 }
295 sequence
296 }
297
298 #[test]
299 fn fill_sensitive_rejects_mismatched_counts_without_mutation() {
300 for count in [1, 3] {
301 let mut sequence = sequence_with_two_transactions();
302 let sensitive = SensitiveScriptSequence {
303 transactions: (0..count)
304 .map(|_| SensitiveTransactionMetadata { rpc: "replacement".to_string() })
305 .collect(),
306 };
307 assert_eq!(
308 sequence.fill_sensitive(&sensitive).unwrap_err().to_string(),
309 format!(
310 "sensitive-cache entry count ({count}) does not match transaction count (2); \
311 the broadcast file and its sensitive-cache counterpart are out of sync"
312 )
313 );
314 assert_eq!(
315 sequence.transactions.iter().map(|tx| tx.rpc.as_str()).collect::<Vec<_>>(),
316 ["first", "second"]
317 );
318 }
319 }
320
321 #[test]
322 fn fill_sensitive_restores_matching_cache() {
323 let mut sequence = sequence_with_two_transactions();
324 let sensitive = SensitiveScriptSequence {
325 transactions: ["restored-first", "restored-second"]
326 .into_iter()
327 .map(|rpc| SensitiveTransactionMetadata { rpc: rpc.to_string() })
328 .collect(),
329 };
330 sequence.fill_sensitive(&sensitive).unwrap();
331 assert_eq!(
332 sequence.transactions.iter().map(|tx| tx.rpc.as_str()).collect::<Vec<_>>(),
333 ["restored-first", "restored-second"]
334 );
335 }
336
337 #[test]
338 fn can_convert_sig() {
339 assert_eq!(sig_to_file_name("run()").as_str(), "run");
340 assert_eq!(
341 sig_to_file_name(
342 "522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
343 )
344 .as_str(),
345 "522bb704"
346 );
347 assert_eq!(
349 sig_to_file_name(
350 "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
351 )
352 .as_str(),
353 "522bb704"
354 );
355 assert_eq!(sig_to_file_name("0x1234").as_str(), "0x1234");
357 assert_eq!(sig_to_file_name("123").as_str(), "123");
358 assert_eq!(sig_to_file_name("0xnotahex").as_str(), "0xnotahex");
360 assert_eq!(sig_to_file_name("not_a_sig_or_hex").as_str(), "not_a_sig_or_hex");
362 }
363}