diff --git a/Cargo.lock b/Cargo.lock index 635edc029..e00aca550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,6 +1652,7 @@ name = "foundry-utils" version = "0.1.0" dependencies = [ "ethers-core", + "ethers-etherscan", "eyre", "hex", "reqwest", diff --git a/cast/src/lib.rs b/cast/src/lib.rs index 08e3359dd..9aebeff9f 100644 --- a/cast/src/lib.rs +++ b/cast/src/lib.rs @@ -13,7 +13,7 @@ use eyre::{Context, Result}; use rustc_hex::{FromHexIter, ToHex}; use std::str::FromStr; -use foundry_utils::{encode_args, get_func, to_table}; +use foundry_utils::{encode_args, get_func, get_func_etherscan, to_table}; // TODO: CastContract with common contract initializers? Same for CastProviders? @@ -115,7 +115,7 @@ where /// /// ```no_run /// use cast::Cast; - /// use ethers_core::types::Address; + /// use ethers_core::types::{Address, Chain}; /// use ethers_providers::{Provider, Http}; /// use std::{str::FromStr, convert::TryFrom}; /// @@ -126,7 +126,7 @@ where /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?; /// let sig = "greet(string)()"; /// let args = vec!["hello".to_owned()]; - /// let data = cast.send(from, to, Some((sig, args))).await?; + /// let data = cast.send(from, to, Some((sig, args)), Chain::Mainnet, None).await?; /// println!("{}", *data); /// # Ok(()) /// # } @@ -136,8 +136,10 @@ where from: F, to: T, args: Option<(&str, Vec)>, + chain: Chain, + etherscan_api_key: Option, ) -> Result> { - let tx = self.build_tx(from, to, args).await?; + let tx = self.build_tx(from, to, args, chain, etherscan_api_key).await?; let res = self.provider.send_transaction(tx, None).await?; Ok::<_, eyre::Error>(res) @@ -147,7 +149,7 @@ where /// /// ```no_run /// use cast::Cast; - /// use ethers_core::types::Address; + /// use ethers_core::types::{Address, Chain}; /// use ethers_providers::{Provider, Http}; /// use std::{str::FromStr, convert::TryFrom}; /// @@ -158,7 +160,7 @@ where /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?; /// let sig = "greet(string)()"; /// let args = vec!["5".to_owned()]; - /// let data = cast.estimate(from, to, Some((sig, args))).await?; + /// let data = cast.estimate(from, to, Some((sig, args)), Chain::Mainnet, None).await?; /// println!("{}", data); /// # Ok(()) /// # } @@ -168,8 +170,10 @@ where from: F, to: T, args: Option<(&str, Vec)>, + chain: Chain, + etherscan_api_key: Option, ) -> Result { - let tx = self.build_tx(from, to, args).await?.into(); + let tx = self.build_tx(from, to, args, chain, etherscan_api_key).await?.into(); let res = self.provider.estimate_gas(&tx).await?; Ok::<_, eyre::Error>(res) @@ -180,17 +184,35 @@ where from: F, to: T, args: Option<(&str, Vec)>, + chain: Chain, + etherscan_api_key: Option, ) -> Result { let from = match from.into() { NameOrAddress::Name(ref ens_name) => self.provider.resolve_name(ens_name).await?, NameOrAddress::Address(addr) => addr, }; + let to = match to.into() { + NameOrAddress::Name(ref ens_name) => self.provider.resolve_name(ens_name).await?, + NameOrAddress::Address(addr) => addr, + }; + // make the call let mut tx = Eip1559TransactionRequest::new().from(from).to(to); if let Some((sig, args)) = args { - let func = get_func(sig)?; + let func = if sig.contains('(') { + get_func(sig)? + } else { + get_func_etherscan( + sig, + to, + args.clone(), + chain, + etherscan_api_key.expect("Must set ETHERSCAN_API_KEY"), + ) + .await? + }; let data = encode_args(&func, &args)?; tx = tx.data(data); } diff --git a/cli/src/cast.rs b/cli/src/cast.rs index e6bdc629c..72b253970 100644 --- a/cli/src/cast.rs +++ b/cli/src/cast.rs @@ -17,7 +17,7 @@ use ethers::{ }, providers::{Middleware, Provider}, signers::{LocalWallet, Signer}, - types::{Address, NameOrAddress, Signature, U256}, + types::{Address, Chain, NameOrAddress, Signature, U256}, }; use rayon::prelude::*; use regex::RegexSet; @@ -157,28 +157,56 @@ async fn main() -> eyre::Result<()> { let provider = Provider::try_from(rpc_url)?; println!("{}", Cast::new(&provider).transaction(hash, field, to_json).await?) } - Subcommands::SendTx { eth, to, sig, cast_async, args } => { + Subcommands::SendTx { eth, to, sig, cast_async, args, chain, etherscan_api_key } => { let provider = Provider::try_from(eth.rpc_url()?)?; let chain_id = Cast::new(&provider).chain_id().await?; if let Some(signer) = eth.signer_with(chain_id, provider.clone()).await? { match signer { WalletType::Ledger(signer) => { - cast_send(&signer, signer.address(), to, sig, args, cast_async).await?; + cast_send( + &signer, + signer.address(), + to, + (sig, args), + chain, + etherscan_api_key, + cast_async, + ) + .await?; } WalletType::Local(signer) => { - cast_send(&signer, signer.address(), to, sig, args, cast_async).await?; + cast_send( + &signer, + signer.address(), + to, + (sig, args), + chain, + etherscan_api_key, + cast_async, + ) + .await?; } WalletType::Trezor(signer) => { - cast_send(&signer, signer.address(), to, sig, args, cast_async).await?; + cast_send( + &signer, + signer.address(), + to, + (sig, args), + chain, + etherscan_api_key, + cast_async, + ) + .await?; } } } else { let from = eth.from.expect("No ETH_FROM or signer specified"); - cast_send(provider, from, to, sig, args, cast_async).await?; + cast_send(provider, from, to, (sig, args), chain, etherscan_api_key, cast_async) + .await?; } } - Subcommands::Estimate { eth, to, sig, args } => { + Subcommands::Estimate { eth, to, sig, args, chain, etherscan_api_key } => { let provider = Provider::try_from(eth.rpc_url()?)?; let cast = Cast::new(&provider); // chain id does not matter here, we're just trying to get the address @@ -191,7 +219,9 @@ async fn main() -> eyre::Result<()> { } else { eth.from.expect("No ETH_FROM or signer specified") }; - let gas = cast.estimate(from, to, Some((sig.as_str(), args))).await?; + let gas = cast + .estimate(from, to, Some((sig.as_str(), args)), chain, etherscan_api_key) + .await?; println!("{}", gas); } Subcommands::CalldataDecode { sig, calldata } => { @@ -441,16 +471,20 @@ async fn cast_send, T: Into provider: M, from: F, to: T, - sig: String, - args: Vec, + args: (String, Vec), + chain: Chain, + etherscan_api_key: Option, cast_async: bool, ) -> eyre::Result<()> where M::Error: 'static, { let cast = Cast::new(provider); - let pending_tx = - cast.send(from, to, if !sig.is_empty() { Some((&sig, args)) } else { None }).await?; + + let sig = args.0; + let params = args.1; + let params = if !sig.is_empty() { Some((&sig[..], params)) } else { None }; + let pending_tx = cast.send(from, to, params, chain, etherscan_api_key).await?; let tx_hash = *pending_tx; if cast_async { diff --git a/cli/src/opts/cast.rs b/cli/src/opts/cast.rs index bd17a77ce..9958e85b7 100644 --- a/cli/src/opts/cast.rs +++ b/cli/src/opts/cast.rs @@ -1,6 +1,6 @@ use std::str::FromStr; -use ethers::types::{Address, BlockId, BlockNumber, NameOrAddress, H256}; +use ethers::types::{Address, BlockId, BlockNumber, Chain, NameOrAddress, H256}; use structopt::StructOpt; use super::{EthereumOpts, Wallet}; @@ -131,12 +131,16 @@ pub enum Subcommands { SendTx { #[structopt(help = "the address you want to transact with", parse(try_from_str = parse_name_or_address))] to: NameOrAddress, - #[structopt(help = "the function signature you want to call")] + #[structopt(help = "the function signature or name you want to call")] sig: String, #[structopt(help = "the list of arguments you want to call the function with")] args: Vec, #[structopt(long, env = "CAST_ASYNC")] cast_async: bool, + #[structopt(long, env = "ETHERSCAN_API_KEY")] + etherscan_api_key: Option, + #[structopt(long, env = "CHAIN", default_value = "mainnet")] + chain: Chain, #[structopt(flatten)] eth: EthereumOpts, }, @@ -145,10 +149,14 @@ pub enum Subcommands { Estimate { #[structopt(help = "the address you want to transact with", parse(try_from_str = parse_name_or_address))] to: NameOrAddress, - #[structopt(help = "the function signature you want to call")] + #[structopt(help = "the function signature or name you want to call")] sig: String, #[structopt(help = "the list of arguments you want to call the function with")] args: Vec, + #[structopt(long, env = "ETHERSCAN_API_KEY")] + etherscan_api_key: Option, + #[structopt(long, env = "CHAIN", default_value = "mainnet")] + chain: Chain, #[structopt(flatten)] eth: EthereumOpts, }, diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 50012cd40..0fa8f0d5c 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" [dependencies] # ethers = "0.5" ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } +ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false } eyre = { version = "0.6.5", default-features = false } hex = "0.4.3" diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 321a7d328..c7a804fef 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -7,6 +7,7 @@ use ethers_core::{ }, types::*, }; +use ethers_etherscan::Client; use eyre::{Result, WrapErr}; use serde::Deserialize; @@ -175,6 +176,27 @@ pub fn get_func(sig: &str) -> Result { Ok(func.clone()) } +pub async fn get_func_etherscan( + function_name: &str, + contract: Address, + args: Vec, + chain: Chain, + etherscan_api_key: String, +) -> Result { + let client = Client::new(chain, etherscan_api_key)?; + let abi = client.contract_abi(contract).await?; + let funcs = abi.functions.get(function_name).unwrap(); + + for func in funcs { + let res = encode_args(func, &args); + if res.is_ok() { + return Ok(func.clone()) + } + } + + Err(eyre::eyre!("Function not found")) +} + /// Parses string input as Token against the expected ParamType pub fn parse_tokens<'a, I: IntoIterator>( params: I,