el refactor

This commit is contained in:
Georgios Konstantopoulos 2020-05-26 12:37:31 +03:00
parent d3b9b378c5
commit 3f313ede01
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
49 changed files with 3363 additions and 1256 deletions

1383
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,44 @@
[package] [workspace]
name = "ethers"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
[dependencies] members = [
ethereum-types = { version = "0.9.2", default-features = false, features = ["serialize"] } # "./crates/ethers",
url = { version = "2.1.1", default-features = false } # "./crates/ethers-abi",
async-trait = { version = "0.1.31", default-features = false } # "./crates/ethers-contract",
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] } # "./crates/ethers-derive",
serde = { version = "1.0.110", default-features = false, features = ["derive"] } # "./crates/ethers-providers",
serde_json = { version = "1.0.53", default-features = false } # "./crates/ethers-signers",
thiserror = { version = "1.0.19", default-features = false } "./crates/ethers-types",
rustc-hex = { version = "2.1.0", default-features = false } # "./crates/ethers-utils",
rand = { version = "0.5.1", default-features = false } # this should be the same rand crate version as the one in secp ]
secp256k1 = { version = "0.17.2", default-features = false, features = ["std", "recovery", "rand"] }
zeroize = { version = "1.1.0", default-features = false }
tiny-keccak = { version = "2.0.2", default-features = false }
solc = { git = "https://github.com/paritytech/rust_solc "} # [dependencies]
rlp = "0.4.5" # ethers-derive = { path = "ethers-derive", optional = true }
ethabi = "12.0.0" #
bincode = "1.2.1" # ethereum-types = { version = "0.9.2", default-features = false, features = ["serialize"] }
arrayvec = "0.5.1" # url = { version = "2.1.1", default-features = false }
# async-trait = { version = "0.1.31", default-features = false }
[dev-dependencies] # reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "0.2.21", features = ["macros"] } # serde = { version = "1.0.110", default-features = false, features = ["derive"] }
failure = "0.1.8" # serde_json = { version = "1.0.53", default-features = false }
rand = { version = "0.5.1" } # thiserror = { version = "1.0.19", default-features = false }
# rustc-hex = { version = "2.1.0", default-features = false }
# rand = { version = "0.5.1", default-features = false } # this should be the same rand crate version as the one in secp
# secp256k1 = { version = "0.17.2", default-features = false, features = ["std", "recovery", "rand"] }
# zeroize = { version = "1.1.0", default-features = false }
# tiny-keccak = { version = "2.0.2", default-features = false }
#
# solc = { git = "https://github.com/paritytech/rust_solc", optional = true }
# rlp = "0.4.5"
# ethabi = "12.0.0"
# bincode = "1.2.1"
# arrayvec = "0.5.1"
#
# [dev-dependencies]
# tokio = { version = "0.2.21", features = ["macros"] }
# failure = "0.1.8"
# rand = { version = "0.5.1" }
#
# [features]
# default = ["derive"]
# derive = ["ethers-derive"]
#

22
contract.sol Normal file
View File

@ -0,0 +1,22 @@
pragma solidity ^0.6.6;
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;
}
}

View File

