abigen: simplify structs and re-enable file/remote codegen

This commit is contained in:
Georgios Konstantopoulos 2020-06-03 23:09:46 +03:00
parent b47c89455c
commit ba7fedc7d3
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
6 changed files with 57 additions and 193 deletions

View File

@ -0,0 +1 @@
[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]

View File

@ -1,10 +1,8 @@
use ethers_contract_abigen::Builder; use ethers_contract_abigen::Abigen;
const ABI: &str = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
fn main() { fn main() {
let builder = Builder::from_str("ERC20Token", ABI); Abigen::new("ERC20Token", "./abi.json")
builder .unwrap()
.generate() .generate()
.unwrap() .unwrap()
.write_to_file("token.rs") .write_to_file("token.rs")

View File

@ -10,7 +10,7 @@ mod methods;
mod types; mod types;
use super::util; use super::util;
use super::Args; use super::Abigen;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use ethers_core::{abi::Abi, types::Address}; use ethers_core::{abi::Abi, types::Address};
use inflector::Inflector; use inflector::Inflector;
@ -21,23 +21,12 @@ use syn::{Path, Visibility};
/// Internal shared context for generating smart contract bindings. /// Internal shared context for generating smart contract bindings.
pub(crate) struct Context { pub(crate) struct Context {
/// The contract name
name: String,
/// The ABI string pre-parsing. /// The ABI string pre-parsing.
abi_str: Literal, abi_str: Literal,
/// The parsed ABI. /// The parsed ABI.
abi: Abi, abi: Abi,
/// 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 contract name as an identifier. /// The contract name as an identifier.
contract_name: Ident, contract_name: Ident,
@ -49,8 +38,8 @@ pub(crate) struct Context {
} }
impl Context { impl Context {
pub fn expand(args: Args) -> Result<TokenStream> { pub(crate) fn expand(args: Abigen) -> Result<TokenStream> {
let cx = Self::from_args(args)?; let cx = Self::from_abigen(args)?;
let name = &cx.contract_name; let name = &cx.contract_name;
let name_mod = util::ident(&format!( let name_mod = util::ident(&format!(
"{}_mod", "{}_mod",
@ -105,7 +94,7 @@ impl Context {
} }
/// Create a context from the code generation arguments. /// Create a context from the code generation arguments.
fn from_args(args: Args) -> Result<Self> { fn from_abigen(args: Abigen) -> Result<Self> {
// get the actual ABI string // get the actual ABI string
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?; let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
@ -116,16 +105,7 @@ impl Context {
format!("failed to parse artifact from source {:?}", args.abi_source,) format!("failed to parse artifact from source {:?}", args.abi_source,)
})?; })?;
let raw_contract_name = args.contract_name; let contract_name = util::ident(&args.contract_name);
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_name = util::ident(&raw_contract_name);
// NOTE: We only check for duplicate signatures here, since if there are // NOTE: We only check for duplicate signatures here, since if there are
// duplicate aliases, the compiler will produce a warning because a // duplicate aliases, the compiler will produce a warning because a
@ -149,11 +129,8 @@ impl Context {
.context("failed to parse event derives")?; .context("failed to parse event derives")?;
Ok(Context { Ok(Context {
name: raw_contract_name,
abi, abi,
abi_str: Literal::string(&abi_str), abi_str: Literal::string(&abi_str),
runtime_crate,
visibility,
contract_name, contract_name,
method_aliases, method_aliases,
event_derives, event_derives,

View File

@ -88,9 +88,9 @@ fn expand_event(event: &Event, event_derives: &[Path]) -> Result<TokenStream> {
let params_len = Literal::usize_unsuffixed(params.len()); let params_len = Literal::usize_unsuffixed(params.len());
let read_param_token = params let read_param_token = params
.iter() .iter()
.map(|(name, ty)| { .map(|(name, _)| {
quote! { quote! {
let #name = #ty::from_token(tokens.next().expect("this should never happen"))?; let #name = Detokenize::from_token(tokens.next().expect("this should never happen"))?;
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@ -25,14 +25,11 @@ pub use util::parse_address;
use anyhow::Result; use anyhow::Result;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use std::collections::HashMap; use std::{collections::HashMap, fs::File, io::Write, path::Path};
use std::fs::File;
use std::io::Write;
use std::path::Path;
/// Internal global arguments passed to the generators for each individual /// Internal global arguments passed to the generators for each individual
/// component that control expansion. /// component that control expansion.
pub(crate) struct Args { pub struct Abigen {
/// The source of the ABI JSON for the contract whose bindings /// The source of the ABI JSON for the contract whose bindings
/// are being generated. /// are being generated.
abi_source: Source, abi_source: Source,
@ -40,120 +37,27 @@ pub(crate) struct Args {
/// Override the contract name to use for the generated type. /// Override the contract name to use for the generated type.
contract_name: String, contract_name: String,
/// 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>,
/// Manually specified contract method aliases. /// Manually specified contract method aliases.
method_aliases: HashMap<String, String>, method_aliases: HashMap<String, String>,
/// Derives added to event structs and enums. /// Derives added to event structs and enums.
event_derives: Vec<String>, event_derives: Vec<String>,
}
impl Args {
/// Creates a new builder given the path to a contract's truffle artifact
/// JSON file.
pub fn new(contract_name: &str, abi_source: Source) -> Self {
Args {
abi_source,
contract_name: contract_name.to_owned(),
runtime_crate_name: "abigen".to_owned(),
visibility_modifier: None,
contract_mod_override: None,
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`. /// Format the code using a locally installed copy of `rustfmt`.
rustfmt: bool, rustfmt: bool,
} }
impl Default for SerializationOptions { impl Abigen {
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 contract's ABI JSON string
pub fn from_str(name: &str, abi: &str) -> Self {
Builder::source(name, Source::String(abi.to_owned()))
}
/// Creates a new builder given the path to a contract's ABI file
pub fn new<P>(name: &str, path: P) -> Self
where
P: AsRef<Path>,
{
Builder::source(name, Source::local(path))
}
/// Creates a new builder from a source URL.
pub fn from_url<S>(name: &str, url: S) -> Result<Self>
where
S: AsRef<str>,
{
let source = Source::parse(url)?;
Ok(Builder::source(name, source))
}
/// Creates a new builder with the given ABI JSON source. /// Creates a new builder with the given ABI JSON source.
pub fn source(name: &str, source: Source) -> Self { pub fn new<S: AsRef<str>>(contract_name: &str, abi_source: S) -> Result<Self> {
Builder { let abi_source = abi_source.as_ref().parse()?;
args: Args::new(name, source), Ok(Self {
options: SerializationOptions::default(), abi_source,
} contract_name: contract_name.to_owned(),
} method_aliases: HashMap::new(),
event_derives: Vec::new(),
/// Sets the crate name for the runtime crate. This setting is usually only rustfmt: true,
/// needed if the crate was renamed in the Cargo manifest. })
pub fn 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 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 contract_mod_override<S>(mut self, name: Option<S>) -> Self
where
S: Into<String>,
{
self.args.contract_mod_override = name.map(S::into);
self
} }
/// Manually adds a solidity method alias to specify what the method name /// Manually adds a solidity method alias to specify what the method name
@ -164,9 +68,7 @@ impl Builder {
S1: Into<String>, S1: Into<String>,
S2: Into<String>, S2: Into<String>,
{ {
self.args self.method_aliases.insert(signature.into(), alias.into());
.method_aliases
.insert(signature.into(), alias.into());
self self
} }
@ -176,7 +78,7 @@ impl Builder {
/// Note that in case `rustfmt` does not exist or produces an error, the /// Note that in case `rustfmt` does not exist or produces an error, the
/// unformatted code will be used. /// unformatted code will be used.
pub fn rustfmt(mut self, rustfmt: bool) -> Self { pub fn rustfmt(mut self, rustfmt: bool) -> Self {
self.options.rustfmt = rustfmt; self.rustfmt = rustfmt;
self self
} }
@ -188,17 +90,15 @@ impl Builder {
where where
S: Into<String>, S: Into<String>,
{ {
self.args.event_derives.push(derive.into()); self.event_derives.push(derive.into());
self self
} }
/// Generates the contract bindings. /// Generates the contract bindings.
pub fn generate(self) -> Result<ContractBindings> { pub fn generate(self) -> Result<ContractBindings> {
let tokens = Context::expand(self.args)?; let rustfmt = self.rustfmt;
Ok(ContractBindings { let tokens = Context::expand(self)?;
tokens, Ok(ContractBindings { tokens, rustfmt })
options: self.options,
})
} }
} }
@ -208,7 +108,7 @@ pub struct ContractBindings {
/// The TokenStream representing the contract bindings. /// The TokenStream representing the contract bindings.
tokens: TokenStream, tokens: TokenStream,
/// The output options used for serialization. /// The output options used for serialization.
options: SerializationOptions, rustfmt: bool,
} }
impl ContractBindings { impl ContractBindings {
@ -220,7 +120,7 @@ impl ContractBindings {
let source = { let source = {
let raw = self.tokens.to_string(); let raw = self.tokens.to_string();
if self.options.rustfmt { if self.rustfmt {
rustfmt::format(&raw).unwrap_or(raw) rustfmt::format(&raw).unwrap_or(raw)
} else { } else {
raw raw

View File

@ -35,17 +35,24 @@ impl Source {
/// Parses an ABI from a source /// Parses an ABI from a source
/// ///
/// Contract ABIs can be retrieved from the local filesystem or online /// Contract ABIs can be retrieved from the local filesystem or online
/// from `etherscan.io`, this method parses ABI source URLs and accepts /// from `etherscan.io`. They can also be provided in-line. This method parses
/// the following: /// ABI source URLs and accepts the following:
///
/// - raw ABI JSON
///
/// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. /// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file.
/// This relative path is rooted in the current working directory. /// This relative path is rooted in the current working directory.
/// To specify the root for relative paths, use `Source::with_root`. /// To specify the root for relative paths, use `Source::with_root`.
///
/// - `/absolute/path/to/Contract.json` or /// - `/absolute/path/to/Contract.json` or
/// `file:///absolute/path/to/Contract.json`: an absolute path or file URL /// `file:///absolute/path/to/Contract.json`: an absolute path or file URL
/// to an ABI JSON file. /// to an ABI JSON file.
///
/// - `http(s)://...` an HTTP url to a contract ABI. /// - `http(s)://...` an HTTP url to a contract ABI.
///
/// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a /// - `etherscan:0xXX..XX` or `https://etherscan.io/address/0xXX..XX`: a
/// address or URL of a verified contract on Etherscan. /// address or URL of a verified contract on Etherscan.
///
/// - `npm:@org/package@1.0.0/path/to/contract.json` an npmjs package with /// - `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 /// an optional version and path (defaulting to the latest version and
/// `index.js`). The contract ABI will be retrieved through /// `index.js`). The contract ABI will be retrieved through
@ -54,6 +61,10 @@ impl Source {
where where
S: AsRef<str>, S: AsRef<str>,
{ {
let source = source.as_ref();
if source.starts_with('[') {
return Ok(Source::String(source.to_owned()));
}
let root = env::current_dir()?.canonicalize()?; let root = env::current_dir()?.canonicalize()?;
Source::with_root(root, source) Source::with_root(root, source)
} }
@ -61,7 +72,7 @@ impl Source {
/// Parses an artifact source from a string and a specified root directory /// Parses an artifact source from a string and a specified root directory
/// for resolving relative paths. See `Source::with_root` for more details /// for resolving relative paths. See `Source::with_root` for more details
/// on supported source strings. /// on supported source strings.
pub fn with_root<P, S>(root: P, source: S) -> Result<Self> fn with_root<P, S>(root: P, source: S) -> Result<Self>
where where
P: AsRef<Path>, P: AsRef<Path>,
S: AsRef<str>, S: AsRef<str>,
@ -88,7 +99,7 @@ impl Source {
} }
/// Creates a local filesystem source from a path string. /// Creates a local filesystem source from a path string.
pub fn local<P>(path: P) -> Self fn local<P>(path: P) -> Self
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@ -96,7 +107,7 @@ impl Source {
} }
/// Creates an HTTP source from a URL. /// Creates an HTTP source from a URL.
pub fn http<S>(url: S) -> Result<Self> fn http<S>(url: S) -> Result<Self>
where where
S: AsRef<str>, S: AsRef<str>,
{ {
@ -104,7 +115,7 @@ impl Source {
} }
/// Creates an Etherscan source from an address string. /// Creates an Etherscan source from an address string.
pub fn etherscan<S>(address: S) -> Result<Self> fn etherscan<S>(address: S) -> Result<Self>
where where
S: AsRef<str>, S: AsRef<str>,
{ {
@ -114,7 +125,7 @@ impl Source {
} }
/// Creates an Etherscan source from an address string. /// Creates an Etherscan source from an address string.
pub fn npm<S>(package_path: S) -> Self fn npm<S>(package_path: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
@ -161,14 +172,14 @@ fn get_local_contract(path: &Path) -> Result<String> {
}; };
let json = fs::read_to_string(path).context("failed to read artifact JSON file")?; let json = fs::read_to_string(path).context("failed to read artifact JSON file")?;
Ok(abi_or_artifact(json)) Ok(json)
} }
/// Retrieves a Truffle artifact or ABI from an HTTP URL. /// Retrieves a Truffle artifact or ABI from an HTTP URL.
fn get_http_contract(url: &Url) -> Result<String> { fn get_http_contract(url: &Url) -> Result<String> {
let json = util::http_get(url.as_str()) let json = util::http_get(url.as_str())
.with_context(|| format!("failed to retrieve JSON from {}", url))?; .with_context(|| format!("failed to retrieve JSON from {}", url))?;
Ok(abi_or_artifact(json)) Ok(json)
} }
/// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an /// Retrieves a contract ABI from the Etherscan HTTP API and wraps it in an
@ -188,16 +199,7 @@ fn get_etherscan_contract(address: Address) -> Result<String> {
address, api_key, address, api_key,
); );
let abi = util::http_get(&abi_url).context("failed to retrieve ABI from Etherscan.io")?; let abi = util::http_get(&abi_url).context("failed to retrieve ABI from Etherscan.io")?;
Ok(abi)
// 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`. /// Retrieves a Truffle artifact or ABI from an npm package through `unpkg.io`.
@ -206,25 +208,7 @@ fn get_npm_contract(package: &str) -> Result<String> {
let json = util::http_get(&unpkg_url) let json = util::http_get(&unpkg_url)
.with_context(|| format!("failed to retrieve JSON from for npm package {}", package))?; .with_context(|| format!("failed to retrieve JSON from for npm package {}", package))?;
Ok(abi_or_artifact(json)) Ok(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)] #[cfg(test)]
@ -263,5 +247,9 @@ mod tests {
let source = Source::with_root(root, url).unwrap(); let source = Source::with_root(root, url).unwrap();
assert_eq!(source, *expected); assert_eq!(source, *expected);
} }
let src = r#"[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"name","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"symbol","type":"string"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"decimals","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"spender","type":"address"},{"name":"value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"totalSupply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"from","type":"address"},{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]"#;
let parsed = Source::parse(src).unwrap();
assert_eq!(parsed, Source::String(src.to_owned()));
} }
} }