forge_script_sequence/
sequence.rs

1use crate::transaction::TransactionWithMetadata;
2use alloy_network::AnyTransactionReceipt;
3use alloy_primitives::{hex, map::HashMap, TxHash};
4use eyre::{ContextCompat, Result, WrapErr};
5use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN};
6use foundry_compilers::ArtifactId;
7use foundry_config::Config;
8use serde::{Deserialize, Serialize};
9use std::{
10    collections::VecDeque,
11    io::{BufWriter, Write},
12    path::PathBuf,
13    time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16pub const DRY_RUN_DIR: &str = "dry-run";
17
18#[derive(Clone, Serialize, Deserialize)]
19pub struct NestedValue {
20    pub internal_type: String,
21    pub value: String,
22}
23
24/// Helper that saves the transactions sequence and its state on which transactions have been
25/// broadcasted
26#[derive(Clone, Default, Serialize, Deserialize)]
27pub struct ScriptSequence {
28    pub transactions: VecDeque<TransactionWithMetadata>,
29    pub receipts: Vec<AnyTransactionReceipt>,
30    pub libraries: Vec<String>,
31    pub pending: Vec<TxHash>,
32    #[serde(skip)]
33    /// Contains paths to the sequence files
34    /// None if sequence should not be saved to disk (e.g. part of a multi-chain sequence)
35    pub paths: Option<(PathBuf, PathBuf)>,
36    pub returns: HashMap<String, NestedValue>,
37    pub timestamp: u64,
38    pub chain: u64,
39    pub commit: Option<String>,
40}
41
42/// Sensitive values from the transactions in a script sequence
43#[derive(Clone, Default, Serialize, Deserialize)]
44pub struct SensitiveTransactionMetadata {
45    pub rpc: String,
46}
47
48/// Sensitive info from the script sequence which is saved into the cache folder
49#[derive(Clone, Default, Serialize, Deserialize)]
50pub struct SensitiveScriptSequence {
51    pub transactions: VecDeque<SensitiveTransactionMetadata>,
52}
53
54impl From<ScriptSequence> for SensitiveScriptSequence {
55    fn from(sequence: ScriptSequence) -> Self {
56        Self {
57            transactions: sequence
58                .transactions
59                .iter()
60                .map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() })
61                .collect(),
62        }
63    }
64}
65
66impl ScriptSequence {
67    /// Loads The sequence for the corresponding json file
68    pub fn load(
69        config: &Config,
70        sig: &str,
71        target: &ArtifactId,
72        chain_id: u64,
73        dry_run: bool,
74    ) -> Result<Self> {
75        let (path, sensitive_path) = Self::get_paths(config, sig, target, chain_id, dry_run)?;
76
77        let mut script_sequence: Self = fs::read_json_file(&path)
78            .wrap_err(format!("Deployment not found for chain `{chain_id}`."))?;
79
80        let sensitive_script_sequence: SensitiveScriptSequence = fs::read_json_file(
81            &sensitive_path,
82        )
83        .wrap_err(format!("Deployment's sensitive details not found for chain `{chain_id}`."))?;
84
85        script_sequence.fill_sensitive(&sensitive_script_sequence);
86
87        script_sequence.paths = Some((path, sensitive_path));
88
89        Ok(script_sequence)
90    }
91
92    /// Saves the transactions as file if it's a standalone deployment.
93    /// `save_ts` should be set to true for checkpoint updates, which might happen many times and
94    /// could result in us saving many identical files.
95    pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> {
96        self.sort_receipts();
97
98        if self.transactions.is_empty() {
99            return Ok(())
100        }
101
102        let Some((path, sensitive_path)) = self.paths.clone() else { return Ok(()) };
103
104        self.timestamp = now().as_secs();
105        let ts_name = format!("run-{}.json", self.timestamp);
106
107        let sensitive_script_sequence: SensitiveScriptSequence = self.clone().into();
108
109        // broadcast folder writes
110        //../run-latest.json
111        let mut writer = BufWriter::new(fs::create_file(&path)?);
112        serde_json::to_writer_pretty(&mut writer, &self)?;
113        writer.flush()?;
114        if save_ts {
115            //../run-[timestamp].json
116            fs::copy(&path, path.with_file_name(&ts_name))?;
117        }
118
119        // cache folder writes
120        //../run-latest.json
121        let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?);
122        serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?;
123        writer.flush()?;
124        if save_ts {
125            //../run-[timestamp].json
126            fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?;
127        }
128
129        if !silent {
130            if shell::is_json() {
131                sh_println!(
132                    "{}",
133                    serde_json::json!({
134                        "status": "success",
135                        "transactions": path.display().to_string(),
136                        "sensitive": sensitive_path.display().to_string(),
137                    })
138                )?;
139            } else {
140                sh_println!("\nTransactions saved to: {}\n", path.display())?;
141                sh_println!("Sensitive values saved to: {}\n", sensitive_path.display())?;
142            }
143        }
144
145        Ok(())
146    }
147
148    pub fn add_receipt(&mut self, receipt: AnyTransactionReceipt) {
149        self.receipts.push(receipt);
150    }
151
152    /// Sorts all receipts with ascending transaction index
153    pub fn sort_receipts(&mut self) {
154        self.receipts.sort_by_key(|r| (r.block_number, r.transaction_index));
155    }
156
157    pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) {
158        if !self.pending.contains(&tx_hash) {
159            self.transactions[index].hash = Some(tx_hash);
160            self.pending.push(tx_hash);
161        }
162    }
163
164    pub fn remove_pending(&mut self, tx_hash: TxHash) {
165        self.pending.retain(|element| element != &tx_hash);
166    }
167
168    /// Gets paths in the formats
169    /// `./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json` and
170    /// `./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json`.
171    pub fn get_paths(
172        config: &Config,
173        sig: &str,
174        target: &ArtifactId,
175        chain_id: u64,
176        dry_run: bool,
177    ) -> Result<(PathBuf, PathBuf)> {
178        let mut broadcast = config.broadcast.to_path_buf();
179        let mut cache = config.cache_path.to_path_buf();
180        let mut common = PathBuf::new();
181
182        let target_fname = target.source.file_name().wrap_err("No filename.")?;
183        common.push(target_fname);
184        common.push(chain_id.to_string());
185        if dry_run {
186            common.push(DRY_RUN_DIR);
187        }
188
189        broadcast.push(common.clone());
190        cache.push(common);
191
192        fs::create_dir_all(&broadcast)?;
193        fs::create_dir_all(&cache)?;
194
195        // TODO: ideally we want the name of the function here if sig is calldata
196        let filename = sig_to_file_name(sig);
197
198        broadcast.push(format!("{filename}-latest.json"));
199        cache.push(format!("{filename}-latest.json"));
200
201        Ok((broadcast, cache))
202    }
203
204    /// Returns the first RPC URL of this sequence.
205    pub fn rpc_url(&self) -> &str {
206        self.transactions.front().expect("empty sequence").rpc.as_str()
207    }
208
209    /// Returns the list of the transactions without the metadata.
210    pub fn transactions(&self) -> impl Iterator<Item = &TransactionMaybeSigned> {
211        self.transactions.iter().map(|tx| tx.tx())
212    }
213
214    pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) {
215        self.transactions
216            .iter_mut()
217            .enumerate()
218            .for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc));
219    }
220}
221
222/// Converts the `sig` argument into the corresponding file path.
223///
224/// This accepts either the signature of the function or the raw calldata.
225pub fn sig_to_file_name(sig: &str) -> String {
226    if let Some((name, _)) = sig.split_once('(') {
227        // strip until call argument parenthesis
228        return name.to_string()
229    }
230    // assume calldata if `sig` is hex
231    if let Ok(calldata) = hex::decode(sig) {
232        // in which case we return the function signature
233        return hex::encode(&calldata[..SELECTOR_LEN])
234    }
235
236    // return sig as is
237    sig.to_string()
238}
239
240pub fn now() -> Duration {
241    SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn can_convert_sig() {
250        assert_eq!(sig_to_file_name("run()").as_str(), "run");
251        assert_eq!(
252            sig_to_file_name(
253                "522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
254            )
255            .as_str(),
256            "522bb704"
257        );
258    }
259}