@ -0,0 +1,9 @@
[package]
name = "ethers-abi"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,9 @@
[package]
name = "ethers-contract"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -0,0 +1,21 @@
[package]
name = "ethers-derive"
version = "0.1.0"
authors = ["Nicholas Rodrigues Lordello <nlordell@gmail.com>", "Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
license = "MIT OR Apache-2.0"
repository = "https://github.com/gnosis/ethcontract-rs"
homepage = "https://github.com/gnosis/ethcontract-rs"
documentation = "https://docs.rs/ethcontract"
description = "Code generation for type-safe bindings to Ethereum smart contracts"
[dependencies]
ethers-abi = { path = "../ethers-abi" }
anyhow = "1.0"
curl = "0.4"
Inflector = "0.11"
proc-macro2 = "1.0"
quote = "1.0"
syn = "1.0.12"
url = "2.1"

View File

@ -0,0 +1,55 @@
# `ethcontract-generate`
An alternative API for generating type-safe contract bindings from `build.rs`
scripts. Using this method instead of the procedural macro has a couple
advantages:
- Proper integration with with RLS and Racer for autocomplete support
- Ability to inspect the generated code
The downside of using the generator API is the requirement of having a build
script instead of a macro invocation.
## Getting Started
Using crate requires two dependencies - one for the runtime and one for the
generator:
```toml
[dependencies]
ethcontract = { version = "...", default-features = false }
[build-dependencies]
ethcontract-generate = "..."
```
It is recommended that both versions be kept in sync or else unexpected
behaviour may occur.
Then, in your `build.rs` include the following code:
```rs
use ethcontract_generate::Builder;
use std::env;
use std::path::Path;
fn main() {
let dest = env::var("OUT_DIR").unwrap();
Builder::new("path/to/truffle/build/contract/Contract.json")
.generate()
.unwrap()
.write_to_file(Path::new(&dest).join("rust_coin.rs"))
.unwrap();
}
```
## Relation to `ethcontract-derive`
`ethcontract-derive` uses `ethcontract-generate` under the hood so their
generated bindings should be identical, they just provide different APIs to the
same functionality.
The long term goal of this project is to maintain `ethcontract-derive`. For now
there is no extra work in having it split into two separate crates. That being
said if RLS support improves for procedural macro generated code, it is possible
that this crate be deprecated in favour of `ethcontract-derive` as long as there
is no good argument to keep it around.

View File

@ -0,0 +1,503 @@
//! Implementation of procedural macro for generating type-safe bindings to an
//! ethereum smart contract.
#![deny(missing_docs, unsafe_code)]
extern crate proc_macro;
mod spanned;
use crate::spanned::{ParseInner, Spanned, parse_address, Address, Builder};
use ethers::abi::{Function, Param, ParamType, FunctionExt, ParamTypeExt};
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens as _};
use std::collections::HashSet;
use std::error::Error;
use syn::ext::IdentExt;
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
use syn::{
braced, parenthesized, parse_macro_input, Error as SynError, Ident, LitInt, LitStr, Path,
Token, Visibility,
};
// TODO: Make it accept an inline ABI array
/// Proc macro to generate type-safe bindings to a contract. This macro accepts
/// an Ethereum contract ABIABI or a path. Note that this path is rooted in
/// the crate's root `CARGO_MANIFEST_DIR`.
///
/// ```ignore
/// ethcontract::contract!("build/contracts/MyContract.json");
/// ```
///
/// Alternatively, other sources may be used, for full details consult the
/// `ethcontract-generate::source` documentation. Some basic examples:
///
/// ```ignore
/// // HTTP(S) source
/// ethcontract::contract!("https://my.domain.local/path/to/contract.json")
/// // Etherscan.io
/// ethcontract::contract!("etherscan:0x0001020304050607080910111213141516171819");
/// ethcontract::contract!("https://etherscan.io/address/0x0001020304050607080910111213141516171819");
/// // npmjs
/// ethcontract::contract!("npm:@org/package@1.0.0/path/to/contract.json")
/// ```
///
/// Note that Etherscan rate-limits requests to their API, to avoid this an
/// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use
/// that API key when retrieving the contract ABI.
///
/// Currently the proc macro accepts additional parameters to configure some
/// aspects of the code generation. Specifically it accepts:
/// - `crate`: The name of the `ethcontract` crate. This is useful if the crate
/// was renamed in the `Cargo.toml` for whatever reason.
/// - `contract`: Override the contract name that is used for the generated
/// type. This is required when using sources that do not provide the contract
/// name in the artifact JSON such as Etherscan.
/// - `mod`: The name of the contract module to place generated code in. Note
/// that the root contract type gets re-exported in the context where the
/// macro was invoked. This defaults to the contract name converted into snake
/// case.
/// - `methods`: A list of mappings from method signatures to method names
/// allowing methods names to be explicitely set for contract methods. This
/// also provides a workaround for generating code for contracts with multiple
/// methods with the same name.
/// - `event_derives`: A list of additional derives that should be added to
/// contract event structs and enums.
///
/// Additionally, the ABI source can be preceeded by a visibility modifier such
/// as `pub` or `pub(crate)`. This visibility modifier is applied to both the
/// generated module and contract re-export. If no visibility modifier is
/// provided, then none is used for the generated code as well, making the
/// module and contract private to the scope where the macro was invoked.
///
/// ```ignore
/// ethcontract::contract!(
/// pub(crate) "build/contracts/MyContract.json",
/// crate = ethcontract_rename,
/// mod = my_contract_instance,
/// contract = MyContractInstance,
/// deployments {
/// 4 => "0x000102030405060708090a0b0c0d0e0f10111213",
/// 5777 => "0x0123456789012345678901234567890123456789",
/// },
/// methods {
/// myMethod(uint256,bool) as my_renamed_method;
/// },
/// event_derives (serde::Deserialize, serde::Serialize),
/// );
/// ```
///
/// See [`ethcontract`](ethcontract) module level documentation for additional
/// information.
#[proc_macro]
pub fn contract(input: TokenStream) -> TokenStream {
let args = parse_macro_input!(input as Spanned<ContractArgs>);
let span = args.span();
expand(args.into_inner())
.unwrap_or_else(|e| SynError::new(span, format!("{:?}", e)).to_compile_error())
.into()
}
fn expand(args: ContractArgs) -> Result<TokenStream2, Box<dyn Error>> {
Ok(args.into_builder()?.generate()?.into_tokens())
}
/// Contract procedural macro arguments.
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
struct ContractArgs {
visibility: Option<String>,
artifact_path: String,
parameters: Vec<Parameter>,
}
impl ContractArgs {
fn into_builder(self) -> Result<Builder, Box<dyn Error>> {
let mut builder = Builder::from_source_url(&self.artifact_path)?
.with_visibility_modifier(self.visibility);
for parameter in self.parameters.into_iter() {
builder = match parameter {
Parameter::Mod(name) => builder.with_contract_mod_override(Some(name)),
Parameter::Contract(name) => builder.with_contract_name_override(Some(name)),
Parameter::Crate(name) => builder.with_runtime_crate_name(name),
Parameter::Deployments(deployments) => {
deployments.into_iter().fold(builder, |builder, d| {
builder.add_deployment(d.network_id, d.address)
})
}
Parameter::Methods(methods) => methods.into_iter().fold(builder, |builder, m| {
builder.add_method_alias(m.signature, m.alias)
}),
Parameter::EventDerives(derives) => derives
.into_iter()
.fold(builder, |builder, derive| builder.add_event_derive(derive)),
};
}
Ok(builder)
}
}
impl ParseInner for ContractArgs {
fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> {
let visibility = match input.parse::<Visibility>()? {
Visibility::Inherited => None,
token => Some(quote!(#token).to_string()),
};
// TODO(nlordell): Due to limitation with the proc-macro Span API, we
// can't currently get a path the the file where we were called from;
// therefore, the path will always be rooted on the cargo manifest
// directory. Eventually we can use the `Span::source_file` API to
// have a better experience.
let (span, artifact_path) = {
let literal = input.parse::<LitStr>()?;
(literal.span(), literal.value())
};
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
let parameters = input
.parse_terminated::<_, Token![,]>(Parameter::parse)?
.into_iter()
.collect();
Ok((
span,
ContractArgs {
visibility,
artifact_path,
parameters,
},
))
}
}
/// A single procedural macro parameter.
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
enum Parameter {
Mod(String),
Contract(String),
Crate(String),
Deployments(Vec<Deployment>),
Methods(Vec<Method>),
EventDerives(Vec<String>),
}
impl Parse for Parameter {
fn parse(input: ParseStream) -> ParseResult<Self> {
let name = input.call(Ident::parse_any)?;
let param = match name.to_string().as_str() {
"crate" => {
input.parse::<Token![=]>()?;
let name = input.call(Ident::parse_any)?.to_string();
Parameter::Crate(name)
}
"mod" => {
input.parse::<Token![=]>()?;
let name = input.parse::<Ident>()?.to_string();
Parameter::Mod(name)
}
"contract" => {
input.parse::<Token![=]>()?;
let name = input.parse::<Ident>()?.to_string();
Parameter::Contract(name)
}
"deployments" => {
let content;
braced!(content in input);
let deployments = {
let parsed =
content.parse_terminated::<_, Token![,]>(Spanned::<Deployment>::parse)?;
let mut deployments = Vec::with_capacity(parsed.len());
let mut networks = HashSet::new();
for deployment in parsed {
if !networks.insert(deployment.network_id) {
return Err(ParseError::new(
deployment.span(),
"duplicate network ID in `ethcontract::contract!` macro invocation",
));
}
deployments.push(deployment.into_inner())
}
deployments
};
Parameter::Deployments(deployments)
}
"methods" => {
let content;
braced!(content in input);
let methods = {
let parsed =
content.parse_terminated::<_, Token![;]>(Spanned::<Method>::parse)?;
let mut methods = Vec::with_capacity(parsed.len());
let mut signatures = HashSet::new();
let mut aliases = HashSet::new();
for method in parsed {
if !signatures.insert(method.signature.clone()) {
return Err(ParseError::new(
method.span(),
"duplicate method signature in `ethcontract::contract!` macro invocation",
));
}
if !aliases.insert(method.alias.clone()) {
return Err(ParseError::new(
method.span(),
"duplicate method alias in `ethcontract::contract!` macro invocation",
));
}
methods.push(method.into_inner())
}
methods
};
Parameter::Methods(methods)
}
"event_derives" => {
let content;
parenthesized!(content in input);
let derives = content
.parse_terminated::<_, Token![,]>(Path::parse)?
.into_iter()
.map(|path| path.to_token_stream().to_string())
.collect();
Parameter::EventDerives(derives)
}
_ => {
return Err(ParseError::new(
name.span(),
format!("unexpected named parameter `{}`", name),
))
}
};
Ok(param)
}
}
/// A manually specified dependency.
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
struct Deployment {
network_id: u32,
address: Address,
}
impl Parse for Deployment {
fn parse(input: ParseStream) -> ParseResult<Self> {
let network_id = input.parse::<LitInt>()?.base10_parse()?;
input.parse::<Token![=>]>()?;
let address = {
let literal = input.parse::<LitStr>()?;
parse_address(&literal.value()).map_err(|err| ParseError::new(literal.span(), err))?
};
Ok(Deployment {
network_id,
address,
})
}
}
/// An explicitely named contract method.
#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
struct Method {
signature: String,
alias: String,
}
impl Parse for Method {
fn parse(input: ParseStream) -> ParseResult<Self> {
let function = {
let name = input.parse::<Ident>()?.to_string();
let content;
parenthesized!(content in input);
let inputs = content
.parse_terminated::<_, Token![,]>(Ident::parse)?
.iter()
.map(|ident| {
let kind = ParamType::from_str(&ident.to_string())
.map_err(|err| ParseError::new(ident.span(), err))?;
Ok(Param {
name: "".into(),
kind,
})
})
.collect::<ParseResult<Vec<_>>>()?;
Function {
name,
inputs,
// NOTE: The output types and const-ness of the function do not
// affect its signature.
outputs: vec![],
constant: false,
}
};
let signature = function.abi_signature();
input.parse::<Token![as]>()?;
let alias = {
let ident = input.parse::<Ident>()?;
ident.to_string()
};
Ok(Method { signature, alias })
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! contract_args_result {
($($arg:tt)*) => {{
use syn::parse::Parser;
<Spanned<ContractArgs> as Parse>::parse
.parse2(quote::quote! { $($arg)* })
}};
}
macro_rules! contract_args {
($($arg:tt)*) => {
contract_args_result!($($arg)*)
.expect("failed to parse contract args")
.into_inner()
};
}
macro_rules! contract_args_err {
($($arg:tt)*) => {
contract_args_result!($($arg)*)
.expect_err("expected parse contract args to error")
};
}
fn deployment(network_id: u32, address: &str) -> Deployment {
Deployment {
network_id,
address: parse_address(address).expect("failed to parse deployment address"),
}
}
fn method(signature: &str, alias: &str) -> Method {
Method {
signature: signature.into(),
alias: alias.into(),
}
}
#[test]
fn parse_contract_args() {
let args = contract_args!("path/to/artifact.json");
assert_eq!(args.artifact_path, "path/to/artifact.json");
}
#[test]
fn crate_parameter_accepts_keywords() {
let args = contract_args!("artifact.json", crate = crate);
assert_eq!(args.parameters, &[Parameter::Crate("crate".into())]);
}
#[test]
fn parse_contract_args_with_defaults() {
let args = contract_args!("artifact.json");
assert_eq!(
args,
ContractArgs {
visibility: None,
artifact_path: "artifact.json".into(),
parameters: vec![],
},
);
}
#[test]
fn parse_contract_args_with_parameters() {
let args = contract_args!(
pub(crate) "artifact.json",
crate = foobar,
mod = contract,
contract = Contract,
deployments {
1 => "0x000102030405060708090a0b0c0d0e0f10111213",
4 => "0x0123456789012345678901234567890123456789",
},
methods {
myMethod(uint256, bool) as my_renamed_method;
myOtherMethod() as my_other_renamed_method;
},
event_derives (Asdf, a::B, a::b::c::D)
);
assert_eq!(
args,
ContractArgs {
visibility: Some(quote!(pub(crate)).to_string()),
artifact_path: "artifact.json".into(),
parameters: vec![
Parameter::Crate("foobar".into()),
Parameter::Mod("contract".into()),
Parameter::Contract("Contract".into()),
Parameter::Deployments(vec![
deployment(1, "0x000102030405060708090a0b0c0d0e0f10111213"),
deployment(4, "0x0123456789012345678901234567890123456789"),
]),
Parameter::Methods(vec![
method("myMethod(uint256,bool)", "my_renamed_method"),
method("myOtherMethod()", "my_other_renamed_method"),
]),
Parameter::EventDerives(vec![
"Asdf".into(),
"a :: B".into(),
"a :: b :: c :: D".into()
])
],
},
);
}
#[test]
fn duplicate_network_id_error() {
contract_args_err!(
"artifact.json",
deployments {
1 => "0x000102030405060708090a0b0c0d0e0f10111213",
1 => "0x0123456789012345678901234567890123456789",
}
);
}
#[test]
fn duplicate_method_rename_error() {
contract_args_err!(
"artifact.json",
methods {
myMethod(uint256) as my_method_1;
myMethod(uint256) as my_method_2;
}
);
contract_args_err!(
"artifact.json",
methods {
myMethod1(uint256) as my_method;
myMethod2(uint256) as my_method;
}
);
}
#[test]
fn method_invalid_method_parameter_type() {
contract_args_err!(
"artifact.json",
methods {
myMethod(invalid) as my_method;
}
);
}
}

View File

@ -0,0 +1,50 @@
//! Provides implementation for helpers used in parsing `TokenStream`s where the
//! data ultimately does not care about its `Span` information, but it is useful
//! during intermediate processing.
use proc_macro2::Span;
use std::ops::Deref;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
/// Trait that abstracts functionality for inner data that can be parsed and
/// wrapped with a specific `Span`.
pub trait ParseInner: Sized {
fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)>;
}
impl<T: Parse> ParseInner for T {
fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> {
Ok((input.span(), T::parse(input)?))
}
}
impl<T: ParseInner> Parse for Spanned<T> {
fn parse(input: ParseStream) -> ParseResult<Self> {
let (span, value) = T::spanned_parse(input)?;
Ok(Spanned(span, value))
}
}
/// A struct that captures `Span` information for inner parsable data.
#[cfg_attr(test, derive(Clone, Debug))]
pub struct Spanned<T>(Span, T);
impl<T> Spanned<T> {
/// Retrieves the captured `Span` information for the parsed data.
pub fn span(&self) -> Span {
self.0
}
/// Retrieves the inner data.
pub fn into_inner(self) -> T {
self.1
}
}
impl<T> Deref for Spanned<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.1
}
}

View File

