From ba7fedc7d3f64a9e19ce678fed147500a2c54aff Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 3 Jun 2020 23:09:46 +0300 Subject: [PATCH] abigen: simplify structs and re-enable file/remote codegen --- .../ethers-contract-abigen/examples/abi.json | 1 + .../ethers-contract-abigen/examples/abigen.rs | 8 +- .../ethers-contract-abigen/src/contract.rs | 33 +---- .../src/contract/events.rs | 4 +- .../ethers-contract-abigen/src/lib.rs | 140 +++--------------- .../ethers-contract-abigen/src/source.rs | 64 ++++---- 6 files changed, 57 insertions(+), 193 deletions(-) create mode 100644 ethers-contract/ethers-contract-abigen/examples/abi.json diff --git a/ethers-contract/ethers-contract-abigen/examples/abi.json b/ethers-contract/ethers-contract-abigen/examples/abi.json new file mode 100644 index 00000000..4ddb8eb5 --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/examples/abi.json @@ -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"}] diff --git a/ethers-contract/ethers-contract-abigen/examples/abigen.rs b/ethers-contract/ethers-contract-abigen/examples/abigen.rs index 0baf58fc..7039ae9a 100644 --- a/ethers-contract/ethers-contract-abigen/examples/abigen.rs +++ b/ethers-contract/ethers-contract-abigen/examples/abigen.rs @@ -1,10 +1,8 @@ -use ethers_contract_abigen::Builder; - -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"}]"#; +use ethers_contract_abigen::Abigen; fn main() { - let builder = Builder::from_str("ERC20Token", ABI); - builder + Abigen::new("ERC20Token", "./abi.json") + .unwrap() .generate() .unwrap() .write_to_file("token.rs") diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index a2ea4a13..30e80bc1 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -10,7 +10,7 @@ mod methods; mod types; use super::util; -use super::Args; +use super::Abigen; use anyhow::{anyhow, Context as _, Result}; use ethers_core::{abi::Abi, types::Address}; use inflector::Inflector; @@ -21,23 +21,12 @@ use syn::{Path, Visibility}; /// Internal shared context for generating smart contract bindings. pub(crate) struct Context { - /// The contract name - name: String, - /// The ABI string pre-parsing. abi_str: Literal, /// The parsed 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. contract_name: Ident, @@ -49,8 +38,8 @@ pub(crate) struct Context { } impl Context { - pub fn expand(args: Args) -> Result { - let cx = Self::from_args(args)?; + pub(crate) fn expand(args: Abigen) -> Result { + let cx = Self::from_abigen(args)?; let name = &cx.contract_name; let name_mod = util::ident(&format!( "{}_mod", @@ -105,7 +94,7 @@ impl Context { } /// Create a context from the code generation arguments. - fn from_args(args: Args) -> Result { + fn from_abigen(args: Abigen) -> Result { // get the actual ABI string 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,) })?; - let raw_contract_name = 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); + let contract_name = util::ident(&args.contract_name); // NOTE: We only check for duplicate signatures here, since if there are // duplicate aliases, the compiler will produce a warning because a @@ -149,11 +129,8 @@ impl Context { .context("failed to parse event derives")?; Ok(Context { - name: raw_contract_name, abi, abi_str: Literal::string(&abi_str), - runtime_crate, - visibility, contract_name, method_aliases, event_derives, diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index 67aae781..0fce108a 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -88,9 +88,9 @@ fn expand_event(event: &Event, event_derives: &[Path]) -> Result { let params_len = Literal::usize_unsuffixed(params.len()); let read_param_token = params .iter() - .map(|(name, ty)| { + .map(|(name, _)| { 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::>(); diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index 1b7e1627..ca02e410 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -25,14 +25,11 @@ pub use util::parse_address; use anyhow::Result; use proc_macro2::TokenStream; -use std::collections::HashMap; -use std::fs::File; -use std::io::Write; -use std::path::Path; +use std::{collections::HashMap, fs::File, io::Write, path::Path}; /// Internal global arguments passed to the generators for each individual /// component that control expansion. -pub(crate) struct Args { +pub struct Abigen { /// The source of the ABI JSON for the contract whose bindings /// are being generated. abi_source: Source, @@ -40,120 +37,27 @@ pub(crate) struct Args { /// Override the contract name to use for the generated type. 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, - - /// Override the contract module name that contains the generated code. - contract_mod_override: Option, - /// Manually specified contract method aliases. method_aliases: HashMap, /// Derives added to event structs and enums. event_derives: Vec, -} -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`. 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 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

