Skip to main content

forge_script/
multi_sequence.rs

1use alloy_network::Network;
2use eyre::{ContextCompat, Result, WrapErr};
3use forge_script_sequence::{
4    DRY_RUN_DIR, ScriptSequence, SensitiveScriptSequence, now, sig_to_file_name,
5};
6use foundry_common::{fs, shell};
7use foundry_compilers::ArtifactId;
8use foundry_config::Config;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12/// Holds the sequences of multiple chain deployments.
13#[derive(Clone, Default, Serialize, Deserialize)]
14#[serde(bound(
15    serialize = "N::TransactionRequest: Serialize, N::TxEnvelope: Serialize",
16    deserialize = "N::TransactionRequest: for<'de2> Deserialize<'de2>, N::TxEnvelope: for<'de2> Deserialize<'de2>"
17))]
18pub struct MultiChainSequence<N: Network> {
19    pub deployments: Vec<ScriptSequence<N>>,
20    #[serde(skip)]
21    pub path: PathBuf,
22    #[serde(skip)]
23    pub sensitive_path: PathBuf,
24    pub timestamp: u128,
25}
26
27/// Sensitive values from script sequences.
28#[derive(Clone, Default, Serialize, Deserialize)]
29pub struct SensitiveMultiChainSequence {
30    pub deployments: Vec<SensitiveScriptSequence>,
31}
32
33impl SensitiveMultiChainSequence {
34    fn from_multi_sequence<N: Network>(sequence: &MultiChainSequence<N>) -> Self {
35        Self {
36            deployments: sequence.deployments.iter().map(SensitiveScriptSequence::from).collect(),
37        }
38    }
39}
40
41impl<N: Network> MultiChainSequence<N> {
42    pub fn new(
43        deployments: Vec<ScriptSequence<N>>,
44        sig: &str,
45        target: &ArtifactId,
46        config: &Config,
47        dry_run: bool,
48    ) -> Result<Self> {
49        let (path, sensitive_path) = Self::get_paths(config, sig, target, dry_run)?;
50
51        Ok(Self { deployments, path, sensitive_path, timestamp: now().as_millis() })
52    }
53
54    /// Gets paths in the formats
55    /// ./broadcast/multi/contract_filename[-timestamp]/sig.json and
56    /// ./cache/multi/contract_filename[-timestamp]/sig.json
57    pub fn get_paths(
58        config: &Config,
59        sig: &str,
60        target: &ArtifactId,
61        dry_run: bool,
62    ) -> Result<(PathBuf, PathBuf)> {
63        let mut broadcast = config.broadcast.clone();
64        let mut cache = config.cache_path.clone();
65        let mut common = PathBuf::new();
66
67        common.push("multi");
68
69        if dry_run {
70            common.push(DRY_RUN_DIR);
71        }
72
73        let target_fname = target
74            .source
75            .file_name()
76            .wrap_err_with(|| format!("No filename for {:?}", target.source))?
77            .to_string_lossy();
78
79        common.push(format!("{target_fname}-latest"));
80
81        broadcast.push(common.clone());
82        cache.push(common);
83
84        fs::create_dir_all(&broadcast)?;
85        fs::create_dir_all(&cache)?;
86
87        let filename = format!("{}.json", sig_to_file_name(sig));
88
89        broadcast.push(filename.clone());
90        cache.push(filename);
91
92        Ok((broadcast, cache))
93    }
94
95    /// Loads the sequences for the multi chain deployment.
96    pub fn load(config: &Config, sig: &str, target: &ArtifactId, dry_run: bool) -> Result<Self>
97    where
98        N::TxEnvelope: for<'d> Deserialize<'d>,
99    {
100        let (path, sensitive_path) = Self::get_paths(config, sig, target, dry_run)?;
101        let mut sequence: Self = foundry_compilers::utils::read_json_file(&path)
102            .wrap_err("Multi-chain deployment not found.")?;
103        let sensitive_sequence: SensitiveMultiChainSequence =
104            foundry_compilers::utils::read_json_file(&sensitive_path)
105                .wrap_err("Multi-chain deployment sensitive details not found.")?;
106
107        let deployments_len = sequence.deployments.len();
108        let sensitive_deployments_len = sensitive_sequence.deployments.len();
109        if deployments_len != sensitive_deployments_len {
110            eyre::bail!(
111                "sensitive-cache deployment count ({sensitive_deployments_len}) does not match \
112                 deployment count ({deployments_len}); the multi-chain deployment and its \
113                 sensitive-cache counterpart are out of sync"
114            );
115        }
116        for (i, deployment) in sequence.deployments.iter_mut().enumerate() {
117            deployment.fill_sensitive(&sensitive_sequence.deployments[i])?;
118        }
119
120        sequence.path = path;
121        sequence.sensitive_path = sensitive_path;
122
123        Ok(sequence)
124    }
125
126    /// Saves the transactions as file if it's a standalone deployment.
127    pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()>
128    where
129        N::TxEnvelope: Serialize,
130    {
131        self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts());
132
133        self.timestamp = now().as_millis();
134
135        let sensitive_sequence = SensitiveMultiChainSequence::from_multi_sequence(&*self);
136
137        // broadcast writes
138        //../Contract-latest/run.json
139        fs::write_pretty_json_file(&self.path, self)?;
140
141        if save_ts {
142            //../Contract-[timestamp]/run.json
143            let path = self.path.to_string_lossy();
144            let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp)));
145            fs::create_dir_all(file.parent().unwrap())?;
146            fs::copy(&self.path, &file)?;
147        }
148
149        // cache writes
150        //../Contract-latest/run.json
151        fs::write_sensitive_json_file(&self.sensitive_path, &sensitive_sequence)?;
152
153        if save_ts {
154            //../Contract-[timestamp]/run.json
155            let path = self.sensitive_path.to_string_lossy();
156            let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp)));
157            fs::create_dir_all(file.parent().unwrap())?;
158            fs::copy(&self.sensitive_path, &file)?;
159        }
160
161        if !silent {
162            if shell::is_json() {
163                sh_println!(
164                    "{}",
165                    serde_json::json!({
166                        "status": "success",
167                        "transactions": self.path.display().to_string(),
168                        "sensitive": self.sensitive_path.display().to_string(),
169                    })
170                )?;
171            } else {
172                sh_println!("\nTransactions saved to: {}\n", self.path.display())?;
173                sh_println!("Sensitive details saved to: {}\n", self.sensitive_path.display())?;
174            }
175        }
176
177        Ok(())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use alloy_network::Ethereum;
185
186    #[test]
187    fn load_rejects_mismatched_deployment_counts() {
188        let dir = tempfile::tempdir().unwrap();
189        let config = Config {
190            broadcast: dir.path().join("broadcast"),
191            cache_path: dir.path().join("cache"),
192            ..Default::default()
193        };
194        let target = ArtifactId {
195            path: PathBuf::from("Script.json"),
196            name: "Script".to_string(),
197            source: PathBuf::from("Script.sol"),
198            version: "0.8.30".parse().unwrap(),
199            build_id: String::new(),
200            profile: "default".to_string(),
201        };
202        let (path, sensitive_path) =
203            MultiChainSequence::<Ethereum>::get_paths(&config, "run()", &target, false).unwrap();
204        let sequence = MultiChainSequence::<Ethereum> {
205            deployments: vec![ScriptSequence::default()],
206            path: PathBuf::new(),
207            sensitive_path: PathBuf::new(),
208            timestamp: 0,
209        };
210        fs::write_pretty_json_file(&path, &sequence).unwrap();
211        for count in [0, 2] {
212            let sensitive = SensitiveMultiChainSequence {
213                deployments: vec![SensitiveScriptSequence::default(); count],
214            };
215            fs::write_sensitive_json_file(&sensitive_path, &sensitive).unwrap();
216            let err = MultiChainSequence::<Ethereum>::load(&config, "run()", &target, false)
217                .err()
218                .expect("mismatched counts must fail");
219            assert_eq!(
220                err.to_string(),
221                format!(
222                    "sensitive-cache deployment count ({count}) does not match deployment count (1); \
223                     the multi-chain deployment and its sensitive-cache counterpart are out of sync"
224                )
225            );
226        }
227    }
228}