feat(types): add solc bindings

This commit is contained in:
Georgios Konstantopoulos 2020-05-31 17:50:00 +03:00
parent bdda7d0883
commit d4bc43ee5b
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
7 changed files with 286 additions and 17 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -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<T: Tokenize>(
pub fn deploy<T: Tokenize>(
&self,
constructor_args: T,
) -> Result<Deployer<'a, P, N, S>, ContractError<P>> {

View File

@ -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 }

View File

@ -46,3 +46,6 @@ pub mod abi;
// Convenience re-export
pub use ethers_utils as utils;
mod solc;
pub use solc::Solc;

View File

@ -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<T> = std::result::Result<T, SolcError>;
#[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<String>,
/// Number of runs
pub optimizer: usize,
/// Evm Version
pub evm_version: EvmVersion,
/// Paths for importing other libraries
pub allowed_paths: Vec<PathBuf>,
}
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::<Vec<String>>();
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<HashMap<String, CompiledContract>> {
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::<Vec<u8>>()
.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::<HashMap<String, CompiledContract>>();
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<PathBuf>) -> 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<String, CompiledContractStr>,
version: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
// Helper struct for deserializing the solc string outputs
struct CompiledContractStr {
abi: String,
bin: String,
}

View File

@ -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::<MainnetWallet>()?
.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::<MainnetWallet>()?;
// get the contract's address
let addr = "ebBe15d9C365fC8a04a82E06644d6B39aF20cC31".parse::<Address>()?;
// 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)?);

View File

@ -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;
}
}