forge_script_sequence/
sequence.rsuse crate::transaction::TransactionWithMetadata;
use alloy_primitives::{hex, map::HashMap, TxHash};
use alloy_rpc_types::AnyTransactionReceipt;
use eyre::{ContextCompat, Result, WrapErr};
use foundry_common::{fs, TransactionMaybeSigned, SELECTOR_LEN};
use foundry_compilers::ArtifactId;
use foundry_config::Config;
use serde::{Deserialize, Serialize};
use std::{
collections::VecDeque,
io::{BufWriter, Write},
path::PathBuf,
time::{Duration, SystemTime, UNIX_EPOCH},
};
pub const DRY_RUN_DIR: &str = "dry-run";
#[derive(Clone, Serialize, Deserialize)]
pub struct NestedValue {
pub internal_type: String,
pub value: String,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct ScriptSequence {
pub transactions: VecDeque<TransactionWithMetadata>,
pub receipts: Vec<AnyTransactionReceipt>,
pub libraries: Vec<String>,
pub pending: Vec<TxHash>,
#[serde(skip)]
pub paths: Option<(PathBuf, PathBuf)>,
pub returns: HashMap<String, NestedValue>,
pub timestamp: u64,
pub chain: u64,
pub commit: Option<String>,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct SensitiveTransactionMetadata {
pub rpc: String,
}
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct SensitiveScriptSequence {
pub transactions: VecDeque<SensitiveTransactionMetadata>,
}
impl From<ScriptSequence> for SensitiveScriptSequence {
fn from(sequence: ScriptSequence) -> Self {
Self {
transactions: sequence
.transactions
.iter()
.map(|tx| SensitiveTransactionMetadata { rpc: tx.rpc.clone() })
.collect(),
}
}
}
impl ScriptSequence {
pub fn load(
config: &Config,
sig: &str,
target: &ArtifactId,
chain_id: u64,
dry_run: bool,
) -> Result<Self> {
let (path, sensitive_path) = Self::get_paths(config, sig, target, chain_id, dry_run)?;
let mut script_sequence: Self = fs::read_json_file(&path)
.wrap_err(format!("Deployment not found for chain `{chain_id}`."))?;
let sensitive_script_sequence: SensitiveScriptSequence = fs::read_json_file(
&sensitive_path,
)
.wrap_err(format!("Deployment's sensitive details not found for chain `{chain_id}`."))?;
script_sequence.fill_sensitive(&sensitive_script_sequence);
script_sequence.paths = Some((path, sensitive_path));
Ok(script_sequence)
}
pub fn save(&mut self, silent: bool, save_ts: bool) -> Result<()> {
self.sort_receipts();
if self.transactions.is_empty() {
return Ok(())
}
let Some((path, sensitive_path)) = self.paths.clone() else { return Ok(()) };
self.timestamp = now().as_secs();
let ts_name = format!("run-{}.json", self.timestamp);
let sensitive_script_sequence: SensitiveScriptSequence = self.clone().into();
let mut writer = BufWriter::new(fs::create_file(&path)?);
serde_json::to_writer_pretty(&mut writer, &self)?;
writer.flush()?;
if save_ts {
fs::copy(&path, path.with_file_name(&ts_name))?;
}
let mut writer = BufWriter::new(fs::create_file(&sensitive_path)?);
serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?;
writer.flush()?;
if save_ts {
fs::copy(&sensitive_path, sensitive_path.with_file_name(&ts_name))?;
}
if !silent {
sh_println!("\nTransactions saved to: {}\n", path.display())?;
sh_println!("Sensitive values saved to: {}\n", sensitive_path.display())?;
}
Ok(())
}
pub fn add_receipt(&mut self, receipt: AnyTransactionReceipt) {
self.receipts.push(receipt);
}
pub fn sort_receipts(&mut self) {
self.receipts.sort_by_key(|r| (r.block_number, r.transaction_index));
}
pub fn add_pending(&mut self, index: usize, tx_hash: TxHash) {
if !self.pending.contains(&tx_hash) {
self.transactions[index].hash = Some(tx_hash);
self.pending.push(tx_hash);
}
}
pub fn remove_pending(&mut self, tx_hash: TxHash) {
self.pending.retain(|element| element != &tx_hash);
}
pub fn get_paths(
config: &Config,
sig: &str,
target: &ArtifactId,
chain_id: u64,
dry_run: bool,
) -> Result<(PathBuf, PathBuf)> {
let mut broadcast = config.broadcast.to_path_buf();
let mut cache = config.cache_path.to_path_buf();
let mut common = PathBuf::new();
let target_fname = target.source.file_name().wrap_err("No filename.")?;
common.push(target_fname);
common.push(chain_id.to_string());
if dry_run {
common.push(DRY_RUN_DIR);
}
broadcast.push(common.clone());
cache.push(common);
fs::create_dir_all(&broadcast)?;
fs::create_dir_all(&cache)?;
let filename = sig_to_file_name(sig);
broadcast.push(format!("{filename}-latest.json"));
cache.push(format!("{filename}-latest.json"));
Ok((broadcast, cache))
}
pub fn rpc_url(&self) -> &str {
self.transactions.front().expect("empty sequence").rpc.as_str()
}
pub fn transactions(&self) -> impl Iterator<Item = &TransactionMaybeSigned> {
self.transactions.iter().map(|tx| tx.tx())
}
pub fn fill_sensitive(&mut self, sensitive: &SensitiveScriptSequence) {
self.transactions
.iter_mut()
.enumerate()
.for_each(|(i, tx)| tx.rpc.clone_from(&sensitive.transactions[i].rpc));
}
}
pub fn sig_to_file_name(sig: &str) -> String {
if let Some((name, _)) = sig.split_once('(') {
return name.to_string()
}
if let Ok(calldata) = hex::decode(sig) {
return hex::encode(&calldata[..SELECTOR_LEN])
}
sig.to_string()
}
pub fn now() -> Duration {
SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_convert_sig() {
assert_eq!(sig_to_file_name("run()").as_str(), "run");
assert_eq!(
sig_to_file_name(
"522bb704000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfFFb92266"
)
.as_str(),
"522bb704"
);
}
}