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#[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#[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 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 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 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 fs::write_pretty_json_file(&self.path, self)?;
140
141 if save_ts {
142 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 fs::write_sensitive_json_file(&self.sensitive_path, &sensitive_sequence)?;
152
153 if save_ts {
154 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}