el refactor
This commit is contained in:
parent
d3b9b378c5
commit
3f313ede01
File diff suppressed because it is too large
Load Diff
70
Cargo.toml
70
Cargo.toml
|
@ -1,30 +1,44 @@
|
|||
[package]
|
||||
name = "ethers"
|
||||
version = "0.1.0"
|
||||
authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
|
||||
edition = "2018"
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
ethereum-types = { version = "0.9.2", default-features = false, features = ["serialize"] }
|
||||
url = { version = "2.1.1", default-features = false }
|
||||
async-trait = { version = "0.1.31", default-features = false }
|
||||
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1.0.110", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.53", default-features = false }
|
||||
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 }
|
||||
members = [
|
||||
# "./crates/ethers",
|
||||
# "./crates/ethers-abi",
|
||||
# "./crates/ethers-contract",
|
||||
# "./crates/ethers-derive",
|
||||
# "./crates/ethers-providers",
|
||||
# "./crates/ethers-signers",
|
||||
"./crates/ethers-types",
|
||||
# "./crates/ethers-utils",
|
||||
]
|
||||
|
||||
solc = { git = "https://github.com/paritytech/rust_solc "}
|
||||
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" }
|
||||
# [dependencies]
|
||||
# ethers-derive = { path = "ethers-derive", optional = true }
|
||||
#
|
||||
# ethereum-types = { version = "0.9.2", default-features = false, features = ["serialize"] }
|
||||
# url = { version = "2.1.1", default-features = false }
|
||||
# async-trait = { version = "0.1.31", default-features = false }
|
||||
# reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
|
||||
# serde = { version = "1.0.110", default-features = false, features = ["derive"] }
|
||||
# serde_json = { version = "1.0.53", default-features = false }
|
||||
# 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"]
|
||||
#
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
})
|
||||
}
|
|
@ -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])
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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, ¶ms)
|
||||
} else {
|
||||
expand_data_struct(&event_name, ¶ms)
|
||||
};
|
||||
|
||||
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(¶m.name.to_snake_case())
|
||||
};
|
||||
let ty = expand_input_type(¶m)?;
|
||||
|
||||
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, ¶ms);
|
||||
|
||||
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, ¶ms);
|
||||
|
||||
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
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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, ¶m.name);
|
||||
let kind = types::expand(¶m.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, ¶m.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(¶m.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) },
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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")),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
};
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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]
|
|
@ -0,0 +1,7 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue