Skip to content

Commit

Permalink
Merge pull request #3 from EtherDebug/forge_clone
Browse files Browse the repository at this point in the history
Add `clone.toml` metadata to forge clone
  • Loading branch information
Troublor authored Mar 28, 2024
2 parents 1fd2b18 + 58a2758 commit d43e20c
Showing 1 changed file with 90 additions and 17 deletions.
107 changes: 90 additions & 17 deletions crates/forge/bin/cmd/clone.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
use std::time::Duration;
use std::{fs::read_dir, path::PathBuf};

use alloy_primitives::Address;
use alloy_primitives::{Address, ChainId, TxHash};
use clap::{Parser, ValueHint};
use eyre::Result;
use foundry_block_explorers::{contract::Metadata, Client};
use foundry_cli::opts::EtherscanOpts;
use foundry_cli::utils::Git;
use foundry_common::compile::ProjectCompiler;
use foundry_common::fs;
use foundry_compilers::artifacts::Settings;
use foundry_compilers::remappings::{RelativeRemapping, Remapping};
use foundry_compilers::ProjectPathsConfig;
use foundry_compilers::{ProjectCompileOutput, ProjectPathsConfig};
use foundry_config::Config;
use toml_edit;

use super::init::InitArgs;
use super::install::DependencyInstallOpts;

/// CLI arguments for `forge clone`.
#[derive(Clone, Debug, Parser)]
Expand All @@ -25,13 +28,36 @@ pub struct CloneArgs {
#[arg(value_hint = ValueHint::DirPath, default_value = ".", value_name = "PATH")]
root: PathBuf,

/// Enable git for the cloned project, default is false.
#[arg(long)]
pub enable_git: bool,

#[command(flatten)]
etherscan: EtherscanOpts,
}

/// CloneMetadata stores the metadata that are not included by `foundry.toml` but necessary for a cloned contract.
/// The metadata can be serialized to the `clone.toml` file in the cloned project root.
#[derive(Debug, Clone, serde::Serialize)]
pub struct CloneMetadata {
/// The path to the source file that contains the contract declaration.
/// The path is relative to the root directory of the project.
pub path: PathBuf,
/// The name of the contract in the file.
pub target_contract: String,
/// The address of the contract on the blockchian.
pub address: Address,
/// The chain id.
pub chain_id: ChainId,
/// The transaction hash of the creation transaction.
pub creation_transaction: TxHash,
/// The address of the deployer (caller of the CREATE/CREATE2).
pub deployer: Address,
}

impl CloneArgs {
pub async fn run(self) -> Result<()> {
let CloneArgs { address, root, etherscan } = self;
let CloneArgs { address, root, enable_git, etherscan } = self;

// parse the contract address
let contract_address: Address = address.parse()?;
Expand All @@ -40,6 +66,13 @@ impl CloneArgs {
let config = Config::from(&etherscan);
let chain = config.chain.unwrap_or_default();
let etherscan_api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
let etherscan_call_interval = if etherscan_api_key.is_empty() {
// if the etherscan api key is not set, we need to wait for 1 seconds between calls
Duration::from_secs(5)
} else {
// if the etherscan api key is set, we can call etherscan more frequently
Duration::from_secs(1)
};

// get the contract code
let client = Client::new(chain, etherscan_api_key)?;
Expand All @@ -53,7 +86,8 @@ impl CloneArgs {
}

// let's try to init the project with default init args
let init_args = InitArgs { root: root.clone(), ..Default::default() };
let opts = DependencyInstallOpts { no_git: !enable_git, ..Default::default() };
let init_args = InitArgs { root: root.clone(), opts, ..Default::default() };
init_args.run().map_err(|e| eyre::eyre!("Project init error: {:?}", e))?;

// canonicalize the root path
Expand Down Expand Up @@ -85,10 +119,31 @@ impl CloneArgs {
update_config_by_metadata(config, doc, &meta).is_ok()
})?;

// Git add and commit the changes
let git = Git::new(&root).quiet(true);
git.add(Some("--all"))?;
git.commit("chore: forge clone")?;
// compile the cloned contract
let compile_output = compile_project(&root)?;
let main_file = find_main_file(&compile_output, &meta.contract_name)?;
let main_file = main_file.strip_prefix(&root)?.to_path_buf();

// dump the metadata to the root directory
std::thread::sleep(etherscan_call_interval);
let creation_tx = client.contract_creation_data(contract_address).await?;
let clone_meta = CloneMetadata {
path: main_file,
target_contract: meta.contract_name,
address: contract_address,
chain_id: etherscan.chain.unwrap_or_default().id(),
creation_transaction: creation_tx.transaction_hash,
deployer: creation_tx.contract_creator,
};
let metadata_content = toml::to_string(&clone_meta)?;
fs::write(root.join("clone.toml"), metadata_content)?;

// Git add and commit the changes if enabled
if enable_git {
let git = Git::new(&root).quiet(true);
git.add(Some("--all"))?;
git.commit("chore: forge clone")?;
}

Ok(())
}
Expand Down Expand Up @@ -285,14 +340,34 @@ fn dump_sources(meta: &Metadata, root: &PathBuf) -> Result<Vec<RelativeRemapping
Ok(remappings.into_iter().map(|r| r.into_relative(&root)).collect())
}