(name: &str, path: P) -> Self - where - P: AsRef, - { - Builder::source(name, Source::local(path)) - } - - /// Creates a new builder from a source URL. - pub fn from_url(name: &str, url: S) -> Result - where - S: AsRef, - { - let source = Source::parse(url)?; - Ok(Builder::source(name, source)) - } - +impl Abigen { /// Creates a new builder with the given ABI JSON source. - pub fn source(name: &str, source: Source) -> Self { - Builder { - args: Args::new(name, 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 runtime_crate_name(mut self, name: S) -> Self - where - S: Into, - { - 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(mut self, vis: Option) -> Self - where - S: Into, - { - self.args.visibility_modifier = vis.map(S::into); - self - } - - /// Sets the optional contract module name override. - pub fn contract_mod_override(mut self, name: Option) -> Self - where - S: Into, - { - self.args.contract_mod_override = name.map(S::into); - self + pub fn new>(contract_name: &str, abi_source: S) -> Result { + let abi_source = abi_source.as_ref().parse()?; + Ok(Self { + abi_source, + contract_name: contract_name.to_owned(), + method_aliases: HashMap::new(), + event_derives: Vec::new(), + rustfmt: true, + }) } /// Manually adds a solidity method alias to specify what the method name @@ -164,9 +68,7 @@ impl Builder { S1: Into, S2: Into, { - self.args - .method_aliases - .insert(signature.into(), alias.into()); + self.method_aliases.insert(signature.into(), alias.into()); self } @@ -176,7 +78,7 @@ impl Builder { /// Note that in case `rustfmt` does not exist or produces an error, the /// unformatted code will be used. pub fn rustfmt(mut self, rustfmt: bool) -> Self { - self.options.rustfmt = rustfmt; + self.rustfmt = rustfmt; self } @@ -188,17 +90,15 @@ impl Builder { where S: Into, { - self.args.event_derives.push(derive.into()); + self.event_derives.push(derive.into()); self } /// Generates the contract bindings. pub fn generate(self) -> Result { - let tokens = Context::expand(self.args)?; - Ok(ContractBindings { - tokens, - options: self.options, - }) + let rustfmt = self.rustfmt; + let tokens = Context::expand(self)?; + Ok(ContractBindings { tokens, rustfmt }) } } @@ -208,7 +108,7 @@ pub struct ContractBindings { /// The TokenStream representing the contract bindings. tokens: TokenStream, /// The output options used for serialization. - options: SerializationOptions, + rustfmt: bool, } impl ContractBindings { @@ -220,7 +120,7 @@ impl ContractBindings { let source = { let raw = self.tokens.to_string(); - if self.options.rustfmt { + if self.rustfmt { rustfmt::format(&raw).unwrap_or(raw) } else { raw diff --git a/ethers-contract/ethers-contract-abigen/src/source.rs b/ethers-contract/ethers-contract-abigen/src/source.rs index 048fa114..960a189c 100644 --- a/ethers-contract/ethers-contract-abigen/src/source.rs +++ b/ethers-contract/ethers-contract-abigen/src/source.rs @@ -35,17 +35,24 @@ impl Source { /// Parses an ABI from a source /// /// Contract ABIs can be retrieved from the local filesystem or online - /// from `etherscan.io`, this method parses ABI source URLs and accepts - /// the following: + /// from `etherscan.io`. They can also be provided in-line. This method parses + /// ABI source URLs and accepts the following: + /// + /// - raw ABI JSON + /// /// - `relative/path/to/Contract.json`: a relative path to an ABI 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 an ABI JSON file. + /// /// - `http(s)://...` an HTTP url to a contract ABI. + /// /// - `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 ABI will be retrieved through @@ -54,6 +61,10 @@ impl Source { where S: AsRef, { + let source = source.as_ref(); + if source.starts_with('[') { + return Ok(Source::String(source.to_owned())); + } let root = env::current_dir()?.canonicalize()?; Source::with_root(root, source) } @@ -61,7 +72,7 @@ impl 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(root: P, source: S) -> Result + fn with_root(root: P, source: S) -> Result where P: AsRef, S: AsRef, @@ -88,7 +99,7 @@ impl Source { } /// Creates a local filesystem source from a path string. - pub fn local

(path: P) -> Self + fn local

(path: P) -> Self where P: AsRef, { @@ -96,7 +107,7 @@ impl Source { } /// Creates an HTTP source from a URL. - pub fn http(url: S) -> Result + fn http(url: S) -> Result where S: AsRef, { @@ -104,7 +115,7 @@ impl Source { } /// Creates an Etherscan source from an address string. - pub fn etherscan(address: S) -> Result + fn etherscan(address: S) -> Result where S: AsRef, { @@ -114,7 +125,7 @@ impl Source { } /// Creates an Etherscan source from an address string. - pub fn npm(package_path: S) -> Self + fn npm(package_path: S) -> Self where S: Into, { @@ -161,14 +172,14 @@ fn get_local_contract(path: &Path) -> Result { }; 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. fn get_http_contract(url: &Url) -> Result { let json = util::http_get(url.as_str()) .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 @@ -188,16 +199,7 @@ fn get_etherscan_contract(address: Address) -> Result { 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) + Ok(abi) } /// Retrieves a Truffle artifact or ABI from an npm package through `unpkg.io`. @@ -206,25 +208,7 @@ fn get_npm_contract(package: &str) -> Result { 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 - } + Ok(json) } #[cfg(test)] @@ -263,5 +247,9 @@ mod tests { let source = Source::with_root(root, url).unwrap(); 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())); } }