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);
108
109 script_sequence.paths = Some((path, sensitive_path));
110
111 Ok(script_sequence)
112 }
113
114 pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()>
118 where
119 N::TxEnvelope: Serialize,
120 {
121 self.sort_receipts();
122
123 if self.transactions.is_empty() {
124 return Ok(());
125 }
126
127 self.timestamp = now().as_millis();
128 let ts_name = format!("run-{}.json", self.timestamp);
129
130 let sensitive_script_sequence = SensitiveScriptSequence::from(&*self);
131
132 let Some((path, sensitive_path)) = self.paths.as_ref() else { return Ok(()) };
133
134 fs::write_pretty_json_file(path, &self)?;
137 if save_ts {
138 fs::copy(path, path.with_file_name(&ts_name))?;
140 }
141
142 fs::write_pretty_json_file(sensitive_path, &sensitive_script_sequence)?;
145 if save_ts {
146 fs::copy(sensitive_path, sensitive_path.with_file_name(&ts_name))?;
148 }
149
150 if !silent {
151 if shell::is_json() {
152 sh_println!(
153 "{}",
154 serde_json::json!({
155 "status": "success",
156 "transactions": path.display().to_string(),
157 "sensitive": sensitive_path.display().to_string(),
158 })
159 )?;
160 } else {
161 sh_println!("\nTransactions saved to: {}\n", path.display())?;
162 sh_println!("Sensitive values saved to: {}\n", sensitive_path.display())?;
163 }
164 }
165
166 Ok(())
167 }
168
169 pub fn add_receipt(&mut self, receipt: N::ReceiptResponse) {
170 self.receipts.push(receipt);
171 }
172
173 pub fn sort_receipts(&mut self) {
175 self.receipts.sort_by_key(|r| (r.block_number(), r.transaction_index()));
176 }
177
178 pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) {
179 if !self.pending.contains(&tx_hash) {
180 self.transactions[index].hash = Some(tx_hash);
181 self.pending.push(tx_hash);
182 }
183 }
184
185 pub fn remove_pending(&mut self, tx_hash: TxHash) {
186 self.pending.retain(|element| element != &tx_hash);
187 }
188
189 pub fn get_paths(
193 config: &Config,
194 sig: &str,
195 target: &ArtifactId,
196 chain_id: u64,
197 dry_run: bool,
198 ) -> Result<(PathBuf, PathBuf)> {
199 let mut broadcast = config.broadcast.to_path_buf();
200 let mut cache = config.cache_path.to_path_buf();
201 let mut common = PathBuf::new();
202
203 let target_fname = target.source.file_name().wrap_err("No filename.")?;
204 common.push(target_fname);
205 common.push(chain_id.to_string());
206 if dry_run {
207 common.push(DRY_RUN_DIR);
208 }
209
210 broadcast.push(common.clone());
211 cache.push(common);
212
213 fs::create_dir_all(&broadcast)?;
214 fs::create_dir_all(&cache)?;
215
216 let filename = sig_to_file_name(sig);
218 let filename_with_ext = format!("{filename}-latest.json");
219
220 broadcast.push(&filename_with_ext);
221 cache.push(&filename_with_ext);
222
223 Ok((broadcast, cache))
224 }
225
226 pub fn rpc_url(&self) -> &str {
228 self.transactions.front().expect("empty sequence").rpc.as_str()
229 }
230
231 pub fn transactions(&self) -> impl Iterator<Item = &TransactionMaybeSigned<N>> {
233 self.transactions.iter().map(|tx| tx.tx())
234 }
235
236 pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) {
237 self.transactions
238 .iter_mut()
239 .enumerate()
240 .for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc));
241 }
242}
243
244pub fn sig_to_file_name(sig: &str) -> String {
248 if let Some((name, _)) = sig.split_once('(') {
249 return name.to_string();
251 }
252 if let Ok(calldata) = hex::decode(sig.strip_prefix("0x").unwrap_or(sig)) {
254 if let Some(selector) = calldata.get(..SELECTOR_LEN) {
256 return hex::encode(selector);
257 }
258 return sig.to_string();
260 }
261
262 sig.to_string()
263}
264
265pub fn now() -> Duration {
266 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn can_convert_sig() {
275 assert_eq!(sig_to_file_name("run()").as_str(), "run");
276 assert_eq!(
277 sig_to_file_name(
278 "522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
279 )
280 .as_str(),
281 "522bb704"
282 );
283 assert_eq!(
285 sig_to_file_name(
286 "0x522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
287 )
288 .as_str(),
289 "522bb704"
290 );
291 assert_eq!(sig_to_file_name("0x1234").as_str(), "0x1234");
293 assert_eq!(sig_to_file_name("123").as_str(), "123");
294 assert_eq!(sig_to_file_name("0xnotahex").as_str(), "0xnotahex");
296 assert_eq!(sig_to_file_name("not_a_sig_or_hex").as_str(), "not_a_sig_or_hex");
298 }
299}