@ -0,0 +1,178 @@
#![deny(missing_docs)]
//! Crate for generating type-safe bindings to Ethereum smart contracts. This
//! crate is intended to be used either indirectly with the `ethcontract`
//! crate's `contract` procedural macro or directly from a build script.
mod common;
mod deployment;
mod events;
mod methods;
mod types;
use crate::util;
use crate::Args;
use anyhow::{anyhow, Context as _, Result};
use ethcontract_common::{Address, Artifact};
use inflector::Inflector;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::quote;
use std::collections::HashMap;
use syn::{Path, Visibility};
/// Internal shared context for generating smart contract bindings.
pub(crate) struct Context {
/// The artifact JSON as string literal.
artifact_json: Literal,
/// The parsed artifact.
artifact: Artifact,
/// The identifier for the runtime crate. Usually this is `ethcontract` but
/// it can be different if the crate was renamed in the Cargo manifest for
/// example.
runtime_crate: Ident,
/// The visibility for the generated module and re-exported contract type.
visibility: Visibility,
/// The name of the module as an identifier in which to place the contract
/// implementation. Note that the main contract type gets re-exported in the
/// root.
contract_mod: Ident,
/// The contract name as an identifier.
contract_name: Ident,
/// Additional contract deployments.
deployments: HashMap<u32, Address>,
/// Manually specified method aliases.
method_aliases: HashMap<String, Ident>,
/// Derives added to event structs and enums.
event_derives: Vec<Path>,
}
impl Context {
/// Create a context from the code generation arguments.
fn from_args(args: Args) -> Result<Self> {
let (artifact_json, artifact) = {
let artifact_json = args
.artifact_source
.artifact_json()
.context("failed to get artifact JSON")?;
let artifact = Artifact::from_json(&artifact_json)
.with_context(|| format!("invalid artifact JSON '{}'", artifact_json))
.with_context(|| {
format!(
"failed to parse artifact from source {:?}",
args.artifact_source,
)
})?;
(Literal::string(&artifact_json), artifact)
};
let raw_contract_name = if let Some(name) = args.contract_name_override.as_ref() {
name
} else if !artifact.contract_name.is_empty() {
&artifact.contract_name
} else {
return Err(anyhow!(
"contract artifact is missing a name, this can happen when \
using a source that does not provide a contract name such as \
Etherscan; in this case the contract must be manually \
specified"
));
};
let runtime_crate = util::ident(&args.runtime_crate_name);
let visibility = match args.visibility_modifier.as_ref() {
Some(vis) => syn::parse_str(vis)?,
None => Visibility::Inherited,
};
let contract_mod = if let Some(name) = args.contract_mod_override.as_ref() {
util::ident(name)
} else {
util::ident(&raw_contract_name.to_snake_case())
};
let contract_name = util::ident(raw_contract_name);
// NOTE: We only check for duplicate signatures here, since if there are
// duplicate aliases, the compiler will produce a warning because a
// method will be re-defined.
let mut method_aliases = HashMap::new();
for (signature, alias) in args.method_aliases.into_iter() {
let alias = syn::parse_str(&alias)?;
if method_aliases.insert(signature.clone(), alias).is_some() {
return Err(anyhow!(
"duplicate method signature '{}' in method aliases",
signature,
));
}
}
let event_derives = args
.event_derives
.iter()
.map(|derive| syn::parse_str::<Path>(derive))
.collect::<Result<Vec<_>, _>>()
.context("failed to parse event derives")?;
Ok(Context {
artifact_json,
artifact,
runtime_crate,
visibility,
contract_mod,
contract_name,
deployments: args.deployments,
method_aliases,
event_derives,
})
}
}
#[cfg(test)]
impl Default for Context {
fn default() -> Self {
Context {
artifact_json: Literal::string("{}"),
artifact: Artifact::empty(),
runtime_crate: util::ident("ethcontract"),
visibility: Visibility::Inherited,
contract_mod: util::ident("contract"),
contract_name: util::ident("Contract"),
deployments: HashMap::new(),
method_aliases: HashMap::new(),
event_derives: Vec::new(),
}
}
}
pub(crate) fn expand(args: Args) -> Result<TokenStream> {
let cx = Context::from_args(args)?;
let contract = expand_contract(&cx).context("error expanding contract from its ABI")?;
Ok(contract)
}
fn expand_contract(cx: &Context) -> Result<TokenStream> {
let runtime_crate = &cx.runtime_crate;
let vis = &cx.visibility;
let contract_mod = &cx.contract_mod;
let contract_name = &cx.contract_name;
let common = common::expand(cx);
let deployment = deployment::expand(cx)?;
let methods = methods::expand(cx)?;
let events = events::expand(cx)?;
Ok(quote! {
#[allow(dead_code)]
#vis mod #contract_mod {
#[rustfmt::skip]
use #runtime_crate as ethcontract;
#common
#deployment
#methods
#events
}
#vis use self::#contract_mod::Contract as #contract_name;
})
}

View File