/// Compile the project in the root directory, and return the compilation result.
pub fn compile_project(root: &PathBuf) -> Result<ProjectCompileOutput> {
std::env::set_current_dir(root)?;
let config = Config::load();
let project = config.project()?;
let compiler = ProjectCompiler::new();
compiler.compile(&project)
}

/// Find the file path that contains the contract with the specified name.
/// The returned path is absolute path.
pub fn find_main_file(compile_output: &ProjectCompileOutput, contract: &str) -> Result<PathBuf> {
for (f, c, _) in compile_output.artifacts_with_files() {
if contract == c {
return Ok(PathBuf::from(f));
}
}
Err(eyre::eyre!("contract not found"))
}

#[cfg(test)]
mod tests {
use std::{path::PathBuf, thread::sleep, time::Duration};

use crate::cmd::clone::compile_project;

use super::CloneArgs;
use foundry_common::compile::ProjectCompiler;
use foundry_compilers::{Artifact, ProjectCompileOutput};
use foundry_config::Config;
use hex::ToHex;
use serial_test::serial;
use tempfile;
Expand All @@ -303,13 +378,7 @@ mod tests {
sleep(Duration::from_secs(5));

println!("project_root: {:#?}", root);

// change directory to the root
std::env::set_current_dir(root).unwrap();
let config = Config::load();
let project = config.project().unwrap();
let compiler = ProjectCompiler::new();
compiler.compile(&project).expect("compilation failure")
compile_project(root).expect("compilation failure")
}

fn assert_compilation_result(
Expand Down Expand Up @@ -340,6 +409,7 @@ mod tests {
address: "0x35Fb958109b70799a8f9Bc2a8b1Ee4cC62034193".to_string(),
root: project_root.clone(),
etherscan: Default::default(),
enable_git: false,
};
let (contract_name, stripped_creation_code) =
pick_creation_info(&args.address).expect("creation code not found");
Expand All @@ -356,6 +426,7 @@ mod tests {
address: "0x8B3D32cf2bb4d0D16656f4c0b04Fa546274f1545".to_string(),
root: project_root.clone(),
etherscan: Default::default(),
enable_git: false,
};
let (contract_name, stripped_creation_code) =
pick_creation_info(&args.address).expect("creation code not found");
Expand All @@ -372,6 +443,7 @@ mod tests {
address: "0xDb53f47aC61FE54F456A4eb3E09832D08Dd7BEec".to_string(),
root: project_root.clone(),
etherscan: Default::default(),
enable_git: false,
};
let (contract_name, stripped_creation_code) =
pick_creation_info(&args.address).expect("creation code not found");
Expand All @@ -388,6 +460,7 @@ mod tests {
address: "0x71356E37e0368Bd10bFDbF41dC052fE5FA24cD05".to_string(),
root: project_root.clone(),
etherscan: Default::default(),
enable_git: false,
};
let (contract_name, stripped_creation_code) =
pick_creation_info(&args.address).expect("creation code not found");
Expand Down

0 comments on commit d43e20c

Please sign in to comment.