feat: use ethers_solc::Solc instead of ethers_core::utils::Solc (#546)

* feat(ethers-solc): deserialize bytecode as bytes

* feat(ethers-solc): add method to fetch compact contract

* feat(ethers-solc): use Abi type instead of Vec<serde_json::Value>

* test(contract): use new Solc bindings

* test(middleware): use new Solc bindings

* chore: remove solc from ethers-core

* chore: remove concurrent setup code from ethers-core

* feat(ethers-solc): add ArtifactOutput::Nothing as a no-op artifact logger

* feat: add ethers-solc to top level crate

* examples: use new solc building pattern

* chore(solc): re-use opt str impl for error code decoding

* fix abigen example

* chore: fix doctests

* chore: remove setup feature

* fix: decode string to bytes correctly

* chore: clippy lints
This commit is contained in:
Georgios Konstantopoulos 2021-10-31 13:34:51 +02:00 committed by GitHub
parent 4123823383
commit f0dea75219
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 155 additions and 784 deletions

12
Cargo.lock generated
View File

@ -857,6 +857,7 @@ dependencies = [
"ethers-middleware",
"ethers-providers",
"ethers-signers",
"ethers-solc",
"hex",
"rand 0.8.4",
"serde",
@ -875,6 +876,7 @@ dependencies = [
"ethers-middleware",
"ethers-providers",
"ethers-signers",
"ethers-solc",
"futures-util",
"hex",
"once_cell",
@ -932,7 +934,6 @@ dependencies = [
"ethabi",
"futures-util",
"generic-array 0.14.4",
"glob",
"hex",
"hex-literal",
"k256",
@ -942,7 +943,6 @@ dependencies = [
"rand 0.8.4",
"rlp",
"rlp-derive",
"semver 1.0.4",
"serde",
"serde_json",
"syn",
@ -988,6 +988,7 @@ dependencies = [
"ethers-core",
"ethers-providers",
"ethers-signers",
"ethers-solc",
"futures-util",
"hex",
"instant",
@ -1074,6 +1075,7 @@ name = "ethers-solc"
version = "0.1.0"
dependencies = [
"colored",
"ethers-core",
"futures-util",
"hex",
"md-5",
@ -1308,12 +1310,6 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "group"
version = "0.10.0"

View File

@ -61,7 +61,6 @@ legacy = [
# individual features per sub-crate
## core
setup = ["ethers-core/setup"]
eip712 = ["ethers-contract/eip712", "ethers-core/eip712"]
## providers
ws = ["ethers-providers/ws"]
@ -77,10 +76,11 @@ abigen = ["ethers-contract/abigen"]
[dependencies]
ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract" }
ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-core", features = ["setup"] }
ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-core" }
ethers-providers = { version = "^0.5.0", default-features = false, path = "./ethers-providers" }
ethers-signers = { version = "^0.5.0", default-features = false, path = "./ethers-signers" }
ethers-middleware = { version = "^0.5.0", default-features = false, path = "./ethers-middleware" }
ethers-solc = { version = "^0.1.0", default-features = false, path = "./ethers-solc" }
[dev-dependencies]
ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract", features = ["abigen", "eip712"] }

View File

@ -32,6 +32,7 @@ ethers-contract-abigen = { version = "^0.5.0", path = "ethers-contract-abigen" }
ethers-contract-derive = { version = "^0.5.0", path = "ethers-contract-derive" }
ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false, features = ["eip712"]}
ethers-derive-eip712 = { version = "0.1.0", path = "../ethers-core/ethers-derive-eip712"}
ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.5", default-features = false, features = ["macros"] }

View File

@ -67,7 +67,6 @@ use std::{fmt::Debug, marker::PhantomData, sync::Arc};
/// ```no_run
/// use ethers_core::{
/// abi::Abi,
/// utils::Solc,
/// types::{Address, H256},
/// };
/// use ethers_contract::Contract;

View File

@ -90,7 +90,7 @@ impl<M: Middleware> Deployer<M> {
/// # Example
///
/// ```no_run
/// use ethers_core::utils::Solc;
/// use ethers_solc::Solc;
/// use ethers_contract::ContractFactory;
/// use ethers_providers::{Provider, Http};
/// use ethers_signers::Wallet;
@ -99,9 +99,9 @@ impl<M: Middleware> Deployer<M> {
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// // first we'll compile the contract (you can alternatively compile it yourself
/// // and pass the ABI/Bytecode
/// let compiled = Solc::new("./tests/contract.sol").build().unwrap();
/// let compiled = Solc::default().compile_source("./tests/contract.sol").unwrap();
/// let contract = compiled
/// .get("SimpleStorage")
/// .get("./tests/contract.sol", "SimpleStorage")
/// .expect("could not find contract");
///
/// // connect to the network
@ -109,7 +109,7 @@ impl<M: Middleware> Deployer<M> {
/// let client = std::sync::Arc::new(client);
///
/// // create a factory which will be used to deploy instances of the contract
/// let factory = ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), client);
/// let factory = ContractFactory::new(contract.abi.unwrap().clone(), contract.bin.unwrap().clone(), client);
///
/// // The deployer created by the `deploy` call exposes a builder which gets consumed
/// // by the async `send` call

View File

@ -4,9 +4,9 @@ use ethers_contract::{abigen, EthEvent};
use ethers_core::{
abi::{AbiDecode, AbiEncode, Address, Tokenizable},
types::{transaction::eip2718::TypedTransaction, Eip1559TransactionRequest, U256},
utils::Solc,
};
use ethers_providers::Provider;
use ethers_solc::Solc;
use std::{convert::TryFrom, sync::Arc};
#[test]
@ -287,11 +287,13 @@ async fn can_handle_underscore_functions() {
.interval(std::time::Duration::from_millis(10));
let client = Arc::new(provider);
let compiled = Solc::new("./tests/solidity-contracts/SimpleStorage.sol").build().unwrap();
let compiled = compiled.get("SimpleStorage").unwrap();
let contract = "SimpleStorage";
let path = "./tests/solidity-contracts/SimpleStorage.sol";
let compiled = Solc::default().compile_source(path).unwrap();
let compiled = compiled.get(path, contract).unwrap();
let factory = ethers_contract::ContractFactory::new(
compiled.abi.clone(),
compiled.bytecode.clone(),
compiled.abi.unwrap().clone(),
compiled.bin.unwrap().clone(),
client.clone(),
);
let addr = factory.deploy("hi".to_string()).unwrap().legacy().send().await.unwrap().address();

View File

@ -10,12 +10,9 @@ use ethers_contract::EthEvent;
mod derive;
use ethers_contract::{Contract, ContractFactory};
use ethers_core::{
abi::Abi,
types::Bytes,
utils::{GanacheInstance, Solc},
};
use ethers_core::{abi::Abi, types::Bytes, utils::GanacheInstance};
use ethers_providers::{Http, Middleware, Provider};
use ethers_solc::Solc;
use std::{convert::TryFrom, sync::Arc, time::Duration};
// Note: The `EthEvent` derive macro implements the necessary conversion between `Tokens` and
@ -33,9 +30,10 @@ pub struct ValueChanged {
/// compiles the given contract and returns the ABI and Bytecode
pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) {
let compiled = Solc::new(&format!("./tests/solidity-contracts/{}", filename)).build().unwrap();
let contract = compiled.get(name).expect("could not find contract");
(contract.abi.clone(), contract.bytecode.clone())
let path = format!("./tests/solidity-contracts/{}", filename);
let compiled = Solc::default().compile_source(&path).unwrap();
let contract = compiled.get(&path, name).expect("could not find contract");
(contract.abi.unwrap().clone(), contract.bin.unwrap().clone())
}
/// connects the private key to http://localhost:8545

View File

@ -27,10 +27,8 @@ tiny-keccak = { version = "2.0.2", default-features = false }
serde = { version = "1.0.124", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.64", default-features = false }
thiserror = { version = "1.0.30", default-features = false }
glob = { version = "0.3.0", default-features = false }
bytes = { version = "1.1.0", features = ["serde"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
semver = "1.0.4"
once_cell = "1.8.0"
# eip712 feature enabled dependencies

View File

@ -33,8 +33,7 @@
//! ## Utilities
//!
//! The crate provides utilities for launching local Ethereum testnets by using `ganache-cli`
//! via the `GanacheBuilder` struct. In addition, you're able to compile contracts on the
//! filesystem by providing a glob to their path, using the `Solc` struct.
//! via the `GanacheBuilder` struct.
//!
//! # Features
//!

View File

@ -10,20 +10,6 @@ mod geth;
#[cfg(not(target_arch = "wasm32"))]
pub use geth::{Geth, GethInstance};
/// Solidity compiler bindings
#[cfg(not(target_arch = "wasm32"))]
pub mod solc;
#[cfg(not(target_arch = "wasm32"))]
pub use solc::{CompiledContract, Solc};
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "setup")]
mod setup;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(feature = "setup")]
pub use setup::*;
mod hash;
pub use hash::{hash_message, id, keccak256, serialize};

View File

@ -1,54 +0,0 @@
//! Setup utilities to start necessary infrastructure
use crate::utils::{
solc::{CompiledContract, SolcError},
Ganache, GanacheInstance, Geth, GethInstance, Solc,
};
use std::collections::HashMap;
/// Builds the contracts and returns a hashmap for each named contract
///
/// Same as [crate::utils::Solc::build] but async
pub async fn compile(solc: Solc) -> Result<HashMap<String, CompiledContract>, SolcError> {
tokio::task::spawn_blocking(|| solc.build()).await.unwrap()
}
/// Launches a [crate::utils::GanacheInstance]
///
/// Same as [crate::utils::Ganache::spawn] but async
pub async fn launch_ganache(ganache: Ganache) -> GanacheInstance {
tokio::task::spawn_blocking(|| ganache.spawn()).await.unwrap()
}
/// Compiles the contracts and launches a [crate::utils::GanacheInstance]
///
/// Same as [crate::utils::setup::compile] and [crate::utils::setup::launch_ganache]
pub async fn compile_and_launch_ganache(
solc: Solc,
ganache: Ganache,
) -> Result<(HashMap<String, CompiledContract>, GanacheInstance), SolcError> {
let solc_fut = compile(solc);
let ganache_fut = launch_ganache(ganache);
let (solc, ganache) = futures_util::join!(solc_fut, ganache_fut);
solc.map(|solc| (solc, ganache))
}
/// Launches a [crate::utils::GethInstance]
///
/// Same as [crate::utils::Geth::spawn] but async
pub async fn launch_geth(geth: Geth) -> GethInstance {
tokio::task::spawn_blocking(|| geth.spawn()).await.unwrap()
}
/// Compiles the contracts and launches a [crate::utils::GethInstance]
///
/// Same as [crate::utils::setup::compile] and [crate::utils::setup::launch_geth]
pub async fn compile_and_launch_geth(
solc: Solc,
geth: Geth,
) -> Result<(HashMap<String, CompiledContract>, GethInstance), SolcError> {
let solc_fut = compile(solc);
let geth_fut = launch_geth(geth);
let (solc, geth) = futures_util::join!(solc_fut, geth_fut);
solc.map(|solc| (solc, geth))
}

View File

@ -1,584 +0,0 @@
use std::{collections::HashMap, fmt, io::BufRead, path::PathBuf, process::Command, str::FromStr};
use glob::glob;
use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use crate::{abi::Abi, types::Bytes};
use once_cell::sync::Lazy;
use semver::Version;
/// The name of the `solc` binary on the system
const SOLC: &str = "solc";
/// Support for configuring the EVM version
/// https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/
static CONSTANTINOPLE_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.4.21").unwrap());
/// Petersburg support
/// https://blog.soliditylang.org/2019/03/05/solidity-0.5.5-release-announcement/
static PETERSBURG_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.5.5").unwrap());
/// Istanbul support
/// https://blog.soliditylang.org/2019/12/09/solidity-0.5.14-release-announcement/
static ISTANBUL_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.5.14").unwrap());
/// Berlin support
/// https://blog.soliditylang.org/2021/06/10/solidity-0.8.5-release-announcement/
static BERLIN_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.8.5").unwrap());
/// London support
/// https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/
static LONDON_SOLC: Lazy<Version> = Lazy::new(|| Version::from_str("0.8.7").unwrap());
type Result<T> = std::result::Result<T, SolcError>;
#[derive(Debug, Error)]
pub enum SolcError {
/// Internal solc error
#[error("Solc Error: {0}")]
SolcError(String),
#[error(transparent)]
SemverError(#[from] semver::Error),
/// Deserialization error
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
/// The result of a solc compilation
pub struct CompiledContract {
/// The contract's ABI
pub abi: Abi,
/// The contract's bytecode
pub bytecode: Bytes,
/// The contract's runtime bytecode
pub runtime_bytecode: Bytes,
}
/// Solidity Compiler Bindings
///
/// Assumes that `solc` is installed and available in the caller's $PATH. Any calls
/// will **panic** otherwise.
///
/// By default, it uses 200 optimizer runs and Istanbul as the EVM version
///
/// # Examples
///
/// ```no_run
/// use ethers_core::utils::Solc;
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Give it a glob
/// let contracts = Solc::new("./contracts/*")
/// .optimizer(Some(200))
/// .build()?;
///
/// // this will return None if the specified contract did not exist in the compiled
/// // files
/// let contract = contracts.get("SimpleStorage").expect("contract not found");
/// # Ok(())
/// # }
/// ```
pub struct Solc {
/// The path to the Solc binary
pub solc_path: Option<PathBuf>,
/// The path where contracts will be read from
pub paths: Vec<String>,
/// Number of optimizer runs. None for no optimization
pub optimizer: Option<usize>,
/// Evm Version
pub evm_version: EvmVersion,
/// Paths for importing other libraries
pub allowed_paths: Vec<PathBuf>,
/// Output a single json document containing the specified information.
/// Default is `abi,bin,bin-runtime`
pub combined_json: Option<String>,
/// Additional arguments to pass to solc
pub args: Vec<String>,
}
impl Solc {
/// Instantiates The Solc builder with the provided glob of Solidity files
pub fn new(path: &str) -> Self {
// Convert the glob to a vector of string paths
// TODO: This might not be the most robust way to do this
let paths = glob(path)
.expect("could not get glob")
.map(|path| path.expect("path not found").to_string_lossy().to_string())
.collect::<Vec<String>>();
Self::new_with_paths(paths)
}
/// Instantiates the Solc builder for the provided paths
pub fn new_with_paths(paths: Vec<String>) -> Self {
Self {
paths,
solc_path: None,
optimizer: Some(200), // default optimizer runs = 200
evm_version: EvmVersion::Istanbul,
allowed_paths: Vec::new(),
combined_json: Some("abi,bin,bin-runtime".to_string()),
args: Vec::new(),
}
}
/// Gets the complete solc output as json object
pub fn exec(self) -> Result<serde_json::Value> {
let path = self.solc_path.unwrap_or_else(|| PathBuf::from(SOLC));
let mut command = Command::new(&path);
let version = Solc::version(Some(path));
if let Some(combined_json) = self.combined_json {
command.arg("--combined-json").arg(combined_json);
}
if let Some(evm_version) = normalize_evm_version(&version, self.evm_version) {
command.arg("--evm-version").arg(evm_version.to_string());
}
if let Some(runs) = self.optimizer {
command.arg("--optimize").arg("--optimize-runs").arg(runs.to_string());
}
command.args(self.args);
for path in self.paths {
command.arg(path);
}
let command = command.output().expect("could not run `solc`");
if !command.status.success() {
return Err(SolcError::SolcError(String::from_utf8_lossy(&command.stderr).to_string()))
}
// Deserialize the output
Ok(serde_json::from_slice(&command.stdout)?)
}
/// Gets the ABI for the contracts
pub fn build_raw(self) -> Result<HashMap<String, CompiledContractStr>> {
let mut output = self.exec()?;
let contract_values = output["contracts"].as_object_mut().ok_or_else(|| {
SolcError::SolcError("no contracts found in `solc` output".to_string())
})?;
let mut contracts = HashMap::with_capacity(contract_values.len());
for (name, contract) in contract_values {
if let serde_json::Value::String(bin) = contract["bin"].take() {
let name = name.rsplit(':').next().expect("could not strip fname").to_owned();
// abi could be an escaped string (solc<=0.7) or an array (solc>=0.8)
let abi = match contract["abi"].take() {
serde_json::Value::String(abi) => abi,
val @ serde_json::Value::Array(_) => val.to_string(),
val => {
return Err(SolcError::SolcError(format!(
"Expected abi in solc output, found {:?}",
val
)))
}
};
let runtime_bin =
if let serde_json::Value::String(bin) = contract["bin-runtime"].take() {
bin
} else {
panic!("no runtime bytecode found")
};
contracts.insert(name, CompiledContractStr { abi, bin, runtime_bin });
} else {
return Err(SolcError::SolcError("could not find `bin` in solc output".to_string()))
}
}
Ok(contracts)
}
/// Builds the contracts and returns a hashmap for each named contract
pub fn build(self) -> Result<HashMap<String, CompiledContract>> {
// Build, and then get the data in the correct format
let contracts = self
.build_raw()?
.into_iter()
.map(|(name, contract)| {
// parse the ABI
let abi = serde_json::from_str(&contract.abi)
.expect("could not parse `solc` abi, this should never happen");
// parse the bytecode
let bytecode =
hex::decode(contract.bin).expect("solc did not produce valid bytecode").into();
// parse the runtime bytecode
let runtime_bytecode = hex::decode(contract.runtime_bin)
.expect("solc did not produce valid runtime-bytecode")
.into();
(name, CompiledContract { abi, bytecode, runtime_bytecode })
})
.collect::<HashMap<String, CompiledContract>>();
Ok(contracts)
}
/// Returns the output of `solc --version`
///
/// # Panics
///
/// If `solc` is not found
pub fn version(solc_path: Option<PathBuf>) -> Version {
let solc_path = solc_path.unwrap_or_else(|| PathBuf::from(SOLC));
let command_output = Command::new(&solc_path)
.arg("--version")
.output()
.unwrap_or_else(|_| panic!("`{:?}` not found", solc_path));
let version = command_output
.stdout
.lines()
.last()
.expect("expected version in solc output")
.expect("could not get solc version");
// Return the version trimmed
let version = version.replace("Version: ", "");
Version::from_str(&version[0..5]).expect("not a version")
}
/// Sets the EVM version for compilation
pub fn evm_version(mut self, version: EvmVersion) -> Self {
self.evm_version = version;
self
}
/// Sets the path to the solc binary
pub fn solc_path(mut self, path: PathBuf) -> Self {
self.solc_path = Some(std::fs::canonicalize(path).unwrap());
self
}
/// Sets the `combined-json` option, by default this is set to `abi,bin,bin-runtime`
/// NOTE: In order to get the `CompiledContract` from `Self::build`, this _must_ contain
/// `abi,bin`.
pub fn combined_json(mut self, combined_json: impl Into<String>) -> Self {
self.combined_json = Some(combined_json.into());
self
}
/// Sets the optimizer runs (default = 200). None indicates no optimization
///
/// ```rust,no_run
/// use ethers_core::utils::Solc;
///
/// // No optimization
/// let contracts = Solc::new("./contracts/*")
/// .optimizer(None)
/// .build().unwrap();
///
/// // Some(200) is default, optimizer on with 200 runs
/// // .arg() allows passing arbitrary args to solc command
/// let optimized_contracts = Solc::new("./contracts/*")
/// .optimizer(Some(200))
/// .arg("--metadata-hash=none")
/// .build().unwrap();
/// ```
pub fn optimizer(mut self, runs: Option<usize>) -> Self {
self.optimizer = runs;
self
}
/// Sets the allowed paths for using files from outside the same directory
// TODO: Test this
pub fn allowed_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.allowed_paths = paths;
self
}
/// Adds an argument to pass to solc
pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
self.args.push(arg.into());
self
}
/// Adds multiple arguments to pass to solc
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for arg in args {
self = self.arg(arg);
}
self
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EvmVersion {
Homestead,
TangerineWhistle,
SpuriusDragon,
Constantinople,
Petersburg,
Istanbul,
Berlin,
London,
}
impl fmt::Display for EvmVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let string = match self {
EvmVersion::Homestead => "homestead",
EvmVersion::TangerineWhistle => "tangerineWhistle",
EvmVersion::SpuriusDragon => "spuriusDragon",
EvmVersion::Constantinople => "constantinople",
EvmVersion::Petersburg => "petersburg",
EvmVersion::Istanbul => "istanbul",
EvmVersion::Berlin => "berlin",
EvmVersion::London => "london",
};
write!(f, "{}", string)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
// Helper struct for deserializing the solc string outputs
struct SolcOutput {
contracts: HashMap<String, CompiledContractStr>,
version: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
/// Helper struct for deserializing the solc string outputs
pub struct CompiledContractStr {
/// The contract's raw ABI
pub abi: String,
/// The contract's bytecode in hex
pub bin: String,
/// The contract's runtime bytecode in hex
pub runtime_bin: String,
}
fn normalize_evm_version(version: &Version, evm_version: EvmVersion) -> Option<EvmVersion> {
// the EVM version flag was only added at 0.4.21
// we work our way backwards
if version >= &CONSTANTINOPLE_SOLC {
// If the Solc is at least at london, it supports all EVM versions
Some(if version >= &LONDON_SOLC {
evm_version
// For all other cases, cap at the at-the-time highest possible fork
} else if version >= &BERLIN_SOLC && evm_version >= EvmVersion::Berlin {
EvmVersion::Berlin
} else if version >= &ISTANBUL_SOLC && evm_version >= EvmVersion::Istanbul {
EvmVersion::Istanbul
} else if version >= &PETERSBURG_SOLC && evm_version >= EvmVersion::Petersburg {
EvmVersion::Petersburg
} else if evm_version >= EvmVersion::Constantinople {
EvmVersion::Constantinople
} else {
evm_version
})
} else {
None
}
}
/// General `solc` contract output
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub abi: Abi,
pub evm: Evm,
#[serde(
deserialize_with = "de_from_json_opt",
serialize_with = "ser_to_inner_json",
skip_serializing_if = "Option::is_none"
)]
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Evm {
pub bytecode: Bytecode,
pub deployed_bytecode: Bytecode,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bytecode {
#[serde(deserialize_with = "deserialize_bytes")]
pub object: Bytes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub compiler: Compiler,
pub language: String,
pub output: Output,
pub settings: Settings,
pub sources: Sources,
pub version: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Compiler {
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output {
pub abi: Vec<SolcAbi>,
pub devdoc: Option<Doc>,
pub userdoc: Option<Doc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SolcAbi {
pub inputs: Vec<Item>,
#[serde(rename = "stateMutability")]
pub state_mutability: Option<String>,
#[serde(rename = "type")]
pub abi_type: String,
pub name: Option<String>,
pub outputs: Option<Vec<Item>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
#[serde(rename = "internalType")]
pub internal_type: String,
pub name: String,
#[serde(rename = "type")]
pub put_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Doc {
pub kind: String,
pub methods: Libraries,
pub version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Libraries {
#[serde(flatten)]
pub libs: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
#[serde(rename = "compilationTarget")]
pub compilation_target: CompilationTarget,
#[serde(rename = "evmVersion")]
pub evm_version: String,
pub libraries: Libraries,
pub metadata: MetadataClass,
pub optimizer: Optimizer,
pub remappings: Vec<Option<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompilationTarget {
#[serde(flatten)]
pub inner: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadataClass {
#[serde(rename = "bytecodeHash")]
pub bytecode_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Optimizer {
pub enabled: bool,
pub runs: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sources {
#[serde(flatten)]
pub inner: HashMap<String, serde_json::Value>,
}
pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(d)?;
Ok(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into())
}
fn de_from_json_opt<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
if let Some(val) = <Option<String>>::deserialize(deserializer)? {
serde_json::from_str(&val).map_err(serde::de::Error::custom)
} else {
Ok(None)
}
}
fn ser_to_inner_json<S, T>(val: &T, s: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
{
let val = serde_json::to_string(val).map_err(serde::ser::Error::custom)?;
s.serialize_str(&val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_solc_version() {
Solc::version(None);
}
#[test]
fn test_evm_version_normalization() {
for (solc_version, evm_version, expected) in &[
// Ensure 0.4.21 it always returns None
("0.4.20", EvmVersion::Homestead, None),
// Constantinople clipping
("0.4.21", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.4.21", EvmVersion::Constantinople, Some(EvmVersion::Constantinople)),
("0.4.21", EvmVersion::London, Some(EvmVersion::Constantinople)),
// Petersburg
("0.5.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.5.5", EvmVersion::Petersburg, Some(EvmVersion::Petersburg)),
("0.5.5", EvmVersion::London, Some(EvmVersion::Petersburg)),
// Istanbul
("0.5.14", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.5.14", EvmVersion::Istanbul, Some(EvmVersion::Istanbul)),
("0.5.14", EvmVersion::London, Some(EvmVersion::Istanbul)),
// Berlin
("0.8.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.8.5", EvmVersion::Berlin, Some(EvmVersion::Berlin)),
("0.8.5", EvmVersion::London, Some(EvmVersion::Berlin)),
// London
("0.8.7", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
("0.8.7", EvmVersion::London, Some(EvmVersion::London)),
("0.8.7", EvmVersion::London, Some(EvmVersion::London)),
] {
assert_eq!(
&normalize_evm_version(&Version::from_str(solc_version).unwrap(), *evm_version),
expected
)
}
}
}

View File

@ -42,6 +42,7 @@ hex = { version = "0.4.3", default-features = false, features = ["std"] }
rand = { version = "0.8.4", default-features = false }
ethers-providers = { version = "^0.5.0", path = "../ethers-providers", default-features = false, features = ["ws", "rustls"] }
once_cell = "1.8.0"
ethers-solc = { version = "^0.1.0", path = "../ethers-solc", default-features = false }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.5", default-features = false, features = ["rt", "macros", "time"] }

View File

@ -214,18 +214,19 @@ async fn deploy_and_call_contract() {
use ethers_core::{
abi::Abi,
types::{BlockNumber, Bytes, H256, U256},
utils::Solc,
};
use ethers_solc::Solc;
use std::sync::Arc;
fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) {
let compiled =
Solc::new(&format!("./tests/solidity-contracts/{}", filename)).build().unwrap();
let contract = compiled.get(name).expect("could not find contract");
(contract.abi.clone(), contract.bytecode.clone())
// compiles the given contract and returns the ABI and Bytecode
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
let path = format!("./tests/solidity-contracts/{}", path);
let compiled = Solc::default().compile_source(&path).unwrap();
let contract = compiled.get(&path, name).expect("could not find contract");
(contract.abi.unwrap().clone(), contract.bin.unwrap().clone())
}
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage");
// Celo testnet
let provider = Provider::<Http>::try_from("https://alfajores-forno.celo-testnet.org")

View File

@ -1,21 +1,27 @@
#![cfg(not(target_arch = "wasm32"))]
#![allow(unused)]
use ethers_contract::{BaseContract, ContractFactory};
use ethers_core::{
types::*,
utils::{Ganache, Solc},
};
use ethers_core::{abi::Abi, types::*, utils::Ganache};
use ethers_middleware::{
transformer::{DsProxy, TransformerMiddleware},
SignerMiddleware,
};
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::{LocalWallet, Signer};
use ethers_solc::Solc;
use rand::Rng;
use std::{convert::TryFrom, sync::Arc, time::Duration};
type HttpWallet = SignerMiddleware<Provider<Http>, LocalWallet>;
// compiles the given contract and returns the ABI and Bytecode
fn compile_contract(path: &str, name: &str) -> (Abi, Bytes) {
let path = format!("./tests/solidity-contracts/{}", path);
let compiled = Solc::default().compile_source(&path).unwrap();
let contract = compiled.get(&path, name).expect("could not find contract");
(contract.abi.unwrap().clone(), contract.bin.unwrap().clone())
}
#[tokio::test]
#[cfg(not(feature = "celo"))]
async fn ds_proxy_transformer() {
@ -35,15 +41,8 @@ async fn ds_proxy_transformer() {
let provider = Arc::new(signer_middleware.clone());
// deploy DsProxyFactory which we'll use to deploy a new DsProxy contract.
let compiled = Solc::new("./tests/solidity-contracts/DSProxy.sol")
.build()
.expect("could not compile DSProxyFactory");
let contract = compiled.get("DSProxyFactory").expect("could not find DSProxyFactory");
let factory = ContractFactory::new(
contract.abi.clone(),
contract.bytecode.clone(),
Arc::clone(&provider),
);
let (abi, bytecode) = compile_contract("DSProxy.sol", "DSProxyFactory");
let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider));
let ds_proxy_factory = factory.deploy(()).unwrap().legacy();
let ds_proxy_factory = ds_proxy_factory.send().await.unwrap();
@ -58,15 +57,8 @@ async fn ds_proxy_transformer() {
let ds_proxy_addr = ds_proxy.address();
// deploy SimpleStorage and try to update its value via transformer middleware.
let compiled = Solc::new("./tests/solidity-contracts/SimpleStorage.sol")
.build()
.expect("could not compile SimpleStorage");
let contract = compiled.get("SimpleStorage").expect("could not find SimpleStorage");
let factory = ContractFactory::new(
contract.abi.clone(),
contract.bytecode.clone(),
Arc::clone(&provider),
);
let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage");
let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider));
let deployer = factory.deploy(()).unwrap().legacy();
let simple_storage = deployer.send().await.unwrap();
@ -108,15 +100,8 @@ async fn ds_proxy_code() {
let provider = Arc::new(signer_middleware.clone());
// deploy DsProxyFactory which we'll use to deploy a new DsProxy contract.
let compiled = Solc::new("./tests/solidity-contracts/DSProxy.sol")
.build()
.expect("could not compile DSProxyFactory");
let contract = compiled.get("DSProxyFactory").expect("could not find DSProxyFactory");
let factory = ContractFactory::new(
contract.abi.clone(),
contract.bytecode.clone(),
Arc::clone(&provider),
);
let (abi, bytecode) = compile_contract("DSProxy.sol", "DSProxyFactory");
let factory = ContractFactory::new(abi, bytecode, Arc::clone(&provider));
let ds_proxy_factory = factory.deploy(()).unwrap().legacy();
let ds_proxy_factory = ds_proxy_factory.send().await.unwrap();
@ -131,11 +116,8 @@ async fn ds_proxy_code() {
let ds_proxy_addr = ds_proxy.address();
// compile the SimpleStorage contract which we will use to interact via DsProxy.
let compiled = Solc::new("./tests/solidity-contracts/SimpleStorage.sol")
.build()
.expect("could not compile SimpleStorage");
let ss = compiled.get("SimpleStorage").expect("could not find SimpleStorage");
let ss_base_contract: BaseContract = ss.abi.clone().into();
let (abi, bytecode) = compile_contract("SimpleStorage.sol", "SimpleStorage");
let ss_base_contract: BaseContract = abi.into();
let expected_value: u64 = rng.gen();
let calldata = ss_base_contract
.encode("setValue", U256::from(expected_value))
@ -145,7 +127,7 @@ async fn ds_proxy_code() {
ds_proxy
.execute::<HttpWallet, Arc<HttpWallet>, Bytes>(
Arc::clone(&provider),
ss.bytecode.clone(),
bytecode.clone(),
calldata,
)
.expect("could not construct DSProxy contract call")

View File

@ -11,9 +11,10 @@ homepage = "https://docs.rs/ethers"
description = """
Utilites for working with solc
"""
keywords = ["ethereum", "web3", "etherscan", "ethers"]
keywords = ["ethereum", "web3", "solc", "solidity", "ethers"]
[dependencies]
ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false }
serde_json = "1.0.68"
serde = { version = "1.0.130", features = ["derive"] }
semver = "1.0.4"

View File

@ -1,4 +1,5 @@
//! Solc artifact types
use ethers_core::{abi::Abi, types::Bytes};
use colored::Colorize;
use md5::Digest;
@ -11,10 +12,7 @@ use std::{
};
use crate::{compile::*, utils};
use serde::{
de::{self, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
/// An ordered list of files and their source
pub type Sources = BTreeMap<PathBuf, Source>;
@ -366,7 +364,7 @@ impl AsRef<str> for Source {
}
/// Output type `solc` produces
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Default)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct CompilerOutput {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Error>,
@ -385,6 +383,15 @@ impl CompilerOutput {
pub fn diagnostics<'a>(&'a self, ignored_error_codes: &'a [u64]) -> OutputDiagnostics {
OutputDiagnostics { errors: &self.errors, ignored_error_codes }
}
/// Given the contract file's path and the contract's name, tries to return the contract's
/// bytecode, runtime bytecode, and abi
pub fn get(&self, path: &str, contract: &str) -> Option<CompactContractRef> {
self.contracts
.get(path)
.and_then(|contracts| contracts.get(contract))
.map(CompactContractRef::from)
}
}
/// Helper type to implement display for solc errors
@ -419,11 +426,11 @@ impl<'a> fmt::Display for OutputDiagnostics<'a> {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Contract {
/// The Ethereum Contract ABI. If empty, it is represented as an empty
/// array. See https://docs.soliditylang.org/en/develop/abi-spec.html
pub abi: Vec<serde_json::Value>,
/// The Ethereum Contract ABI.
/// See https://docs.soliditylang.org/en/develop/abi-spec.html
pub abi: Option<Abi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<String>,
#[serde(default)]
@ -443,15 +450,19 @@ pub struct Contract {
}
/// Minimal representation of a contract's abi with bytecode
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CompactContract {
/// The Ethereum Contract ABI. If empty, it is represented as an empty
/// array. See https://docs.soliditylang.org/en/develop/abi-spec.html
pub abi: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bin: Option<String>,
pub abi: Option<Abi>,
#[serde(
default,
deserialize_with = "deserialize_opt_bytes",
skip_serializing_if = "Option::is_none"
)]
pub bin: Option<Bytes>,
#[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")]
pub bin_runtime: Option<String>,
pub bin_runtime: Option<Bytes>,
}
impl From<Contract> for CompactContract {
@ -469,25 +480,25 @@ impl From<Contract> for CompactContract {
/// Helper type to serialize while borrowing from `Contract`
#[derive(Clone, Debug, Serialize)]
pub struct CompactContractRef<'a> {
pub abi: &'a [serde_json::Value],
pub abi: Option<&'a Abi>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bin: Option<&'a str>,
pub bin: Option<&'a Bytes>,
#[serde(default, rename = "bin-runtime", skip_serializing_if = "Option::is_none")]
pub bin_runtime: Option<&'a str>,
pub bin_runtime: Option<&'a Bytes>,
}
impl<'a> From<&'a Contract> for CompactContractRef<'a> {
fn from(c: &'a Contract) -> Self {
let (bin, bin_runtime) = if let Some(ref evm) = c.evm {
(
Some(evm.bytecode.object.as_str()),
evm.deployed_bytecode.bytecode.as_ref().map(|evm| evm.object.as_str()),
Some(&evm.bytecode.object),
evm.deployed_bytecode.bytecode.as_ref().map(|evm| &evm.object),
)
} else {
(None, None)
};
Self { abi: &c.abi, bin, bin_runtime }
Self { abi: c.abi.as_ref(), bin, bin_runtime }
}
}
@ -545,7 +556,8 @@ pub struct Bytecode {
#[serde(default, skip_serializing_if = "::std::collections::BTreeMap::is_empty")]
pub function_debug_data: BTreeMap<String, FunctionDebugData>,
/// The bytecode as a hex string.
pub object: String,
#[serde(deserialize_with = "deserialize_bytes")]
pub object: Bytes,
/// Opcodes list (string)
pub opcodes: String,
/// The source mapping as a string. See the source mapping definition.
@ -660,26 +672,12 @@ pub struct Error {
pub r#type: String,
pub component: String,
pub severity: Severity,
#[serde(default, deserialize_with = "from_optional_str")]
#[serde(default, with = "display_from_str_opt")]
pub error_code: Option<u64>,
pub message: String,
pub formatted_message: Option<String>,
}
fn from_optional_str<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: FromStr,
T::Err: fmt::Display,
D: Deserializer<'de>,
{
let s = Option::<String>::deserialize(deserializer)?;
if let Some(s) = s {
T::from_str(&s).map_err(de::Error::custom).map(Some)
} else {
Ok(None)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(msg) = &self.formatted_message {
@ -821,6 +819,26 @@ mod display_from_str_opt {
}
}
pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(d)?;
Ok(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into())
}
pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result<Option<Bytes>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<String>::deserialize(d)?;
if let Some(value) = value {
Ok(Some(hex::decode(&value).map_err(|e| serde::de::Error::custom(e.to_string()))?.into()))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -53,7 +53,7 @@ impl Solc {
}
/// Convenience function for compiling all sources under the given path
pub fn compile_source<T: Serialize>(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
pub fn compile_source(&self, path: impl AsRef<Path>) -> Result<CompilerOutput> {
self.compile(&CompilerInput::new(path)?)
}

View File

@ -206,6 +206,8 @@ impl SolcConfigBuilder {
/// Determines how to handle compiler output
pub enum ArtifactOutput {
/// No-op, does not write the artifacts to disk.
Nothing,
/// Creates a single json artifact with
/// ```json
/// {
@ -225,6 +227,7 @@ impl ArtifactOutput {
/// Is expected to handle the output and where to store it
pub fn on_output(&self, output: &CompilerOutput, layout: &ProjectPathsConfig) -> Result<()> {
match self {
ArtifactOutput::Nothing => Ok(()),
ArtifactOutput::MinimalCombined => {
fs::create_dir_all(&layout.artifacts)?;
@ -254,6 +257,9 @@ impl Default for ArtifactOutput {
impl fmt::Debug for ArtifactOutput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArtifactOutput::Nothing => {
write!(f, "Nothing")
}
ArtifactOutput::MinimalCombined => {
write!(f, "MinimalCombined")
}

View File

@ -231,7 +231,7 @@ impl Default for ProjectBuilder {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub enum ProjectCompileOutput<'a> {
/// Nothing to compile because unchanged sources
Unchanged,

View File

@ -1,4 +1,4 @@
use ethers::{contract::Abigen, utils::Solc};
use ethers::{contract::Abigen, solc::Solc};
fn main() -> anyhow::Result<()> {
let mut args = std::env::args();
@ -9,10 +9,11 @@ fn main() -> anyhow::Result<()> {
println!("Generating bindings for {}\n", contract);
// compile it if needed
// compile it
let abi = if contract.ends_with(".sol") {
let contracts = Solc::new(&contract).build_raw()?;
contracts.get(&contract_name).unwrap().abi.clone()
let contracts = Solc::default().compile_source(&contract)?;
let abi = contracts.get(&contract, &contract_name).unwrap().abi.unwrap();
serde_json::to_string(abi).unwrap()
} else {
contract
};

View File

@ -1,9 +1,7 @@
use anyhow::Result;
use ethers::{
prelude::*,
utils::{compile_and_launch_ganache, Ganache, Solc},
};
use std::{convert::TryFrom, sync::Arc, time::Duration};
use ethers::{prelude::*, utils::Ganache};
use ethers_solc::{ArtifactOutput, Project, ProjectCompileOutput, ProjectPathsConfig};
use std::{convert::TryFrom, path::PathBuf, sync::Arc, time::Duration};
// Generate the type-safe contract bindings by providing the ABI
// definition in human readable format
@ -19,13 +17,30 @@ abigen!(
#[tokio::main]
async fn main() -> Result<()> {
// 1. compile the contract (note this requires that you are inside the `examples` directory) and
// launch ganache
let (compiled, ganache) =
compile_and_launch_ganache(Solc::new("**/contract.sol"), Ganache::new()).await?;
let contract = compiled.get("SimpleStorage").expect("could not find contract");
// the directory we use is root-dir/examples
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples");
// we use `root` for both the project root and for where to search for contracts since
// everything is in the same directory
let paths = ProjectPathsConfig::builder().root(&root).sources(&root).build().unwrap();
// get the solc project instance using the paths above
let solc = Project::builder()
.paths(paths)
.ephemeral()
.artifacts(ArtifactOutput::Nothing)
.build()
.unwrap();
// compile the project and get the artifacts
let compiled = solc.compile().unwrap();
let compiled = match compiled {
ProjectCompileOutput::Compiled((output, _)) => output,
_ => panic!("expected compilation artifacts"),
};
let path = root.join("contract.sol");
let path = path.to_str();
let contract = compiled.get(path.unwrap(), "SimpleStorage").expect("could not find contract");
// 2. instantiate our wallet
// 2. instantiate our wallet & ganache
let ganache = Ganache::new().spawn();
let wallet: LocalWallet = ganache.keys()[0].clone().into();
// 3. connect to the network
@ -37,8 +52,11 @@ async fn main() -> Result<()> {
let client = Arc::new(client);
// 5. create a factory which will be used to deploy instances of the contract
let factory =
ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), client.clone());
let factory = ContractFactory::new(
contract.abi.unwrap().clone(),
contract.bin.unwrap().clone(),
client.clone(),
);
// 6. deploy it with the constructor arguments
let contract = factory.deploy("initial value".to_string())?.legacy().send().await?;

View File

@ -88,6 +88,7 @@ pub use ethers_core as core;
pub use ethers_middleware as middleware;
pub use ethers_providers as providers;
pub use ethers_signers as signers;
pub use ethers_solc as solc;
// Re-export ethers_core::utils/types/abi
// We hide these docs so that the rustdoc links send the visitor
@ -107,4 +108,5 @@ pub mod prelude {
pub use ethers_middleware::*;
pub use ethers_providers::*;
pub use ethers_signers::*;
pub use ethers_solc::*;
}