Skip to content

Commit

Permalink
feat(cast): add artifact method
Browse files Browse the repository at this point in the history
  • Loading branch information
pawurb committed Nov 1, 2024
1 parent 56639cd commit 83559eb
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 2 deletions.
6 changes: 5 additions & 1 deletion crates/cast/bin/args.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::cmd::{
access_list::AccessListArgs, bind::BindArgs, call::CallArgs,
access_list::AccessListArgs, artifact::ArtifactArgs, bind::BindArgs, call::CallArgs,
constructor_args::ConstructorArgsArgs, create2::Create2Args, creation_code::CreationCodeArgs,
estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs,
mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs,
Expand Down Expand Up @@ -955,6 +955,10 @@ pub enum CastSubcommand {
#[command(visible_alias = "cc")]
CreationCode(CreationCodeArgs),

/// Generate an artifact file, that can be used to deploy a contract locally.
#[command(visible_alias = "ar")]
Artifact(ArtifactArgs),

/// Display constructor arguments used for the contract initialization.
#[command(visible_alias = "cra")]
ConstructorArgs(ConstructorArgsArgs),
Expand Down
93 changes: 93 additions & 0 deletions crates/cast/bin/cmd/artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use alloy_primitives::Address;
use clap::{command, Parser};
use eyre::Result;
use foundry_block_explorers::Client;
use foundry_cli::{
opts::{EtherscanOpts, RpcOpts},
utils,
};
use foundry_common::fs;
use foundry_config::Config;
use serde_json::json;
use std::path::PathBuf;

use super::{
creation_code::{fetch_creation_code, parse_code_output},
interface::{fetch_abi_from_etherscan, load_abi_from_file},
};

/// CLI arguments for `cast artifact`.
#[derive(Parser)]
pub struct ArtifactArgs {
/// An Ethereum address, for which the artifact will be produced.
contract: Address,

/// Path to file containing the contract's JSON ABI. It's necessary if the target contract is
/// not verified on Etherscan.
#[arg(long)]
abi_path: Option<String>,

/// The path to the output file.
///
/// If not specified, the artifact will be output to stdout.
#[arg(
short,
long,
value_hint = clap::ValueHint::FilePath,
value_name = "PATH",
)]
output: Option<PathBuf>,

#[command(flatten)]
etherscan: EtherscanOpts,

#[command(flatten)]
rpc: RpcOpts,
}

impl ArtifactArgs {
pub async fn run(self) -> Result<()> {
let Self { contract, etherscan, rpc, output: output_location, abi_path } = self;

let config = Config::from(&etherscan);
let chain = config.chain.unwrap_or_default();
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
let client = Client::new(chain, api_key)?;

let config = Config::from(&rpc);
let provider = utils::get_provider(&config)?;

let abi = if let Some(abi_path) = abi_path.clone() {
load_abi_from_file(&abi_path, None)?
} else {
fetch_abi_from_etherscan(contract, &etherscan).await?
};

let (abi, _) = abi.first().ok_or_else(|| eyre::eyre!("No ABI found"))?;

let bytecode = fetch_creation_code(contract, client, provider).await?;
let bytecode =
parse_code_output(bytecode, contract, &etherscan, abi_path, true, false).await?;

let artifact = json!({
"abi": abi,
"bytecode": {
"object": bytecode
}
});

let artifact = serde_json::to_string_pretty(&artifact)?;

if let Some(loc) = output_location {
if let Some(parent) = loc.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&loc, artifact)?;
sh_println!("Saved artifact at {}", loc.display())?;
} else {
sh_println!("{artifact}")?;
}

Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/cast/bin/cmd/creation_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl CreationCodeArgs {
/// - The complete bytecode
/// - The bytecode without constructor arguments
/// - Only the constructor arguments
async fn parse_code_output(
pub async fn parse_code_output(
bytecode: Bytes,
contract: Address,
etherscan: &EtherscanOpts,
Expand Down
1 change: 1 addition & 0 deletions crates/cast/bin/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! [`foundry_config::Config`].
pub mod access_list;
pub mod artifact;
pub mod bind;
pub mod call;
pub mod constructor_args;
Expand Down
1 change: 1 addition & 0 deletions crates/cast/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ async fn main_args(args: CastArgs) -> Result<()> {
CastSubcommand::Interface(cmd) => cmd.run().await?,
CastSubcommand::CreationCode(cmd) => cmd.run().await?,
CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?,
CastSubcommand::Artifact(cmd) => cmd.run().await?,
CastSubcommand::Bind(cmd) => cmd.run().await?,
CastSubcommand::PrettyCalldata { calldata, offline } => {
let calldata = stdin::unwrap_line(calldata)?;
Expand Down
23 changes: 23 additions & 0 deletions crates/cast/tests/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1500,3 +1500,26 @@ casttest!(fetch_constructor_args_from_etherscan, |_prj, cmd| {
"#]]);
});

// tests that displays a sample contract artifact
// <https://etherscan.io/address/0x0923cad07f06b2d0e5e49e63b8b35738d4156b95>
casttest!(fetch_artifact_from_etherscan, |_prj, cmd| {
let eth_rpc_url = next_http_rpc_endpoint();
cmd.args([
"artifact",
"--etherscan-api-key",
&next_mainnet_etherscan_api_key(),
"0x0923cad07f06b2d0e5e49e63b8b35738d4156b95",
"--rpc-url",
eth_rpc_url.as_str(),
])
.assert_success()
.stdout_eq(str![[r#"{
"abi": [],
"bytecode": {
"object": "0x60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea264697066735822122074c61e8e4eefd410ca92eec26e8112ec6e831d0a4bf35718fdd78b45d68220d064736f6c63430008070033"
}
}
"#]]);
});

0 comments on commit 83559eb

Please sign in to comment.