From d4bc43ee5bb8feb1620054b946de900de8a59e8b Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Sun, 31 May 2020 17:50:00 +0300 Subject: [PATCH] feat(types): add solc bindings --- Cargo.lock | 7 + crates/ethers-contract/src/factory.rs | 2 +- crates/ethers-types/Cargo.toml | 2 + crates/ethers-types/src/lib.rs | 3 + crates/ethers-types/src/solc.rs | 210 ++++++++++++++++++++++++++ crates/ethers/examples/contract.rs | 57 +++++-- crates/ethers/examples/contract.sol | 22 +++ 7 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 crates/ethers-types/src/solc.rs create mode 100644 crates/ethers/examples/contract.sol diff --git a/Cargo.lock b/Cargo.lock index 752669a2..5fb5575f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,7 @@ dependencies = [ "ethabi 12.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "ethereum-types 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", "ethers-utils 0.1.0", + "glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-hex 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -437,6 +438,11 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "h2" version = "0.2.5" @@ -1554,6 +1560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum futures-task 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" "checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" "checksum h2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" "checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" "checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" diff --git a/crates/ethers-contract/src/factory.rs b/crates/ethers-contract/src/factory.rs index d0be9264..ee68dd2e 100644 --- a/crates/ethers-contract/src/factory.rs +++ b/crates/ethers-contract/src/factory.rs @@ -90,7 +90,7 @@ where /// Deploys an instance of the contract with the provider constructor arguments /// and returns the contract's instance - pub async fn deploy( + pub fn deploy( &self, constructor_args: T, ) -> Result, ContractError

> { diff --git a/crates/ethers-types/Cargo.toml b/crates/ethers-types/Cargo.toml index 59ceb197..236460ff 100644 --- a/crates/ethers-types/Cargo.toml +++ b/crates/ethers-types/Cargo.toml @@ -19,9 +19,11 @@ zeroize = { version = "1.1.0", default-features = false } # misc serde = { version = "1.0.110", default-features = false, features = ["derive"] } +serde_json = { version = "1.0.53", default-features = false, features = ["alloc"] } rustc-hex = { version = "2.1.0", default-features = false } thiserror = { version = "1.0.19", default-features = false } arrayvec = { version = "0.5.1", default-features = false, optional = true } +glob = "0.3.0" [dev-dependencies] serde_json = { version = "1.0.53", default-features = false } diff --git a/crates/ethers-types/src/lib.rs b/crates/ethers-types/src/lib.rs index 02754663..b8d22f15 100644 --- a/crates/ethers-types/src/lib.rs +++ b/crates/ethers-types/src/lib.rs @@ -46,3 +46,6 @@ pub mod abi; // Convenience re-export pub use ethers_utils as utils; + +mod solc; +pub use solc::Solc; diff --git a/crates/ethers-types/src/solc.rs b/crates/ethers-types/src/solc.rs new file mode 100644 index 00000000..8c30a99d --- /dev/null +++ b/crates/ethers-types/src/solc.rs @@ -0,0 +1,210 @@ +//! Solidity Compiler Bindings +//! +//! Assumes that `solc` is installed and available in the caller's $PATH. Any calls +//! will fail otherwise. +//! +//! # Examples +//! +//! ```rust,ignore +//! // Give it a glob +//! let contracts = Solc::new("./contracts/*") +//! .optimizer(200) +//! .build(); +//! let contract = contracts.get("SimpleStorage").unwrap(); +//! ``` +use crate::{abi::Abi, Bytes}; +use glob::glob; +use rustc_hex::FromHex; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt, io::BufRead, path::PathBuf, process::Command}; +use thiserror::Error; + +/// The name of the `solc` binary on the system +const SOLC: &str = "solc"; + +type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum SolcError { + /// Internal solc error + #[error("Solc Error: {0}")] + SolcError(String), + /// Deserialization error + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), +} + +#[derive(Clone, Debug)] +/// The result of a solc compilation +pub struct CompiledContract { + /// The contract's ABI + pub abi: Abi, + /// The contract's bytecode + pub bytecode: Bytes, +} + +/// Solc builder +pub struct Solc { + /// The path where contracts will be read from + pub paths: Vec, + + /// Number of runs + pub optimizer: usize, + + /// Evm Version + pub evm_version: EvmVersion, + + /// Paths for importing other libraries + pub allowed_paths: Vec, +} + +impl Solc { + /// Instantiates the Solc builder for the provided paths + 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::>(); + + Self { + paths, + optimizer: 200, // default optimizer runs = 200 + evm_version: EvmVersion::Istanbul, + allowed_paths: Vec::new(), + } + } + + /// Builds the contracts and returns a hashmap for each named contract + pub fn build(self) -> Result> { + let mut command = Command::new(SOLC); + + command + .arg("--evm-version") + .arg(self.evm_version.to_string()) + .arg("--combined-json") + .arg("abi,bin"); + + 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 + let output: SolcOutput = serde_json::from_slice(&command.stdout)?; + + // Get the data in the correct format + let contracts = output + .contracts + .into_iter() + .map(|(name, contract)| { + let abi = serde_json::from_str(&contract.abi) + .expect("could not parse `solc` abi, this should never happen"); + + let bytecode = contract + .bin + .from_hex::>() + .expect("solc did not produce valid bytecode") + .into(); + + let name = name + .rsplit(":") + .next() + .expect("could not strip fname") + .to_owned(); + (name, CompiledContract { abi, bytecode }) + }) + .collect::>(); + + Ok(contracts) + } + + /// Returns the output of `solc --version` + /// + /// # Panics + /// + /// If `solc` is not in the user's $PATH + pub fn version() -> String { + let command_output = Command::new(SOLC) + .arg("--version") + .output() + .expect(&format!("`{}` not in user's $PATH", SOLC)); + + let version = command_output + .stdout + .lines() + .last() + .expect("expected version in solc output") + .expect("could not get solc version"); + + // Return the version trimmed + version.replace("Version: ", "") + } + + /// Sets the EVM version for compilation + pub fn evm_version(mut self, version: EvmVersion) -> Self { + self.evm_version = version; + self + } + + /// Sets the optimizer runs (default = 200) + pub fn optimizer_runs(mut self, runs: 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) -> Self { + self.allowed_paths = paths; + self + } +} + +#[derive(Clone, Debug)] +pub enum EvmVersion { + Homestead, + TangerineWhistle, + SpuriusDragon, + Constantinople, + Petersburg, + Istanbul, + Berlin, +} + +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", + }; + write!(f, "{}", string) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +// Helper struct for deserializing the solc string outputs +struct SolcOutput { + contracts: HashMap, + version: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +// Helper struct for deserializing the solc string outputs +struct CompiledContractStr { + abi: String, + bin: String, +} diff --git a/crates/ethers/examples/contract.rs b/crates/ethers/examples/contract.rs index d02c0c52..a30e0015 100644 --- a/crates/ethers/examples/contract.rs +++ b/crates/ethers/examples/contract.rs @@ -1,10 +1,14 @@ use anyhow::Result; -use ethers::{providers::HttpProvider, signers::MainnetWallet, types::Address}; +use ethers::{ + contract::{abigen, ContractFactory}, + providers::HttpProvider, + signers::MainnetWallet, + types::Solc, + utils::ganache::GanacheBuilder, +}; use std::convert::TryFrom; -use ethers::contract::abigen; - -// Generate the contract code +// Generate the contract bindings by providing the ABI abigen!( SimpleContract, r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","constant": true, "type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#, @@ -13,27 +17,48 @@ abigen!( #[tokio::main] async fn main() -> Result<()> { - // connect to the network - let provider = HttpProvider::try_from("http://localhost:8545")?; + // 1. compile the contract (note this requires that you are inside the `ethers/examples` directory) + let compiled = Solc::new("./contract.sol").build()?; + let contract = compiled + .get("SimpleStorage") + .expect("could not find contract"); - // create a wallet and connect it to the provider - let client = "ea878d94d9b1ffc78b45fc7bfc72ec3d1ce6e51e80c8e376c3f7c9a861f7c214" - .parse::()? - .connect(&provider); + // 2. launch ganache + let port = 8546u64; + let url = format!("http://localhost:{}", port).to_string(); + let _ganache = GanacheBuilder::new().port(port) + .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") + .spawn(); - // Contract should take both provider or a signer + // 3. instantiate our wallet + let wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + .parse::()?; - // get the contract's address - let addr = "ebBe15d9C365fC8a04a82E06644d6B39aF20cC31".parse::

()?; + // 4. connect to the network + let provider = HttpProvider::try_from(url.as_str())?; - // instantiate it - let contract = SimpleContract::new(addr, &client); + // 5. instantiate the client with the wallet + let client = wallet.connect(&provider); - // call the method + // 6. create a factory which will be used to deploy instances of the contract + let factory = ContractFactory::new(&client, &contract.abi, &contract.bytecode); + + // 7. deploy it with the constructor arguments + let contract = factory.deploy("initial value".to_string())?.send().await?; + + // 8. get the contract's address + let addr = contract.address(); + + // 9. instantiate the contract + let contract = SimpleContract::new(addr.clone(), &client); + + // 10. call the `setValue` method let _tx_hash = contract.set_value("hi".to_owned()).send().await?; + // 11. get all events let logs = contract.value_changed().from_block(0u64).query().await?; + // 12. get the new value let value = contract.get_value().call().await?; println!("Value: {}. Logs: {}", value, serde_json::to_string(&logs)?); diff --git a/crates/ethers/examples/contract.sol b/crates/ethers/examples/contract.sol new file mode 100644 index 00000000..9d04f2f4 --- /dev/null +++ b/crates/ethers/examples/contract.sol @@ -0,0 +1,22 @@ +pragma solidity >=0.4.24; + +contract SimpleStorage { + + event ValueChanged(address indexed author, string oldValue, string newValue); + + string _value; + + constructor(string memory value) public { + emit ValueChanged(msg.sender, _value, value); + _value = value; + } + + function getValue() view public returns (string memory) { + return _value; + } + + function setValue(string memory value) public { + emit ValueChanged(msg.sender, _value, value); + _value = value; + } +}