Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add clone.toml metadata to forge clone #3

Merged
merged 2 commits into from
Mar 28, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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