@ -0,0 +1,200 @@
use crate::contract::Context;
use crate::util::expand_doc;
use ethcontract_common::Address;
use proc_macro2::{Literal, TokenStream};
use quote::quote;
pub(crate) fn expand(cx: &Context) -> TokenStream {
let artifact_json = &cx.artifact_json;
let contract_name = &cx.contract_name;
let doc_str = cx
.artifact
.devdoc
.details
.as_deref()
.unwrap_or("Generated by `ethcontract`");
let doc = expand_doc(doc_str);
let deployments = cx.deployments.iter().map(|(network_id, address)| {
let network_id = Literal::string(&network_id.to_string());
let address = expand_address(*address);
quote! {
artifact.networks.insert(
#network_id.to_owned(),
self::ethcontract::common::truffle::Network {
address: #address,
transaction_hash: None,
},
);
}
});
quote! {
#doc
#[derive(Clone)]
pub struct Contract {
methods: Methods,
}
impl Contract {
/// Retrieves the truffle artifact used to generate the type safe
/// API for this contract.
pub fn artifact() -> &'static self::ethcontract::Artifact {
use self::ethcontract::private::lazy_static;
use self::ethcontract::Artifact;
lazy_static! {
pub static ref ARTIFACT: Artifact = {
#[allow(unused_mut)]
let mut artifact = Artifact::from_json(#artifact_json)
.expect("valid artifact JSON");
#( #deployments )*
artifact
};
}
&ARTIFACT
}
/// Creates a new contract instance with the specified `web3`
/// provider at the given `Address`.
///
/// Note that this does not verify that a contract with a maching
/// `Abi` is actually deployed at the given address.
pub fn at<F, T>(
web3: &self::ethcontract::web3::api::Web3<T>,
address: self::ethcontract::Address,
) -> Self
where
F: self::ethcontract::web3::futures::Future<
Item = self::ethcontract::json::Value,
Error = self::ethcontract::web3::Error,
> + Send + 'static,
T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
{
Contract::with_transaction(web3, address, None)
}
/// Creates a new contract instance with the specified `web3` provider with
/// the given `Abi` at the given `Address` and an optional transaction hash.
/// This hash is used to retrieve contract related information such as the
/// creation block (which is useful for fetching all historic events).
///
/// Note that this does not verify that a contract with a matching `Abi` is
/// actually deployed at the given address nor that the transaction hash,
/// when provided, is actually for this contract deployment.
pub fn with_transaction<F, T>(
web3: &self::ethcontract::web3::api::Web3<T>,
address: self::ethcontract::Address,
transaction_hash: Option<self::ethcontract::H256>,
) -> Self
where
F: self::ethcontract::web3::futures::Future<
Item = self::ethcontract::json::Value,
Error = self::ethcontract::web3::Error,
> + Send + 'static,
T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
{
use self::ethcontract::Instance;
use self::ethcontract::transport::DynTransport;
use self::ethcontract::web3::api::Web3;
let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let abi = Self::artifact().abi.clone();
let instance = Instance::with_transaction(web3, abi, address, transaction_hash);
Contract::from_raw(instance)
}
/// Creates a contract from a raw instance.
fn from_raw(instance: self::ethcontract::dyns::DynInstance) -> Self {
let methods = Methods { instance };
Contract { methods }
}
/// Returns the contract address being used by this instance.
pub fn address(&self) -> self::ethcontract::Address {
self.raw_instance().address()
}
/// Returns the hash for the transaction that deployed the contract
/// if it is known, `None` otherwise.
pub fn transaction_hash(&self) -> Option<self::ethcontract::H256> {
self.raw_instance().transaction_hash()
}
/// Returns a reference to the default method options used by this
/// contract.
pub fn defaults(&self) -> &self::ethcontract::contract::MethodDefaults {
&self.raw_instance().defaults
}
/// Returns a mutable reference to the default method options used
/// by this contract.
pub fn defaults_mut(&mut self) -> &mut self::ethcontract::contract::MethodDefaults {
&mut self.raw_instance_mut().defaults
}
/// Returns a reference to the raw runtime instance used by this
/// contract.
pub fn raw_instance(&self) -> &self::ethcontract::dyns::DynInstance {
&self.methods.instance
}
/// Returns a mutable reference to the raw runtime instance used by
/// this contract.
fn raw_instance_mut(&mut self) -> &mut self::ethcontract::dyns::DynInstance {
&mut self.methods.instance
}
}
impl std::fmt::Debug for Contract {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple(stringify!(#contract_name))
.field(&self.address())
.finish()
}
}
}
}
/// Expands an `Address` into a literal representation that can be used with
/// quasi-quoting for code generation.
fn expand_address(address: Address) -> TokenStream {
let bytes = address
.as_bytes()
.iter()
.copied()
.map(Literal::u8_unsuffixed);
quote! {
self::ethcontract::H160([#( #bytes ),*])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[rustfmt::skip]
fn expand_address_value() {
assert_quote!(
expand_address(Address::zero()),
{
self::ethcontract::H160([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ])
},
);
assert_quote!(
expand_address("000102030405060708090a0b0c0d0e0f10111213".parse().unwrap()),
{
self::ethcontract::H160([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
},
);
}
}

View File

@ -0,0 +1,167 @@
use crate::contract::{methods, Context};
use crate::util;
use anyhow::{Context as _, Result};
use inflector::Inflector;
use proc_macro2::{Literal, TokenStream};
use quote::quote;
pub(crate) fn expand(cx: &Context) -> Result<TokenStream> {
let deployed = expand_deployed(&cx);
let deploy =
expand_deploy(&cx).context("error generating contract `deploy` associated function")?;
Ok(quote! {
#deployed
#deploy
})
}
fn expand_deployed(cx: &Context) -> TokenStream {
if cx.artifact.networks.is_empty() && cx.deployments.is_empty() {
return quote! {};
}
quote! {
impl Contract {
/// Locates a deployed contract based on the current network ID
/// reported by the `web3` provider.
///
/// Note that this does not verify that a contract with a maching
/// `Abi` is actually deployed at the given address.
pub async fn deployed<F, T>(
web3: &self::ethcontract::web3::api::Web3<T>,
) -> Result<Self, self::ethcontract::errors::DeployError>
where
F: self::ethcontract::web3::futures::Future<
Item = self::ethcontract::json::Value,
Error = self::ethcontract::web3::Error
> + Send + 'static,
T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
{
use self::ethcontract::{Instance, Web3};
use self::ethcontract::transport::DynTransport;
let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let instance = Instance::deployed(web3, Contract::artifact().clone()).await?;
Ok(Contract::from_raw(instance))
}
}
}
}
fn expand_deploy(cx: &Context) -> Result<TokenStream> {
if cx.artifact.bytecode.is_empty() {
// do not generate deploy method for contracts that have empty bytecode
return Ok(quote! {});
}
// TODO(nlordell): not sure how contructor documentation get generated as I
// can't seem to get truffle to output it
let doc = util::expand_doc("Generated by `ethcontract`");
let (input, arg) = match cx.artifact.abi.constructor() {
Some(contructor) => (
methods::expand_inputs(&contructor.inputs)?,
methods::expand_inputs_call_arg(&contructor.inputs),
),
None => (quote! {}, quote! {()}),
};
let libs: Vec<_> = cx
.artifact
.bytecode
.undefined_libraries()
.map(|name| (name, util::safe_ident(&name.to_snake_case())))
.collect();
let (lib_struct, lib_input, link) = if !libs.is_empty() {
let lib_struct = {
let lib_struct_fields = libs.iter().map(|(name, field)| {
let doc = util::expand_doc(&format!("Address of the `{}` library.", name));
quote! {
#doc pub #field: self::ethcontract::Address
}
});
quote! {
/// Undefinied libraries in the contract bytecode that are
/// required for linking in order to deploy.
pub struct Libraries {
#( #lib_struct_fields, )*
}
}
};
let link = {
let link_libraries = libs.iter().map(|(name, field)| {
let name_lit = Literal::string(&name);
quote! {
bytecode.link(#name_lit, libs.#field).expect("valid library");
}
});
quote! {
let mut bytecode = bytecode;
#( #link_libraries )*
}
};
(lib_struct, quote! { , libs: Libraries }, link)
} else {
Default::default()
};
Ok(quote! {
#lib_struct
impl Contract {
#doc
pub fn builder<F, T>(
web3: &self::ethcontract::web3::api::Web3<T> #lib_input #input ,
) -> self::ethcontract::dyns::DynDeployBuilder<Self>
where
F: self::ethcontract::web3::futures::Future<
Item = self::ethcontract::json::Value,
Error = self::ethcontract::web3::Error,
> + Send + 'static,
T: self::ethcontract::web3::Transport<Out = F> + Send + Sync + 'static,
{
use self::ethcontract::dyns::DynTransport;
use self::ethcontract::contract::DeployBuilder;
use self::ethcontract::web3::api::Web3;
let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let bytecode = Self::artifact().bytecode.clone();
#link
DeployBuilder::new(web3, bytecode, #arg).expect("valid deployment args")
}
}
impl self::ethcontract::contract::Deploy<self::ethcontract::dyns::DynTransport> for Contract {
type Context = self::ethcontract::common::Bytecode;
fn bytecode(cx: &Self::Context) -> &self::ethcontract::common::Bytecode {
cx
}
fn abi(_: &Self::Context) -> &self::ethcontract::common::Abi {
&Self::artifact().abi
}
fn from_deployment(
web3: self::ethcontract::dyns::DynWeb3,
address: self::ethcontract::Address,
transaction_hash: self::ethcontract::H256,
_: Self::Context,
) -> Self {
Self::with_transaction(&web3, address, Some(transaction_hash))
}
}
})
}

View File

@ -0,0 +1,855 @@
use crate::contract::{types, Context};
use crate::util;
use anyhow::Result;
use ethcontract_common::abi::{Event, EventParam, Hash, ParamType};
use ethcontract_common::abiext::EventExt;
use inflector::Inflector;
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use syn::Path;
pub(crate) fn expand(cx: &Context) -> Result<TokenStream> {
let structs_mod = expand_structs_mod(cx)?;
let filters = expand_filters(cx)?;
let all_events = expand_all_events(cx);
Ok(quote! {
#structs_mod
#filters
#all_events
})
}
/// Expands into a module containing all the event data structures from the ABI.
fn expand_structs_mod(cx: &Context) -> Result<TokenStream> {
let data_types = cx
.artifact
.abi
.events()
.map(|event| expand_data_type(event, &cx.event_derives))
.collect::<Result<Vec<_>>>()?;
if data_types.is_empty() {
return Ok(quote! {});
}
Ok(quote! {
/// Module containing all generated data models for this contract's
/// events.
pub mod event_data {
use super::ethcontract;
#( #data_types )*
}
})
}
fn expand_derives(derives: &[Path]) -> TokenStream {
quote! {#(#derives),*}
}
/// Expands an ABI event into a single event data type. This can expand either
/// into a structure or a tuple in the case where all event parameters (topics
/// and data) are anonymous.
fn expand_data_type(event: &Event, event_derives: &[Path]) -> Result<TokenStream> {
let event_name = expand_struct_name(event);
let signature = expand_hash(event.signature());
let abi_signature = event.abi_signature();
let abi_signature_lit = Literal::string(&abi_signature);
let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature));
let params = expand_params(event)?;
let all_anonymous_fields = event.inputs.iter().all(|input| input.name.is_empty());
let (data_type_definition, data_type_construction) = if all_anonymous_fields {
expand_data_tuple(&event_name, &params)
} else {
expand_data_struct(&event_name, &params)
};
let params_len = Literal::usize_unsuffixed(params.len());
let read_param_token = params
.iter()
.map(|(name, ty)| {
quote! {
let #name = <#ty as self::ethcontract::web3::contract::tokens::Tokenizable>
::from_token(tokens.next().unwrap())?;
}
})
.collect::<Vec<_>>();
let derives = expand_derives(event_derives);
Ok(quote! {
#[derive(Clone, Debug, Default, Eq, PartialEq, #derives)]
pub #data_type_definition
impl #event_name {
/// Retrieves the signature for the event this data corresponds to.
/// This signature is the Keccak-256 hash of the ABI signature of
/// this event.
pub fn signature() -> self::ethcontract::H256 {
#signature
}
/// Retrieves the ABI signature for the event this data corresponds
/// to. For this event the value should always be:
///
#abi_signature_doc
pub fn abi_signature() -> &'static str {
#abi_signature_lit
}
}
impl self::ethcontract::web3::contract::tokens::Detokenize for #event_name {
fn from_tokens(
tokens: Vec<self::ethcontract::common::abi::Token>,
) -> Result<Self, self::ethcontract::web3::contract::Error> {
if tokens.len() != #params_len {
return Err(self::ethcontract::web3::contract::Error::InvalidOutputType(format!(
"Expected {} tokens, got {}: {:?}",
#params_len,
tokens.len(),
tokens
)));
}
#[allow(unused_mut)]
let mut tokens = tokens.into_iter();
#( #read_param_token )*
Ok(#data_type_construction)
}
}
})
}
/// Expands an ABI event into an identifier for its event data type.
fn expand_struct_name(event: &Event) -> TokenStream {
let event_name = util::ident(&event.name.to_pascal_case());
quote! { #event_name }
}
/// Expands an ABI event into name-type pairs for each of its parameters.
fn expand_params(event: &Event) -> Result<Vec<(TokenStream, TokenStream)>> {
event
.inputs
.iter()
.enumerate()
.map(|(i, input)| {
// NOTE: Events can contain nameless values.
let name = util::expand_input_name(i, &input.name);
let ty = expand_input_type(&input)?;
Ok((name, ty))
})
.collect()
}
/// Expands an event data structure from its name-type parameter pairs. Returns
/// a tuple with the type definition (i.e. the struct declaration) and
/// construction (i.e. code for creating an instance of the event data).
fn expand_data_struct(
name: &TokenStream,
params: &[(TokenStream, TokenStream)],
) -> (TokenStream, TokenStream) {
let fields = params
.iter()
.map(|(name, ty)| quote! { pub #name: #ty })
.collect::<Vec<_>>();
let param_names = params
.iter()
.map(|(name, _)| name)
.cloned()
.collect::<Vec<_>>();
let definition = quote! { struct #name { #( #fields, )* } };
let construction = quote! { #name { #( #param_names ),* } };
(definition, construction)
}
/// Expands an event data named tuple from its name-type parameter pairs.
/// Returns a tuple with the type definition and construction.
fn expand_data_tuple(
name: &TokenStream,
params: &[(TokenStream, TokenStream)],
) -> (TokenStream, TokenStream) {
let fields = params
.iter()
.map(|(_, ty)| quote! { pub #ty })
.collect::<Vec<_>>();
let param_names = params
.iter()
.map(|(name, _)| name)
.cloned()
.collect::<Vec<_>>();
let definition = quote! { struct #name( #( #fields ),* ); };
let construction = quote! { #name( #( #param_names ),* ) };
(definition, construction)
}
/// Expands into an `Events` type with method definitions for creating event
/// streams for all non-anonymous contract events in the ABI.
fn expand_filters(cx: &Context) -> Result<TokenStream> {
let standard_events = cx
.artifact
.abi
.events()
.filter(|event| !event.anonymous)
.collect::<Vec<_>>();
if standard_events.is_empty() {
return Ok(quote! {});
}
let filters = standard_events
.iter()
.map(|event| expand_filter(event))
.collect::<Result<Vec<_>>>()?;
let builders = standard_events
.iter()
.map(|event| expand_builder_type(event))
.collect::<Result<Vec<_>>>()?;
Ok(quote! {
impl Contract {
/// Retrieves a handle to a type containing for creating event
/// streams for all the contract events.
pub fn events(&self) -> Events<'_> {
Events {
instance: self.raw_instance(),
}
}
}
pub struct Events<'a> {
instance: &'a self::ethcontract::dyns::DynInstance,
}
impl Events<'_> {
#( #filters )*
}
/// Module containing the generated event stream builders with type safe
/// filter methods for this contract's events.
pub mod event_builders {
use super::ethcontract;
use super::event_data;
#( #builders )*
}
})
}
/// Expands into a single method for contracting an event stream.
fn expand_filter(event: &Event) -> Result<TokenStream> {
let name = util::safe_ident(&event.name.to_snake_case());
let builder_name = expand_builder_name(event);
let signature = expand_hash(event.signature());
Ok(quote! {
/// Generated by `ethcontract`.
pub fn #name(&self) -> self::event_builders::#builder_name {
self::event_builders::#builder_name(
self.instance.event(#signature)
.expect("generated event filter"),
)
}
})
}
/// Expands an ABI event into a wrapped `EventBuilder` type with type-safe
/// filter methods.
fn expand_builder_type(event: &Event) -> Result<TokenStream> {
let event_name = expand_struct_name(event);
let builder_doc = util::expand_doc(&format!(
"A builder for creating a filtered stream of `{}` events.",
event_name
));
let builder_name = expand_builder_name(event);
let topic_filters = expand_builder_topic_filters(event)?;
Ok(quote! {
#builder_doc
pub struct #builder_name(
/// The inner event builder.
pub self::ethcontract::dyns::DynEventBuilder<self::event_data::#event_name>,
);
impl #builder_name {
/// Sets the starting block from which to stream logs for.
///
/// If left unset defaults to the latest block.
#[allow(clippy::wrong_self_convention)]
pub fn from_block(mut self, block: self::ethcontract::BlockNumber) -> Self {
self.0 = (self.0).from_block(block);
self
}
/// Sets the last block from which to stream logs for.
///
/// If left unset defaults to the streaming until the end of days.
#[allow(clippy::wrong_self_convention)]
pub fn to_block(mut self, block: self::ethcontract::BlockNumber) -> Self {
self.0 = (self.0).to_block(block);
self
}
/// Limit the number of events that can be retrieved by this filter.
///
/// Note that this parameter is non-standard.
pub fn limit(mut self, value: usize) -> Self {
self.0 = (self.0).limit(value);
self
}
/// The polling interval. This is used as the interval between
/// consecutive `eth_getFilterChanges` calls to get filter updates.
pub fn poll_interval(mut self, value: std::time::Duration) -> Self {
self.0 = (self.0).poll_interval(value);
self
}
#topic_filters
/// Returns a future that resolves with a collection of all existing
/// logs matching the builder parameters.
pub async fn query(self) -> std::result::Result<
std::vec::Vec<self::ethcontract::Event<self::event_data::#event_name>>,
self::ethcontract::errors::EventError,
> {
(self.0).query().await
}
/// Creates an event stream from the current event builder.
pub fn stream(self) -> impl self::ethcontract::futures::stream::Stream<
Item = std::result::Result<
self::ethcontract::StreamEvent<self::event_data::#event_name>,
self::ethcontract::errors::EventError,
>,
> {
(self.0).stream()
}
}
})
}
/// Expands an ABI event into filter methods for its indexed parameters.
fn expand_builder_topic_filters(event: &Event) -> Result<TokenStream> {
let topic_filters = event
.inputs
.iter()
.filter(|input| input.indexed)
.enumerate()
.map(|(topic_index, input)| expand_builder_topic_filter(topic_index, input))
.collect::<Result<Vec<_>>>()?;
Ok(quote! {
#( #topic_filters )*
})
}
/// Expands a event parameter into an event builder filter method for the
/// specified topic index.
fn expand_builder_topic_filter(topic_index: usize, param: &EventParam) -> Result<TokenStream> {
let doc = util::expand_doc(&format!(
"Adds a filter for the {} event parameter.",
param.name,
));
let topic = util::ident(&format!("topic{}", topic_index));
let name = if param.name.is_empty() {
topic.clone()
} else {
util::safe_ident(&param.name.to_snake_case())
};
let ty = expand_input_type(&param)?;
Ok(quote! {
#doc
pub fn #name(mut self, topic: self::ethcontract::Topic<#ty>) -> Self {
self.0 = (self.0).#topic(topic);
self
}
})
}
/// Expands an ABI event into an identifier for its event data type.
fn expand_builder_name(event: &Event) -> TokenStream {
let builder_name = util::ident(&format!("{}Builder", &event.name.to_pascal_case()));
quote! { #builder_name }
}
/// Expands into the `all_events` method on the root contract type if it
/// contains events. Expands to nothing otherwise.
fn expand_all_events(cx: &Context) -> TokenStream {
if cx.artifact.abi.events.is_empty() {
return quote! {};
}
let event_enum = expand_event_enum(cx);
let event_parse_log = expand_event_parse_log(cx);
quote! {
impl Contract {
/// Returns a log stream with all events.
pub fn all_events(&self) -> self::ethcontract::dyns::DynAllEventsBuilder<Event> {
self::ethcontract::dyns::DynAllEventsBuilder::new(
self.raw_instance().web3(),
self.address(),
self.transaction_hash(),
)
}
}
#event_enum
#event_parse_log
}
}
/// Expands into an enum with one variant for each distinct event type,
/// including anonymous types.
fn expand_event_enum(cx: &Context) -> TokenStream {
let variants = {
let mut events = cx.artifact.abi.events().collect::<Vec<_>>();
// NOTE: We sort the events by name so that the generated enum is
// consistent. This also faciliates testing as so that the same ABI
// yields consistent code.
events.sort_unstable_by_key(|event| &event.name);
events
.into_iter()
.map(|event| {
let struct_name = expand_struct_name(&event);
quote! {
#struct_name(self::event_data::#struct_name)
}
})
.collect::<Vec<_>>()
};
let derives = expand_derives(&cx.event_derives);
quote! {
/// A contract event.
#[derive(Clone, Debug, Eq, PartialEq, #derives)]
pub enum Event {
#( #variants, )*
}
}
}
/// Expands the `ParseLog` implementation for the event enum.
fn expand_event_parse_log(cx: &Context) -> TokenStream {
let all_events = {
let mut all_events = cx
.artifact
.abi
.events()
.map(|event| {
let struct_name = expand_struct_name(&event);
let name = Literal::string(&event.name);
let decode_event = quote! {
log.clone().decode(
&Contract::artifact()
.abi
.event(#name)
.expect("generated event decode")
)
};
(event, struct_name, decode_event)
})
.collect::<Vec<_>>();
// NOTE: We sort the events by name so that the anonymous error decoding
// is consistent. Since the events are stored in a `HashMap`, there is
// no guaranteed order, and in the case where there is ambiguity in
// decoding anonymous events, its nice if they follow some strict and
// predictable order.
all_events.sort_unstable_by_key(|(event, _, _)| &event.name);
all_events
};
let standard_event_match_arms = all_events
.iter()
.filter(|(event, _, _)| !event.anonymous)
.map(|(event, struct_name, decode_event)| {
// These are all possible stardard (i.e. non-anonymous) events that
// the contract can produce, along with its signature and index in
// the contract ABI. For these, we match topic 0 to the signature
// and try to decode.
let signature = expand_hash(event.signature());
quote! {
#signature => Ok(Event::#struct_name(#decode_event?)),
}
})
.collect::<Vec<_>>();
let anonymous_event_try_decode = all_events
.iter()
.filter(|(event, _, _)| event.anonymous)
.map(|(_, struct_name, decode_event)| {
// For anonymous events, just try to decode one at a time and return
// the first that succeeds.
quote! {
if let Ok(data) = #decode_event {
return Ok(Event::#struct_name(data));
}
}
})
.collect::<Vec<_>>();
let invalid_data = expand_invalid_data();
quote! {
impl self::ethcontract::contract::ParseLog for Event {
fn parse_log(
log: self::ethcontract::RawLog,
) -> Result<Self, self::ethcontract::errors::ExecutionError> {
let standard_event = log.topics
.get(0)
.copied()
.map(|topic| match topic {
#( #standard_event_match_arms )*
_ => #invalid_data,
});
if let Some(Ok(data)) = standard_event {
return Ok(data);
}
#( #anonymous_event_try_decode )*
#invalid_data
}
}
}
}
/// Expands an event property type.
///
/// Note that this is slightly different than an expanding a Solidity type as
/// complex types like arrays and strings get emited as hashes when they are
/// indexed.
fn expand_input_type(input: &EventParam) -> Result<TokenStream> {
Ok(match (&input.kind, input.indexed) {
(ParamType::Array(..), true)
| (ParamType::Bytes, true)
| (ParamType::FixedArray(..), true)
| (ParamType::String, true)
| (ParamType::Tuple(..), true) => {
quote! { self::ethcontract::H256 }
}
(kind, _) => types::expand(kind)?,
})
}
/// Expands a 256-bit `Hash` into a literal representation that can be used with
/// quasi-quoting for code generation.
fn expand_hash(hash: Hash) -> TokenStream {
let bytes = hash.as_bytes().iter().copied().map(Literal::u8_unsuffixed);
quote! {
self::ethcontract::H256([#( #bytes ),*])
}
}
/// Expands to a generic `InvalidData` error.
fn expand_invalid_data() -> TokenStream {
quote! {
Err(self::ethcontract::errors::ExecutionError::from(
self::ethcontract::common::abi::Error::InvalidData
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ethcontract_common::abi::{EventParam, ParamType};
#[test]
fn expand_empty_filters() {
assert_quote!(expand_filters(&Context::default()).unwrap(), {});
}
#[test]
fn expand_transfer_filter() {
let event = Event {
name: "Transfer".into(),
inputs: vec![
EventParam {
name: "from".into(),
kind: ParamType::Address,
indexed: true,
},
EventParam {
name: "to".into(),
kind: ParamType::Address,
indexed: true,
},
EventParam {
name: "amount".into(),
kind: ParamType::Uint(256),
indexed: false,
},
],
anonymous: false,
};
let signature = expand_hash(event.signature());
assert_quote!(expand_filter(&event).unwrap(), {
/// Generated by `ethcontract`.
pub fn transfer(&self) -> self::event_builders::TransferBuilder {
self::event_builders::TransferBuilder(
self.instance.event(#signature)
.expect("generated event filter"),
)
}
});
}
#[test]
fn expand_transfer_builder_topic_filters() {
let event = Event {
name: "Transfer".into(),
inputs: vec![
EventParam {
name: "from".into(),
kind: ParamType::Address,
indexed: true,
},
EventParam {
name: "to".into(),
kind: ParamType::Address,
indexed: true,
},
EventParam {
name: "amount".into(),
kind: ParamType::Uint(256),
indexed: false,
},
],
anonymous: false,
};
#[rustfmt::skip]
assert_quote!(expand_builder_topic_filters(&event).unwrap(), {
#[doc = "Adds a filter for the from event parameter."]
pub fn from(mut self, topic: self::ethcontract::Topic<self::ethcontract::Address>) -> Self {
self.0 = (self.0).topic0(topic);
self
}
#[doc = "Adds a filter for the to event parameter."]
pub fn to(mut self, topic: self::ethcontract::Topic<self::ethcontract::Address>) -> Self {
self.0 = (self.0).topic1(topic);
self
}
});
}
#[test]
fn expand_data_struct_value() {
let event = Event {
name: "Foo".into(),
inputs: vec![
EventParam {
name: "a".into(),
kind: ParamType::Bool,
indexed: false,
},
EventParam {
name: String::new(),
kind: ParamType::Address,
indexed: false,
},
],
anonymous: false,
};
let name = expand_struct_name(&event);
let params = expand_params(&event).unwrap();
let (definition, construction) = expand_data_struct(&name, &params);
assert_quote!(definition, {
struct Foo {
pub a: bool,
pub p1: self::ethcontract::Address,
}
});
assert_quote!(construction, { Foo { a, p1 } });
}
#[test]
fn expand_data_tuple_value() {
let event = Event {
name: "Foo".into(),
inputs: vec![
EventParam {
name: String::new(),
kind: ParamType::Bool,
indexed: false,
},
EventParam {
name: String::new(),
kind: ParamType::Address,
indexed: false,
},
],
anonymous: false,
};
let name = expand_struct_name(&event);
let params = expand_params(&event).unwrap();
let (definition, construction) = expand_data_tuple(&name, &params);
assert_quote!(definition, {
struct Foo(pub bool, pub self::ethcontract::Address);
});
assert_quote!(construction, { Foo(p0, p1) });
}
#[test]
fn expand_enum_for_all_events() {
let context = {
let mut context = Context::default();
context.artifact.abi.events.insert(
"Foo".into(),
vec![Event {
name: "Foo".into(),
inputs: vec![EventParam {
name: String::new(),
kind: ParamType::Bool,
indexed: false,
}],
anonymous: false,
}],
);
context.artifact.abi.events.insert(
"Bar".into(),
vec![Event {
name: "Bar".into(),
inputs: vec![EventParam {
name: String::new(),
kind: ParamType::Address,
indexed: false,
}],
anonymous: true,
}],
);
context.event_derives = ["Asdf", "a::B", "a::b::c::D"]
.iter()
.map(|derive| syn::parse_str::<Path>(derive).unwrap())
.collect();
context
};
assert_quote!(expand_event_enum(&context), {
/// A contract event.
#[derive(Clone, Debug, Eq, PartialEq, Asdf, a::B, a::b::c::D)]
pub enum Event {
Bar(self::event_data::Bar),
Foo(self::event_data::Foo),
}
});
}
#[test]
fn expand_parse_log_impl_for_all_events() {
let context = {
let mut context = Context::default();
context.artifact.abi.events.insert(
"Foo".into(),
vec![Event {
name: "Foo".into(),
inputs: vec![EventParam {
name: String::new(),
kind: ParamType::Bool,
indexed: false,
}],
anonymous: false,
}],
);
context.artifact.abi.events.insert(
"Bar".into(),
vec![Event {
name: "Bar".into(),
inputs: vec![EventParam {
name: String::new(),
kind: ParamType::Address,
indexed: false,
}],
anonymous: true,
}],
);
context
};
let foo_signature = expand_hash(context.artifact.abi.event("Foo").unwrap().signature());
let invalid_data = expand_invalid_data();
assert_quote!(expand_event_parse_log(&context), {
impl self::ethcontract::contract::ParseLog for Event {
fn parse_log(
log: self::ethcontract::RawLog,
) -> Result<Self, self::ethcontract::errors::ExecutionError> {
let standard_event = log.topics
.get(0)
.copied()
.map(|topic| match topic {
#foo_signature => Ok(Event::Foo(
log.clone().decode(
&Contract::artifact()
.abi
.event("Foo")
.expect("generated event decode")
)?
)),
_ => #invalid_data,
});
if let Some(Ok(data)) = standard_event {
return Ok(data);
}
if let Ok(data) = log.clone().decode(
&Contract::artifact()
.abi
.event("Bar")
.expect("generated event decode")
) {
return Ok(Event::Bar(data));
}
#invalid_data
}
}
});
}
#[test]
#[rustfmt::skip]
fn expand_hash_value() {
assert_quote!(
expand_hash(
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".parse().unwrap()
),
{
self::ethcontract::H256([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
])
},
);
}
}

View File

@ -0,0 +1,255 @@
use crate::contract::{types, Context};
use crate::util;
use anyhow::{anyhow, Context as _, Result};
use ethcontract_common::abi::{Function, Param};
use ethcontract_common::abiext::FunctionExt;
use ethcontract_common::hash::H32;
use inflector::Inflector;
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use syn::Ident;
pub(crate) fn expand(cx: &Context) -> Result<TokenStream> {
let functions = expand_functions(cx)?;
let fallback = expand_fallback(cx);
Ok(quote! {
#functions
#fallback
})
}
/// Expands a context into a method struct containing all the generated bindings
/// to the Solidity contract methods.
fn expand_functions(cx: &Context) -> Result<TokenStream> {
let mut aliases = cx.method_aliases.clone();
let functions = cx
.artifact
.abi
.functions()
.map(|function| {
let signature = function.abi_signature();
expand_function(&cx, function, aliases.remove(&signature))
.with_context(|| format!("error expanding function '{}'", signature))
})
.collect::<Result<Vec<_>>>()?;
if let Some(unused) = aliases.keys().next() {
return Err(anyhow!(
"a manual method alias for '{}' was specified but this method does not exist",
unused,
));
}
let methods_attrs = quote! { #[derive(Clone)] };
let methods_struct = quote! {
struct Methods {
instance: self::ethcontract::dyns::DynInstance,
}
};
if functions.is_empty() {
// NOTE: The methods struct is still needed when there are no functions
// as it contains the the runtime instance. The code is setup this way
// so that the contract can implement `Deref` targetting the methods
// struct and, therefore, call the methods directly.
return Ok(quote! {
#methods_attrs
#methods_struct
});
}
Ok(quote! {
impl Contract {
/// Retrives a reference to type containing all the generated
/// contract methods. This can be used for methods where the name
/// would collide with a common method (like `at` or `deployed`).
pub fn methods(&self) -> &Methods {
&self.methods
}
}
/// Type containing all contract methods for generated contract type.
#methods_attrs
pub #methods_struct
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
impl Methods {
#( #functions )*
}
impl std::ops::Deref for Contract {
type Target = Methods;
fn deref(&self) -> &Self::Target {
&self.methods
}
}
})
}
fn expand_function(cx: &Context, function: &Function, alias: Option<Ident>) -> Result<TokenStream> {
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case()));
let signature = function.abi_signature();
let selector = expand_selector(function.selector());
let doc_str = cx
.artifact
.devdoc
.methods
.get(&signature)
.or_else(|| cx.artifact.userdoc.methods.get(&signature))
.and_then(|entry| entry.details.as_ref())
.map(String::as_str)
.unwrap_or("Generated by `ethcontract`");
let doc = util::expand_doc(doc_str);
let input = expand_inputs(&function.inputs)?;
let outputs = expand_fn_outputs(&function.outputs)?;
let (method, result_type_name) = if function.constant {
(quote! { view_method }, quote! { DynViewMethodBuilder })
} else {
(quote! { method }, quote! { DynMethodBuilder })
};
let result = quote! { self::ethcontract::dyns::#result_type_name<#outputs> };
let arg = expand_inputs_call_arg(&function.inputs);
Ok(quote! {
#doc
pub fn #name(&self #input) -> #result {
self.instance.#method(#selector, #arg)
.expect("generated call")
}
})
}
pub(crate) fn expand_inputs(inputs: &[Param]) -> Result<TokenStream> {
let params = inputs
.iter()
.enumerate()
.map(|(i, param)| {
let name = util::expand_input_name(i, &param.name);
let kind = types::expand(&param.kind)?;
Ok(quote! { #name: #kind })
})
.collect::<Result<Vec<_>>>()?;
Ok(quote! { #( , #params )* })
}
pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
let names = inputs
.iter()
.enumerate()
.map(|(i, param)| util::expand_input_name(i, &param.name));
quote! { ( #( #names ,)* ) }
}
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
match outputs.len() {
0 => Ok(quote! { self::ethcontract::Void }),
1 => types::expand(&outputs[0].kind),
_ => {
let types = outputs
.iter()
.map(|param| types::expand(&param.kind))
.collect::<Result<Vec<_>>>()?;
Ok(quote! { (#( #types ),*) })
}
}
}
fn expand_selector(selector: H32) -> TokenStream {
let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
quote! { [#( #bytes ),*] }
}
/// Expands a context into fallback method when the contract implements one,
/// and an empty token stream otherwise.
fn expand_fallback(cx: &Context) -> TokenStream {
if cx.artifact.abi.fallback {
quote! {
impl Contract {
/// Returns a method builder to setup a call to a smart
/// contract's fallback function.
pub fn fallback<D>(&self, data: D) -> self::ethcontract::dyns::DynMethodBuilder<
self::ethcontract::Void,
>
where
D: Into<Vec<u8>>,
{
self.raw_instance().fallback(data)
.expect("generated fallback method")
}
}
}
} else {
quote! {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ethcontract_common::abi::ParamType;
#[test]
fn expand_inputs_empty() {
assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},);
}
#[test]
fn expand_inputs_() {
assert_quote!(
expand_inputs(
&[
Param {
name: "a".to_string(),
kind: ParamType::Bool,
},
Param {
name: "b".to_string(),
kind: ParamType::Address,
},
],
)
.unwrap(),
{ , a: bool, b: self::ethcontract::Address },
);
}
#[test]
fn expand_fn_outputs_empty() {
assert_quote!(expand_fn_outputs(&[],).unwrap(), {
self::ethcontract::Void
});
}
#[test]
fn expand_fn_outputs_single() {
assert_quote!(
expand_fn_outputs(&[Param {
name: "a".to_string(),
kind: ParamType::Bool,
}])
.unwrap(),
{ bool },
);
}
#[test]
fn expand_fn_outputs_muliple() {
assert_quote!(
expand_fn_outputs(&[
Param {
name: "a".to_string(),
kind: ParamType::Bool,
},
Param {
name: "b".to_string(),
kind: ParamType::Address,
},
],)
.unwrap(),
{ (bool, self::ethcontract::Address) },
);
}
}

View File

@ -0,0 +1,48 @@
use anyhow::{anyhow, Result};
use ethcontract_common::abi::ParamType;
use proc_macro2::{Literal, TokenStream};
use quote::quote;
pub(crate) fn expand(kind: &ParamType) -> Result<TokenStream> {
match kind {
ParamType::Address => Ok(quote! { self::ethcontract::Address }),
ParamType::Bytes => Ok(quote! { Vec<u8> }),
ParamType::Int(n) => match n / 8 {
1 => Ok(quote! { i8 }),
2 => Ok(quote! { i16 }),
3..=4 => Ok(quote! { i32 }),
5..=8 => Ok(quote! { i64 }),
9..=16 => Ok(quote! { i128 }),
17..=32 => Ok(quote! { self::ethcontract::I256 }),
_ => Err(anyhow!("unsupported solidity type int{}", n)),
},
ParamType::Uint(n) => match n / 8 {
1 => Ok(quote! { u8 }),
2 => Ok(quote! { u16 }),
3..=4 => Ok(quote! { u32 }),
5..=8 => Ok(quote! { u64 }),
9..=16 => Ok(quote! { u128 }),
17..=32 => Ok(quote! { self::ethcontract::U256 }),
_ => Err(anyhow!("unsupported solidity type uint{}", n)),
},
ParamType::Bool => Ok(quote! { bool }),
ParamType::String => Ok(quote! { String }),
ParamType::Array(t) => {
let inner = expand(t)?;
Ok(quote! { Vec<#inner> })
}
ParamType::FixedBytes(n) => {
// TODO(nlordell): what is the performance impact of returning large
// `FixedBytes` and `FixedArray`s with `web3`?
let size = Literal::usize_unsuffixed(*n);
Ok(quote! { [u8; #size] })
}
ParamType::FixedArray(t, n) => {
// TODO(nlordell): see above
let inner = expand(t)?;
let size = Literal::usize_unsuffixed(*n);
Ok(quote! { [#inner; #size] })
}
ParamType::Tuple(_) => Err(anyhow!("ABIEncoderV2 is currently not supported")),
}
}

View File

@ -0,0 +1,286 @@
#![deny(missing_docs, unsafe_code)]
//! Crate for generating type-safe bindings to Ethereum smart contracts. This
//! crate is intended to be used either indirectly with the `ethcontract`
//! crate's `contract` procedural macro or directly from a build script.
#[cfg(test)]
#[allow(missing_docs)]
#[macro_use]
#[path = "test/macros.rs"]
mod test_macros;
mod contract;
mod rustfmt;
mod source;
mod util;
pub use crate::source::Source;
pub use crate::util::parse_address;
use anyhow::Result;
pub use ethcontract_common::Address;
use proc_macro2::TokenStream;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;
/// Internal global arguments passed to the generators for each individual
/// component that control expansion.
pub(crate) struct Args {
/// The source of the truffle artifact JSON for the contract whose bindings
/// are being generated.
artifact_source: Source,
/// The runtime crate name to use.
runtime_crate_name: String,
/// The visibility modifier to use for the generated module and contract
/// re-export.
visibility_modifier: Option<String>,
/// Override the contract module name that contains the generated code.
contract_mod_override: Option<String>,
/// Override the contract name to use for the generated type.
contract_name_override: Option<String>,
/// Manually specified deployed contract addresses.
deployments: HashMap<u32, Address>,
/// Manually specified contract method aliases.
method_aliases: HashMap<String, String>,
/// Derives added to event structs and enums.
event_derives: Vec<String>,
}
impl Args {
/// Creates a new builder given the path to a contract's truffle artifact
/// JSON file.
pub fn new(source: Source) -> Self {
Args {
artifact_source: source,
runtime_crate_name: "ethcontract".to_owned(),
visibility_modifier: None,
contract_mod_override: None,
contract_name_override: None,
deployments: HashMap::new(),
method_aliases: HashMap::new(),
event_derives: Vec::new(),
}
}
}
/// Internal output options for controlling how the generated code gets
/// serialized to file.
struct SerializationOptions {
/// Format the code using a locally installed copy of `rustfmt`.
rustfmt: bool,
}
impl Default for SerializationOptions {
fn default() -> Self {
SerializationOptions { rustfmt: true }
}
}
/// Builder for generating contract code. Note that no code is generated until
/// the builder is finalized with `generate` or `output`.
pub struct Builder {
/// The contract binding generation args.
args: Args,
/// The serialization options.
options: SerializationOptions,
}
impl Builder {
/// Creates a new builder given the path to a contract's truffle artifact
/// JSON file.
pub fn new<P>(artifact_path: P) -> Self
where
P: AsRef<Path>,
{
Builder::with_source(Source::local(artifact_path))
}
/// Creates a new builder from a source URL.
pub fn from_source_url<S>(source_url: S) -> Result<Self>
where
S: AsRef<str>,
{
let source = Source::parse(source_url)?;
Ok(Builder::with_source(source))
}
/// Creates a new builder with the given artifact JSON source.
pub fn with_source(source: Source) -> Self {
Builder {
args: Args::new(source),
options: SerializationOptions::default(),
}
}
/// Sets the crate name for the runtime crate. This setting is usually only
/// needed if the crate was renamed in the Cargo manifest.
pub fn with_runtime_crate_name<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.args.runtime_crate_name = name.into();
self
}
/// Sets an optional visibility modifier for the generated module and
/// contract re-export.
pub fn with_visibility_modifier<S>(mut self, vis: Option<S>) -> Self
where
S: Into<String>,
{
self.args.visibility_modifier = vis.map(S::into);
self
}
/// Sets the optional contract module name override.
pub fn with_contract_mod_override<S>(mut self, name: Option<S>) -> Self
where
S: Into<String>,
{
self.args.contract_mod_override = name.map(S::into);
self
}
/// Sets the optional contract name override. This setting is needed when
/// using a artifact JSON source that does not provide a contract name such
/// as Etherscan.
pub fn with_contract_name_override<S>(mut self, name: Option<S>) -> Self
where
S: Into<String>,
{
self.args.contract_name_override = name.map(S::into);
self
}
/// Manually adds specifies the deployed address of a contract for a given
/// network. Note that manually specified deployments take precedence over
/// deployments in the Truffle artifact (in the `networks` property of the
/// artifact).
///
/// This is useful for integration test scenarios where the address of a
/// contract on the test node is deterministic (for example using
/// `ganache-cli -d`) but the contract address is not part of the Truffle
/// artifact; or to override a deployment included in a Truffle artifact.
pub fn add_deployment(mut self, network_id: u32, address: Address) -> Self {
self.args.deployments.insert(network_id, address);
self
}
/// Manually adds specifies the deployed address as a string of a contract
/// for a given network. See `Builder::add_deployment` for more information.
///
/// # Panics
///
/// This method panics if the specified address string is invalid. See
/// `parse_address` for more information on the address string format.
pub fn add_deployment_str<S>(self, network_id: u32, address: S) -> Self
where
S: AsRef<str>,
{
self.add_deployment(
network_id,
parse_address(address).expect("failed to parse address"),
)
}
/// Manually adds a solidity method alias to specify what the method name
/// will be in Rust. For solidity methods without an alias, the snake cased
/// method name will be used.
pub fn add_method_alias<S1, S2>(mut self, signature: S1, alias: S2) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
self.args
.method_aliases
.insert(signature.into(), alias.into());
self
}
/// Specify whether or not to format the code using a locally installed copy
/// of `rustfmt`.
///
/// Note that in case `rustfmt` does not exist or produces an error, the
/// unformatted code will be used.
pub fn with_rustfmt(mut self, rustfmt: bool) -> Self {
self.options.rustfmt = rustfmt;
self
}
/// Add a custom derive to the derives for event structs and enums.
///
/// This makes it possible to for example derive serde::Serialize and
/// serde::Deserialize for events.
///
/// # Examples
///
/// ```
/// use ethcontract_generate::Builder;
/// let builder = Builder::new("path")
/// .add_event_derive("serde::Serialize")
/// .add_event_derive("serde::Deserialize");
/// ```
pub fn add_event_derive<S>(mut self, derive: S) -> Self
where
S: Into<String>,
{
self.args.event_derives.push(derive.into());
self
}
/// Generates the contract bindings.
pub fn generate(self) -> Result<ContractBindings> {
let tokens = contract::expand(self.args)?;
Ok(ContractBindings {
tokens,
options: self.options,
})
}
}
/// Type-safe contract bindings generated by a `Builder`. This type can be
/// either written to file or into a token stream for use in a procedural macro.
pub struct ContractBindings {
/// The TokenStream representing the contract bindings.
tokens: TokenStream,
/// The output options used for serialization.
options: SerializationOptions,
}
impl ContractBindings {
/// Writes the bindings to a given `Write`.
pub fn write<W>(&self, mut w: W) -> Result<()>
where
W: Write,
{
let source = {
let raw = self.tokens.to_string();
if self.options.rustfmt {
rustfmt::format(&raw).unwrap_or(raw)
} else {
raw
}
};
w.write_all(source.as_bytes())?;
Ok(())
}
/// Writes the bindings to the specified file.
pub fn write_to_file<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
let file = File::create(path)?;
self.write(file)
}
/// Converts the bindings into its underlying token stream. This allows it
/// to be used within a procedural macro.
pub fn into_tokens(self) -> TokenStream {
self.tokens
}
}

View File

@ -0,0 +1,36 @@
//! This module implements basic `rustfmt` code formatting.
use anyhow::{anyhow, Result};
use std::io::Write;
use std::process::{Command, Stdio};
/// Format the raw input source string and return formatted output.
pub fn format<S>(source: S) -> Result<String>
where
S: AsRef<str>,
{
let mut rustfmt = Command::new("rustfmt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
{
let stdin = rustfmt
.stdin
.as_mut()
.ok_or_else(|| anyhow!("stdin was not created for `rustfmt` child process"))?;
stdin.write_all(source.as_ref().as_bytes())?;
}
let output = rustfmt.wait_with_output()?;
if !output.status.success() {
return Err(anyhow!(
"`rustfmt` exited with code {}:\n{}",
output.status,
String::from_utf8_lossy(&output.stderr),
));
}
let stdout = String::from_utf8(output.stdout)?;
Ok(stdout)
}

View File

@ -0,0 +1,260 @@
//! Module implements reading of contract artifacts from various sources.
use crate::util;
use anyhow::{anyhow, Context, Error, Result};
use ethcontract_common::Address;
use std::borrow::Cow;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use url::Url;
/// A source of a Truffle artifact JSON.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Source {
/// A Truffle artifact or ABI located on the local file system.
Local(PathBuf),
/// A truffle artifact or ABI to be retrieved over HTTP(S).
Http(Url),
/// An address of a mainnet contract that has been verified on Etherscan.io.
Etherscan(Address),
/// The package identifier of an npm package with a path to a Truffle
/// artifact or ABI to be retrieved from `unpkg.io`.
Npm(String),
}
impl Source {
/// Parses an artifact source from a string.
///
/// Contract artifacts can be retrieved from the local filesystem or online
/// from `etherscan.io`, this method parses artifact source URLs and accepts
/// the following:
/// - `relative/path/to/Contract.json`: a relative path to a truffle
/// artifact JSON file. This relative path is rooted in the current
/// working directory. To specify the root for relative paths, use
/// `Source::with_root`.
/// - `/absolute/path/to/Contract.json` or
/// `file:///absolute/path/to/Contract.json`: an absolute path or file URL
/// to a truffle artifact JSON file.
/// - `http(s)://...` an HTTP url to a contract ABI or Truffle artifact.
/// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a
/// address or URL of a verified contract on Etherscan.
/// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with
/// an optional version and path (defaulting to the latest version and
/// `index.js`). The contract artifact or ABI will be retrieved through
/// `unpkg.io`.
pub fn parse<S>(source: S) -> Result<Self>
where
S: AsRef<str>,
{
let root = env::current_dir()?.canonicalize()?;
Source::with_root(root, source)
}
/// Parses an artifact source from a string and a specified root directory
/// for resolving relative paths. See `Source::with_root` for more details
/// on supported source strings.
pub fn with_root<P, S>(root: P, source: S) -> Result<Self>
where
P: AsRef<Path>,
S: AsRef<str>,
{
let base = Url::from_directory_path(root)
.map_err(|_| anyhow!("root path '{}' is not absolute"))?;
let url = base.join(source.as_ref())?;
match url.scheme() {
"file" => Ok(Source::local(url.path())),
"http" | "https" => match url.host_str() {
Some("etherscan.io") => Source::etherscan(
url.path()
.rsplit('/')
.next()
.ok_or_else(|| anyhow!("HTTP URL does not have a path"))?,
),
_ => Ok(Source::Http(url)),
},
"etherscan" => Source::etherscan(url.path()),
"npm" => Ok(Source::npm(url.path())),
_ => Err(anyhow!("unsupported URL '{}'", url)),
}
}
/// Creates a local filesystem source from a path string.
pub fn local<P>(path: P) -> Self
where
P: AsRef<Path>,
{
Source::Local(path.as_ref().into())
}
/// Creates an HTTP source from a URL.
pub fn http<S>(url: S) -> Result<Self>
where
S: AsRef<str>,
{
Ok(Source::Http(Url::parse(url.as_ref())?))
}
/// Creates an Etherscan source from an address string.
pub fn etherscan<S>(address: S) -> Result<Self>
where
S: AsRef<str>,
{
let address =
util::parse_address(address).context("failed to parse address for Etherscan source")?;
Ok(Source::Etherscan(address))
}
/// Creates an Etherscan source from an address string.
pub fn npm<S>(package_path: S) -> Self
where
S: Into<String>,
{
Source::Npm(package_path.into())
}
/// Retrieves the source JSON of the artifact this will either read the JSON
/// from the file system or retrieve a contract ABI from the network
/// dependending on the source type.
pub fn artifact_json(&self) -> Result<String> {
match self {
Source::Local(path) => get_local_contract(path),
Source::Http(url) => get_http_contract(url),
Source::Etherscan(address) => get_etherscan_contract(*address),
Source::Npm(package) => get_npm_contract(package),
}
}
}
impl FromStr for Source {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Source::parse(s)
}
}
/// Reads a Truffle artifact JSON file from the local filesystem.
fn get_local_contract(path: &Path) -> Result<String> {
let path = if path.is_relative() {
let absolute_path = path.canonicalize().with_context(|| {
format!(
"unable to canonicalize file from working dir {} with path {}",
env::current_dir()
.map(|cwd| cwd.display().to_string())
.unwrap_or_else(|err| format!("??? ({})", err)),
path.display(),
)
})?;
Cow::Owned(absolute_path)
} else {
Cow::Borrowed(path)
};
let json = fs::read_to_string(path).context("failed to read artifact JSON file")?;
Ok(abi_or_artifact(json))
}
/// Retrieves a Truffle artifact or ABI from an HTTP URL.
fn get_http_contract(url: &Url) -> Result<String> {
let json = util::http_get(url.as_str())
.with_context(|| format!("failed to retrieve JSON from {}", url))?;
Ok(abi_or_artifact(json))
}
/// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an
/// artifact JSON for compatibility with the code generation facilities.
fn get_etherscan_contract(address: Address) -> Result<String> {
// NOTE: We do not retrieve the bytecode since deploying contracts with the
// same bytecode is unreliable as the libraries have already linked and
// probably don't reference anything when deploying on other networks.
let api_key = env::var("ETHERSCAN_API_KEY")
.map(|key| format!("&apikey={}", key))
.unwrap_or_default();
let abi_url = format!(
"http://api.etherscan.io/api\
?module=contract&action=getabi&address={:?}&format=raw{}",
address, api_key,
);
let abi = util::http_get(&abi_url).context("failed to retrieve ABI from Etherscan.io")?;
// NOTE: Wrap the retrieved ABI in an empty contract, this is because
// currently, the code generation infrastructure depends on having an
// `Artifact` instance.
let json = format!(
r#"{{"abi":{},"networks":{{"1":{{"address":"{:?}"}}}}}}"#,
abi, address,
);
Ok(json)
}
/// Retrieves a Truffle artifact or ABI from an npm package through `unpkg.io`.
fn get_npm_contract(package: &str) -> Result<String> {
let unpkg_url = format!("https://unpkg.io/{}", package);
let json = util::http_get(&unpkg_url)
.with_context(|| format!("failed to retrieve JSON from for npm package {}", package))?;
Ok(abi_or_artifact(json))
}
/// A best-effort coersion of an ABI or Truffle artifact JSON document into a
/// Truffle artifact JSON document.
///
/// This method uses the fact that ABIs are arrays and Truffle artifacts are
/// objects to guess at what type of document this is. Note that no parsing or
/// validation is done at this point as the document gets parsed and validated
/// at generation time.
///
/// This needs to be done as currently the contract generation infrastructure
/// depends on having a Truffle artifact.
fn abi_or_artifact(json: String) -> String {
if json.trim().starts_with('[') {
format!(r#"{{"abi":{}}}"#, json.trim())
} else {
json
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_source() {
let root = "/rooted";
for (url, expected) in &[
(
"relative/Contract.json",
Source::local("/rooted/relative/Contract.json"),
),
(
"/absolute/Contract.json",
Source::local("/absolute/Contract.json"),
),
(
"https://my.domain.eth/path/to/Contract.json",
Source::http("https://my.domain.eth/path/to/Contract.json").unwrap(),
),
(
"etherscan:0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
),
(
"https://etherscan.io/address/0x0001020304050607080910111213141516171819",
Source::etherscan("0x0001020304050607080910111213141516171819").unwrap(),
),
(
"npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json",
Source::npm("@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json"),
),
] {
let source = Source::with_root(root, url).unwrap();
assert_eq!(source, *expected);
}
}
}

View File

@ -0,0 +1,10 @@
/// Asserts the result of an expansion matches source output.
///
/// # Panics
///
/// If the expanded source does not match the quoted source.
macro_rules! assert_quote {
($ex:expr, { $($t:tt)* } $(,)?) => {
assert_eq!($ex.to_string(), quote::quote! { $($t)* }.to_string())
};
}

View File

@ -0,0 +1,117 @@
use anyhow::{anyhow, Result};
use curl::easy::Easy;
use ethcontract_common::Address;
use inflector::Inflector;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::quote;
use syn::Ident as SynIdent;
/// Expands a identifier string into an token.
pub fn ident(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
/// Expands an identifier string into a token and appending `_` if the
/// identifier is for a reserved keyword.
///
/// Parsing keywords like `self` can fail, in this case we add an underscore.
pub fn safe_ident(name: &str) -> Ident {
syn::parse_str::<SynIdent>(name).unwrap_or_else(|_| ident(&format!("{}_", name)))
}
/// Expands a positional identifier string that may be empty.
///
/// Note that this expands the parameter name with `safe_ident`, meaning that
/// identifiers that are reserved keywords get `_` appended to them.
pub fn expand_input_name(index: usize, name: &str) -> TokenStream {
let name_str = match name {
"" => format!("p{}", index),
n => n.to_snake_case(),
};
let name = safe_ident(&name_str);
quote! { #name }
}
/// Expands a doc string into an attribute token stream.
pub fn expand_doc(s: &str) -> TokenStream {
let doc = Literal::string(s);
quote! {
#[doc = #doc]
}
}
/// Parses the given address string
pub fn parse_address<S>(address_str: S) -> Result<Address>
where
S: AsRef<str>,
{
let address_str = address_str.as_ref();
if !address_str.starts_with("0x") {
return Err(anyhow!("address must start with '0x'"));
}
Ok(address_str[2..].parse()?)
}
/// Perform an HTTP GET request and return the contents of the response.
pub fn http_get(url: &str) -> Result<String> {
let mut buffer = Vec::new();
let mut handle = Easy::new();
handle.url(url)?;
{
let mut transfer = handle.transfer();
transfer.write_function(|data| {
buffer.extend_from_slice(data);
Ok(data.len())
})?;
transfer.perform()?;
}
let buffer = String::from_utf8(buffer)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_name_to_ident_empty() {
assert_quote!(expand_input_name(0, ""), { p0 });
}
#[test]
fn input_name_to_ident_keyword() {
assert_quote!(expand_input_name(0, "self"), { self_ });
}
#[test]
fn input_name_to_ident_snake_case() {
assert_quote!(expand_input_name(0, "CamelCase1"), { camel_case_1 });
}
#[test]
fn parse_address_missing_prefix() {
if parse_address("0000000000000000000000000000000000000000").is_ok() {
panic!("parsing address not starting with 0x should fail");
}
}
#[test]
fn parse_address_address_too_short() {
if parse_address("0x00000000000000").is_ok() {
panic!("parsing address not starting with 0x should fail");
}
}
#[test]
fn parse_address_ok() {
let expected = Address::from([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
]);
assert_eq!(
parse_address("0x000102030405060708090a0b0c0d0e0f10111213").unwrap(),
expected
);
}
}

View File

@ -0,0 +1,9 @@
[package]
name = "ethers-providers"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -0,0 +1,9 @@
[package]
name = "ethers-signers"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "ethers-types"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ethereum-types = { version = "0.9.2", default-features = false, features = ["serialize"] }
serde = { version = "1.0.110", default-features = false, features = ["derive"] }
rlp = { version = "0.4.5", default-features = false }
rustc-hex = { version = "2.1.0", default-features = false }
thiserror = { version = "1.0.19", default-features = false }
# crypto
secp256k1 = { version = "0.17.2", default-features = false, features = ["std", "recovery", "rand"] }
rand = { version = "0.5.1", default-features = false } # this should be the same rand crate version as the one in secp
zeroize = { version = "1.1.0", default-features = false }

View File

@ -0,0 +1,9 @@
[package]
name = "ethers-utils"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

11
crates/ethers/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "ethers"
version = "0.1.0"
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# TODO: Make this have features for each available module