Skip to content

Commit 0780d02

Browse files
yash-atreyarplusq
authored andcommitted
refactor(script): mv ScriptSequence to new crate (foundry-rs#9098)
* refac(`script`): extract script sequence and related types to new crate * replace MultiChainSequence in script crate * replace TransactionWithMetadata and AdditionalContract * replace ScriptSequence * replace all underlying ScriptSequence and related types * doc nits * add `ScriptTransactionBuilder` * remove `TxWithMetadata` * mv verify fns and use `ScriptSequence` directly * clippy
1 parent eff7128 commit 0780d02

File tree

16 files changed

+629
-545
lines changed

16 files changed

+629
-545
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ members = [
2020
"crates/evm/traces/",
2121
"crates/fmt/",
2222
"crates/forge/",
23+
"crates/script-sequence/",
2324
"crates/macros/",
2425
"crates/test-utils/",
2526
]
@@ -147,6 +148,7 @@ forge-fmt = { path = "crates/fmt" }
147148
forge-verify = { path = "crates/verify" }
148149
forge-script = { path = "crates/script" }
149150
forge-sol-macro-gen = { path = "crates/sol-macro-gen" }
151+
forge-script-sequence = { path = "crates/script-sequence" }
150152
foundry-cheatcodes = { path = "crates/cheatcodes" }
151153
foundry-cheatcodes-spec = { path = "crates/cheatcodes/spec" }
152154
foundry-cli = { path = "crates/cli" }

crates/script-sequence/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "forge-script-sequence"
3+
description = "Script sequence types"
4+
5+
version.workspace = true
6+
edition.workspace = true
7+
rust-version.workspace = true
8+
authors.workspace = true
9+
license.workspace = true
10+
homepage.workspace = true
11+
repository.workspace = true
12+
13+
[lints]
14+
workspace = true
15+
16+
[dependencies]
17+
foundry-config.workspace = true
18+
foundry-common.workspace = true
19+
foundry-compilers = { workspace = true, features = ["full"] }
20+
21+
serde.workspace = true
22+
eyre.workspace = true
23+
serde_json.workspace = true
24+
tracing.workspace = true
25+
26+
revm-inspectors.workspace = true
27+
28+
alloy-rpc-types.workspace = true
29+
alloy-primitives.workspace = true

crates/script-sequence/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//! Script Sequence and related types.
2+
3+
pub mod sequence;
4+
pub mod transaction;
5+
6+
pub use sequence::*;
7+
pub use transaction::*;
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
use crate::transaction::TransactionWithMetadata;
2+
use alloy_primitives::{hex, map::HashMap, TxHash};
3+
use alloy_rpc_types::AnyTransactionReceipt;
4+
use eyre::{ContextCompat, Result, WrapErr};
5+
use foundry_common::{fs, shell, TransactionMaybeSigned, SELECTOR_LEN};
6+
use foundry_compilers::ArtifactId;
7+
use foundry_config::Config;
8+
use serde::{Deserialize, Serialize};
9+
use std::{
10+
collections::VecDeque,
11+
io::{BufWriter, Write},
12+
path::PathBuf,
13+
time::{Duration, SystemTime, UNIX_EPOCH},
14+
};
15+
16+
pub const DRY_RUN_DIR: &str = "dry-run";
17+
18+
#[derive(Clone, Serialize, Deserialize)]
19+
pub 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)]
27+
pub 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)]
44+
pub 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)]
50+
pub struct SensitiveScriptSequence {
51+
pub transactions: VecDeque<SensitiveTransactionMetadata>,
52+
}
53+
54+
impl 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+
66+
impl 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+
shell::println(format!("\nTransactions saved to: {}\n", path.display()))?;
131+
shell::println(format!("Sensitive values saved to: {}\n", sensitive_path.display()))?;
132+
}
133+
134+
Ok(())
135+
}
136+
137+
pub fn add_receipt(&mut self, receipt: AnyTransactionReceipt) {
138+
self.receipts.push(receipt);
139+
}
140+
141+
/// Sorts all receipts with ascending transaction index
142+
pub fn sort_receipts(&mut self) {
143+
self.receipts.sort_by_key(|r| (r.block_number, r.transaction_index));
144+
}
145+
146+
pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) {
147+
if !self.pending.contains(&tx_hash) {
148+
self.transactions[index].hash = Some(tx_hash);
149+
self.pending.push(tx_hash);
150+
}
151+
}
152+
153+
pub fn remove_pending(&mut self, tx_hash: TxHash) {
154+
self.pending.retain(|element| element != &tx_hash);
155+
}
156+
157+
/// Gets paths in the formats
158+
/// `./broadcast/[contract_filename]/[chain_id]/[sig]-[timestamp].json` and
159+
/// `./cache/[contract_filename]/[chain_id]/[sig]-[timestamp].json`.
160+
pub fn get_paths(
161+
config: &Config,
162+
sig: &str,
163+
target: &ArtifactId,
164+
chain_id: u64,
165+
dry_run: bool,
166+
) -> Result<(PathBuf, PathBuf)> {
167+
let mut broadcast = config.broadcast.to_path_buf();
168+
let mut cache = config.cache_path.to_path_buf();
169+
let mut common = PathBuf::new();
170+
171+
let target_fname = target.source.file_name().wrap_err("No filename.")?;
172+
common.push(target_fname);
173+
common.push(chain_id.to_string());
174+
if dry_run {
175+
common.push(DRY_RUN_DIR);
176+
}
177+
178+
broadcast.push(common.clone());
179+
cache.push(common);
180+
181+
fs::create_dir_all(&broadcast)?;
182+
fs::create_dir_all(&cache)?;
183+
184+
// TODO: ideally we want the name of the function here if sig is calldata
185+
let filename = sig_to_file_name(sig);
186+
187+
broadcast.push(format!("{filename}-latest.json"));
188+
cache.push(format!("{filename}-latest.json"));
189+
190+
Ok((broadcast, cache))
191+
}
192+
193+
/// Returns the first RPC URL of this sequence.
194+
pub fn rpc_url(&self) -> &str {
195+
self.transactions.front().expect("empty sequence").rpc.as_str()
196+
}
197+
198+
/// Returns the list of the transactions without the metadata.
199+
pub fn transactions(&self) -> impl Iterator<Item = &TransactionMaybeSigned> {
200+
self.transactions.iter().map(|tx| tx.tx())
201+
}
202+
203+
pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) {
204+
self.transactions
205+
.iter_mut()
206+
.enumerate()
207+
.for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc));
208+
}
209+
}
210+
211+
/// Converts the `sig` argument into the corresponding file path.
212+
///
213+
/// This accepts either the signature of the function or the raw calldata.
214+
pub fn sig_to_file_name(sig: &str) -> String {
215+
if let Some((name, _)) = sig.split_once('(') {
216+
// strip until call argument parenthesis
217+
return name.to_string()
218+
}
219+
// assume calldata if `sig` is hex
220+
if let Ok(calldata) = hex::decode(sig) {
221+
// in which case we return the function signature
222+
return hex::encode(&calldata[..SELECTOR_LEN])
223+
}
224+
225+
// return sig as is
226+
sig.to_string()
227+
}
228+
229+
pub fn now() -> Duration {
230+
SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
231+
}
232+
233+
#[cfg(test)]
234+
mod tests {
235+
use super::*;
236+
237+
#[test]
238+
fn can_convert_sig() {
239+
assert_eq!(sig_to_file_name("run()").as_str(), "run");
240+
assert_eq!(
241+
sig_to_file_name(
242+
"522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
243+
)
244+
.as_str(),
245+
"522bb704"
246+
);
247+
}
248+
}

0 commit comments

Comments
 (0)