diff --git a/ethcontract-common/src/artifact/hardhat.rs b/ethcontract-common/src/artifact/hardhat.rs index 51420292..b2392d18 100644 --- a/ethcontract-common/src/artifact/hardhat.rs +++ b/ethcontract-common/src/artifact/hardhat.rs @@ -166,10 +166,11 @@ impl HardHatLoader { } /// Loads an artifact from disk. - pub fn load_from_file(&self, p: &Path) -> Result { - let file = File::open(p)?; + pub fn load_from_file(&self, p: impl AsRef) -> Result { + let path = p.as_ref(); + let file = File::open(path)?; let reader = BufReader::new(file); - self.load_artifact(p.display(), reader, from_reader, from_reader) + self.load_artifact(path.display(), reader, from_reader, from_reader) } fn load_artifact( diff --git a/ethcontract-common/src/artifact/truffle.rs b/ethcontract-common/src/artifact/truffle.rs index ce5aea5d..deaab56a 100644 --- a/ethcontract-common/src/artifact/truffle.rs +++ b/ethcontract-common/src/artifact/truffle.rs @@ -85,10 +85,11 @@ impl TruffleLoader { } /// Loads an artifact from disk. - pub fn load_from_file(&self, p: &Path) -> Result { - let file = File::open(p)?; + pub fn load_from_file(&self, p: impl AsRef) -> Result { + let path = p.as_ref(); + let file = File::open(path)?; let reader = BufReader::new(file); - self.load_artifact(p.display(), reader, from_reader) + self.load_artifact(path.display(), reader, from_reader) } /// Loads a contract from a loaded JSON value. @@ -112,8 +113,9 @@ impl TruffleLoader { } /// Loads a contract from disk. - pub fn load_contract_from_file(&self, p: &Path) -> Result { - let file = File::open(p)?; + pub fn load_contract_from_file(&self, p: impl AsRef) -> Result { + let path = p.as_ref(); + let file = File::open(path)?; let reader = BufReader::new(file); self.load_contract(reader, from_reader) } diff --git a/ethcontract-common/src/contract.rs b/ethcontract-common/src/contract.rs index 2d50699b..535eb1c4 100644 --- a/ethcontract-common/src/contract.rs +++ b/ethcontract-common/src/contract.rs @@ -31,8 +31,13 @@ pub struct Contract { impl Contract { /// Creates an empty contract instance. pub fn empty() -> Self { + Contract::with_name(String::default()) + } + + /// Creates an empty contract instance with the given name. + pub fn with_name(name: impl Into) -> Self { Contract { - name: String::new(), + name: name.into(), abi: Abi { constructor: None, functions: HashMap::new(), diff --git a/ethcontract-derive/Cargo.toml b/ethcontract-derive/Cargo.toml index c6424098..9a44e89f 100644 --- a/ethcontract-derive/Cargo.toml +++ b/ethcontract-derive/Cargo.toml @@ -15,6 +15,7 @@ Proc macro for generating type-safe bindings to Ethereum smart contracts. proc-macro = true [dependencies] +anyhow = "1.0" ethcontract-common = { version = "0.12.2", path = "../ethcontract-common" } ethcontract-generate = { version = "0.12.2", path = "../ethcontract-generate" } proc-macro2 = "1.0" diff --git a/ethcontract-derive/src/lib.rs b/ethcontract-derive/src/lib.rs index cbf85405..a2f2238e 100644 --- a/ethcontract-derive/src/lib.rs +++ b/ethcontract-derive/src/lib.rs @@ -8,14 +8,17 @@ extern crate proc_macro; mod spanned; use crate::spanned::{ParseInner, Spanned}; +use anyhow::Result; use ethcontract_common::abi::{Function, Param, ParamType}; use ethcontract_common::abiext::{FunctionExt, ParamTypeExt}; -use ethcontract_generate::{parse_address, Address, Builder}; +use ethcontract_common::artifact::truffle::TruffleLoader; +use ethcontract_common::contract::Network; +use ethcontract_common::{Address, Contract}; +use ethcontract_generate::{parse_address, ContractBuilder, Source}; 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::{ @@ -102,15 +105,21 @@ use syn::{ #[proc_macro] pub fn contract(input: TokenStream) -> TokenStream { let args = parse_macro_input!(input as Spanned); - let span = args.span(); - expand(args.into_inner()) + Source::parse(&args.artifact_path) + .and_then(|s| s.artifact_json()) + .and_then(|j| { + TruffleLoader::new() + .load_contract_from_str(&j) + .map_err(Into::into) + }) + .and_then(|c| expand(args.into_inner(), &c)) .unwrap_or_else(|e| SynError::new(span, format!("{:?}", e)).to_compile_error()) .into() } -fn expand(args: ContractArgs) -> Result> { - Ok(args.into_builder()?.generate()?.into_tokens()) +fn expand(args: ContractArgs, contract: &Contract) -> Result { + Ok(args.into_builder().generate(contract)?.into_tokens()) } /// Contract procedural macro arguments. @@ -122,30 +131,41 @@ struct ContractArgs { } impl ContractArgs { - fn into_builder(self) -> Result> { - let mut builder = Builder::from_source_url(&self.artifact_path)? - .with_visibility_modifier(self.visibility); + fn into_builder(self) -> ContractBuilder { + let mut builder = ContractBuilder::new(); + + builder.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), + match parameter { + Parameter::Mod(name) => builder.contract_mod_override = Some(name), + Parameter::Contract(name) => builder.contract_name_override = Some(name), + Parameter::Crate(name) => builder.runtime_crate_name = name, Parameter::Deployments(deployments) => { - deployments.into_iter().fold(builder, |builder, d| { - builder.add_deployment(d.network_id, d.address, None) - }) + for deployment in deployments { + builder.networks.insert( + deployment.network_id.to_string(), + Network { + address: deployment.address, + deployment_information: None, + }, + ); + } + } + Parameter::Methods(methods) => { + for method in methods { + builder + .method_aliases + .insert(method.signature, method.alias); + } + } + Parameter::EventDerives(derives) => { + builder.event_derives.extend(derives); } - 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) + builder } } diff --git a/ethcontract-generate/README.md b/ethcontract-generate/README.md index 371d26a3..88094c4a 100644 --- a/ethcontract-generate/README.md +++ b/ethcontract-generate/README.md @@ -3,8 +3,9 @@ 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 + +- 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. @@ -27,19 +28,28 @@ behaviour may occur. Then, in your `build.rs` include the following code: -```rs -use ethcontract_generate::Builder; -use std::env; -use std::path::Path; +```rust +use ethcontract_generate::loaders::TruffleLoader; +use ethcontract_generate::ContractBuilder; fn main() { - let dest = env::var("OUT_DIR").unwrap(); - Builder::new("path/to/truffle/build/contract/Contract.json") - .generate() + // Prepare filesystem paths. + let out_dir = std::env::var("OUT_DIR").unwrap(); + let dest = std::path::Path::new(&out_dir).join("rust_coin.rs"); + + // Load a contract. + let contract = TruffleLoader::new() + .load_contract_from_file("../build/Contract.json") + .unwrap(); + + // Generate bindings for it. + ContractBuilder::new() + .generate(&contract) .unwrap() - .write_to_file(Path::new(&dest).join("rust_coin.rs")) + .write_to_file(dest) .unwrap(); } + ``` ## Relation to `ethcontract-derive` diff --git a/ethcontract-generate/src/contract.rs b/ethcontract-generate/src/generate.rs similarity index 66% rename from ethcontract-generate/src/contract.rs rename to ethcontract-generate/src/generate.rs index 2bf9707c..36e162f9 100644 --- a/ethcontract-generate/src/contract.rs +++ b/ethcontract-generate/src/generate.rs @@ -1,5 +1,3 @@ -#![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. @@ -10,83 +8,69 @@ mod events; mod methods; mod types; -use crate::util; -use crate::Args; +use crate::{util, ContractBuilder}; use anyhow::{anyhow, Context as _, Result}; -use ethcontract_common::{Address, Contract, DeploymentInformation}; +use ethcontract_common::contract::Network; +use ethcontract_common::Contract; use inflector::Inflector; use proc_macro2::{Ident, TokenStream}; use quote::quote; use std::collections::HashMap; use syn::{Path, Visibility}; -pub(crate) struct Deployment { - pub address: Address, - pub deployment_information: Option, -} - /// Internal shared context for generating smart contract bindings. -pub(crate) struct Context { +pub(crate) struct Context<'a> { /// The parsed contract. - contract: Contract, + contract: &'a Contract, + /// 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, + networks: HashMap, + /// Manually specified method aliases. method_aliases: HashMap, + /// Derives added to event structs and enums. event_derives: Vec, } -impl Context { +impl<'a> Context<'a> { /// Create a context from the code generation arguments. - fn from_args(args: Args) -> Result { - let contract = { - let artifact_json = args - .artifact_source - .artifact_json() - .context("failed to get artifact JSON")?; - - Contract::from_json(&artifact_json) - .with_context(|| format!("invalid artifact JSON '{}'", artifact_json)) - .with_context(|| { - format!( - "failed to parse artifact from source {:?}", - args.artifact_source, - ) - })? - }; - - let raw_contract_name = if let Some(name) = args.contract_name_override.as_ref() { + fn from_builder(contract: &'a Contract, builder: ContractBuilder) -> Result { + let raw_contract_name = if let Some(name) = &builder.contract_name_override { name } else if !contract.name.is_empty() { &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 \ + 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() { + let runtime_crate = util::ident(&builder.runtime_crate_name); + let visibility = match &builder.visibility_modifier { Some(vis) => syn::parse_str(vis)?, None => Visibility::Inherited, }; - let contract_mod = if let Some(name) = args.contract_mod_override.as_ref() { + let contract_mod = if let Some(name) = &builder.contract_mod_override { util::ident(name) } else { util::ident(&raw_contract_name.to_snake_case()) @@ -97,7 +81,7 @@ impl Context { // 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() { + for (signature, alias) in builder.method_aliases.into_iter() { let alias = syn::parse_str(&alias)?; if method_aliases.insert(signature.clone(), alias).is_some() { return Err(anyhow!( @@ -107,7 +91,7 @@ impl Context { } } - let event_derives = args + let event_derives = builder .event_derives .iter() .map(|derive| syn::parse_str::(derive)) @@ -120,31 +104,15 @@ impl Context { visibility, contract_mod, contract_name, - deployments: args.deployments, + networks: builder.networks, method_aliases, event_derives, }) } } -#[cfg(test)] -impl Default for Context { - fn default() -> Self { - Context { - contract: Contract::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 { - let cx = Context::from_args(args)?; +pub(crate) fn expand(contract: &Contract, builder: ContractBuilder) -> Result { + let cx = Context::from_builder(contract, builder)?; let contract = expand_contract(&cx).context("error expanding contract from its ABI")?; Ok(contract) diff --git a/ethcontract-generate/src/contract/common.rs b/ethcontract-generate/src/generate/common.rs similarity index 94% rename from ethcontract-generate/src/contract/common.rs rename to ethcontract-generate/src/generate/common.rs index 9cde822e..17f6735f 100644 --- a/ethcontract-generate/src/contract/common.rs +++ b/ethcontract-generate/src/generate/common.rs @@ -1,4 +1,4 @@ -use crate::contract::Context; +use crate::generate::Context; use crate::util::expand_doc; use ethcontract_common::artifact::truffle::TruffleLoader; use ethcontract_common::{Address, DeploymentInformation}; @@ -16,17 +16,16 @@ pub(crate) fn expand(cx: &Context) -> TokenStream { .unwrap_or("Generated by `ethcontract`"); let doc = expand_doc(doc_str); - let artifact_json = TruffleLoader::save_to_string(&cx.contract).unwrap(); + let artifact_json = TruffleLoader::save_to_string(cx.contract).unwrap(); - let deployments = cx.deployments.iter().map(|(network_id, deployment)| { - let network_id = Literal::string(&network_id.to_string()); - let address = expand_address(deployment.address); - let deployment_information = - expand_deployment_information(deployment.deployment_information); + let deployments = cx.networks.iter().map(|(chain_id, network)| { + let chain_id = Literal::string(chain_id); + let address = expand_address(network.address); + let deployment_information = expand_deployment_information(network.deployment_information); quote! { artifact.networks.insert( - #network_id.to_owned(), + #chain_id.to_owned(), self::ethcontract::common::contract::Network { address: #address, deployment_information: #deployment_information, diff --git a/ethcontract-generate/src/contract/deployment.rs b/ethcontract-generate/src/generate/deployment.rs similarity index 98% rename from ethcontract-generate/src/contract/deployment.rs rename to ethcontract-generate/src/generate/deployment.rs index 0e2eccf7..036b82cb 100644 --- a/ethcontract-generate/src/contract/deployment.rs +++ b/ethcontract-generate/src/generate/deployment.rs @@ -1,4 +1,4 @@ -use crate::contract::{methods, Context}; +use crate::generate::{methods, Context}; use crate::util; use anyhow::{Context as _, Result}; use inflector::Inflector; @@ -17,7 +17,7 @@ pub(crate) fn expand(cx: &Context) -> Result { } fn expand_deployed(cx: &Context) -> TokenStream { - if cx.contract.networks.is_empty() && cx.deployments.is_empty() { + if cx.contract.networks.is_empty() && cx.networks.is_empty() { return quote! {}; } diff --git a/ethcontract-generate/src/contract/events.rs b/ethcontract-generate/src/generate/events.rs similarity index 92% rename from ethcontract-generate/src/contract/events.rs rename to ethcontract-generate/src/generate/events.rs index a9103b94..1f1b26ee 100644 --- a/ethcontract-generate/src/contract/events.rs +++ b/ethcontract-generate/src/generate/events.rs @@ -1,4 +1,4 @@ -use crate::contract::{types, Context}; +use crate::generate::{types, Context}; use crate::util; use anyhow::Result; use ethcontract_common::abi::{Event, EventParam, Hash, ParamType}; @@ -557,11 +557,15 @@ fn expand_invalid_data() -> TokenStream { #[cfg(test)] mod tests { use super::*; + use crate::ContractBuilder; use ethcontract_common::abi::{EventParam, ParamType}; + use ethcontract_common::Contract; #[test] fn expand_empty_filters() { - assert_quote!(expand_filters(&Context::default()).unwrap(), {}); + let contract = Contract::with_name("Contract"); + let context = Context::from_builder(&contract, ContractBuilder::new()).unwrap(); + assert_quote!(expand_filters(&context).unwrap(), {}); } #[test] @@ -703,38 +707,39 @@ mod tests { #[test] fn expand_enum_for_all_events() { - let context = { - let mut context = Context::default(); - context.contract.abi.events.insert( - "Foo".into(), - vec![Event { - name: "Foo".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Bool, - indexed: false, - }], - anonymous: false, + let mut contract = Contract::with_name("Contract"); + + contract.abi.events.insert( + "Foo".into(), + vec![Event { + name: "Foo".into(), + inputs: vec![EventParam { + name: String::new(), + kind: ParamType::Bool, + indexed: false, }], - ); - context.contract.abi.events.insert( - "Bar".into(), - vec![Event { - name: "Bar".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }], - anonymous: true, + anonymous: false, + }], + ); + contract.abi.events.insert( + "Bar".into(), + vec![Event { + name: "Bar".into(), + inputs: vec![EventParam { + name: String::new(), + kind: ParamType::Address, + indexed: false, }], - ); - context.event_derives = ["Asdf", "a::B", "a::b::c::D"] - .iter() - .map(|derive| syn::parse_str::(derive).unwrap()) - .collect(); - context - }; + anonymous: true, + }], + ); + + let mut context = Context::from_builder(&contract, ContractBuilder::new()).unwrap(); + + context.event_derives = ["Asdf", "a::B", "a::b::c::D"] + .iter() + .map(|derive| syn::parse_str::(derive).unwrap()) + .collect(); assert_quote!(expand_event_enum(&context), { /// A contract event. @@ -748,34 +753,34 @@ mod tests { #[test] fn expand_parse_log_impl_for_all_events() { - let context = { - let mut context = Context::default(); - context.contract.abi.events.insert( - "Foo".into(), - vec![Event { - name: "Foo".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Bool, - indexed: false, - }], - anonymous: false, + let mut contract = Contract::with_name("Contract"); + + contract.abi.events.insert( + "Foo".into(), + vec![Event { + name: "Foo".into(), + inputs: vec![EventParam { + name: String::new(), + kind: ParamType::Bool, + indexed: false, }], - ); - context.contract.abi.events.insert( - "Bar".into(), - vec![Event { - name: "Bar".into(), - inputs: vec![EventParam { - name: String::new(), - kind: ParamType::Address, - indexed: false, - }], - anonymous: true, + anonymous: false, + }], + ); + contract.abi.events.insert( + "Bar".into(), + vec![Event { + name: "Bar".into(), + inputs: vec![EventParam { + name: String::new(), + kind: ParamType::Address, + indexed: false, }], - ); - context - }; + anonymous: true, + }], + ); + + let context = Context::from_builder(&contract, ContractBuilder::new()).unwrap(); let foo_signature = expand_hash(context.contract.abi.event("Foo").unwrap().signature()); let invalid_data = expand_invalid_data(); diff --git a/ethcontract-generate/src/contract/methods.rs b/ethcontract-generate/src/generate/methods.rs similarity index 99% rename from ethcontract-generate/src/contract/methods.rs rename to ethcontract-generate/src/generate/methods.rs index 3154d290..55845480 100644 --- a/ethcontract-generate/src/contract/methods.rs +++ b/ethcontract-generate/src/generate/methods.rs @@ -1,4 +1,4 @@ -use crate::contract::{types, Context}; +use crate::generate::{types, Context}; use crate::util; use anyhow::{anyhow, Context as _, Result}; use ethcontract_common::abi::{Function, Param, StateMutability}; diff --git a/ethcontract-generate/src/contract/types.rs b/ethcontract-generate/src/generate/types.rs similarity index 100% rename from ethcontract-generate/src/contract/types.rs rename to ethcontract-generate/src/generate/types.rs diff --git a/ethcontract-generate/src/lib.rs b/ethcontract-generate/src/lib.rs index 092bc1a7..21527a85 100644 --- a/ethcontract-generate/src/lib.rs +++ b/ethcontract-generate/src/lib.rs @@ -10,202 +10,145 @@ #[path = "test/macros.rs"] mod test_macros; -mod contract; +pub mod source; + +mod generate; mod rustfmt; -mod source; mod util; pub use crate::source::Source; pub use crate::util::parse_address; + +pub use ethcontract_common::artifact::{Artifact, ContractMut, InsertResult}; + +/// Convenience re-imports so that you don't have to add `ethcontract-common` +/// as a dependency. +pub mod loaders { + pub use ethcontract_common::artifact::hardhat::{ + Format as HardHatFormat, HardHatLoader, NetworkEntry, + }; + pub use ethcontract_common::artifact::truffle::TruffleLoader; +} + use anyhow::Result; -use contract::Deployment; -use ethcontract_common::DeploymentInformation; -pub use ethcontract_common::{Address, TransactionHash}; +use ethcontract_common::contract::Network; +use ethcontract_common::Contract; use proc_macro2::TokenStream; use std::collections::HashMap; use std::fs::File; -use std::io::Write; +use std::io::{BufWriter, 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 artifact JSON for the contract whose bindings - /// are being generated. - artifact_source: Source, +/// Builder for generating contract code. Note that no code is generated until +/// the builder is finalized with `generate` or `output`. +pub struct ContractBuilder { /// The runtime crate name to use. - runtime_crate_name: String, + pub runtime_crate_name: String, + /// The visibility modifier to use for the generated module and contract /// re-export. - visibility_modifier: Option, + pub visibility_modifier: Option, + /// Override the contract module name that contains the generated code. - contract_mod_override: Option, + pub contract_mod_override: Option, + /// Override the contract name to use for the generated type. - contract_name_override: Option, + pub contract_name_override: Option, + /// Manually specified deployed contract address and transaction hash. - deployments: HashMap, + pub networks: HashMap, + /// Manually specified contract method aliases. - method_aliases: HashMap, + pub method_aliases: HashMap, + /// Derives added to event structs and enums. - event_derives: Vec, + pub event_derives: Vec, + + /// Format generated code sing locally installed copy of `rustfmt`. + pub rustfmt: bool, } -impl Args { - /// Creates a new builder given the path to a contract's artifact - /// JSON file. - pub fn new(source: Source) -> Self { - Args { - artifact_source: source, - runtime_crate_name: "ethcontract".to_owned(), +impl ContractBuilder { + /// Create a new contract builder with default settings. + pub fn new() -> Self { + ContractBuilder { + runtime_crate_name: "ethcontract".to_string(), 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 artifact - /// JSON file. - pub fn new

(artifact_path: P) -> Self - where - P: AsRef, - { - Builder::with_source(Source::local(artifact_path)) - } - - /// Creates a new builder from a source URL. - pub fn from_source_url(source_url: S) -> Result - where - S: AsRef, - { - 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(), + networks: Default::default(), + method_aliases: Default::default(), + event_derives: vec![], + rustfmt: true, } } - /// Sets the crate name for the runtime crate. This setting is usually only + /// Set 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(mut self, name: S) -> Self - where - S: Into, - { - self.args.runtime_crate_name = name.into(); + pub fn runtime_crate_name(mut self, name: impl Into) -> Self { + self.runtime_crate_name = name.into(); self } - /// Sets an optional visibility modifier for the generated module and + /// Set an optional visibility modifier for the generated module and /// contract re-export. - pub fn with_visibility_modifier(mut self, vis: Option) -> Self - where - S: Into, - { - self.args.visibility_modifier = vis.map(S::into); + pub fn visibility_modifier(mut self, vis: impl Into) -> Self { + self.visibility_modifier = Some(vis.into()); self } - /// Sets the optional contract module name override. - pub fn with_contract_mod_override(mut self, name: Option) -> Self - where - S: Into, - { - self.args.contract_mod_override = name.map(S::into); + /// Set the optional contract module name override. + pub fn contract_mod_override(mut self, name: impl Into) -> Self { + self.contract_mod_override = Some(name.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 + /// Set the optional contract name override. This setting is needed when + /// using an artifact JSON source that does not provide a contract name such /// as Etherscan. - pub fn with_contract_name_override(mut self, name: Option) -> Self - where - S: Into, - { - self.args.contract_name_override = name.map(S::into); + pub fn contract_name_override(mut self, name: impl Into) -> Self { + self.contract_name_override = Some(name.into()); self } - /// Manually adds specifies the deployed address and deployment transaction + /// Add a deployed address and deployment transaction /// hash or block of a contract for a given network. Note that manually specified /// deployments take precedence over deployments in the artifact. /// /// This is useful for integration test scenarios where the address of a /// contract on the test node is deterministic, but the contract address /// is not in the artifact. - pub fn add_deployment( - mut self, - network_id: u32, - address: Address, - deployment_information: Option, - ) -> Self { - let deployment = Deployment { - address, - deployment_information, - }; - self.args.deployments.insert(network_id, deployment); + pub fn add_network(mut self, chain_id: impl Into, network: Network) -> Self { + self.networks.insert(chain_id.into(), network); self } - /// Manually adds specifies the deployed address as a string of a contract - /// for a given network. See `Builder::add_deployment` for more information. + /// Add a deployed address. Parses address from string. + /// See [`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(self, network_id: u32, address: S) -> Self - where - S: AsRef, - { - self.add_deployment( - network_id, - parse_address(address).expect("failed to parse address"), - None, + /// [`parse_address`] for more information on the address string format. + pub fn add_network_str(self, chain_id: impl Into, address: &str) -> Self { + self.add_network( + chain_id, + Network { + address: parse_address(address).expect("failed to parse address"), + deployment_information: None, + }, ) } - /// Manually adds a solidity method alias to specify what the method name + /// Add 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(mut self, signature: S1, alias: S2) -> Self - where - S1: Into, - S2: Into, - { - self.args - .method_aliases - .insert(signature.into(), alias.into()); + pub fn add_method_alias( + mut self, + signature: impl Into, + alias: impl Into, + ) -> Self { + self.method_aliases.insert(signature.into(), alias.into()); self } @@ -214,61 +157,72 @@ impl Builder { /// /// 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; + pub fn rustfmt(mut self, rustfmt: bool) -> Self { + self.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. + /// 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") + /// # use ethcontract_generate::ContractBuilder; + /// let builder = ContractBuilder::new() /// .add_event_derive("serde::Serialize") /// .add_event_derive("serde::Deserialize"); /// ``` - pub fn add_event_derive(mut self, derive: S) -> Self - where - S: Into, - { - self.args.event_derives.push(derive.into()); + pub fn add_event_derive(mut self, derive: impl Into) -> Self { + self.event_derives.push(derive.into()); self } - /// Generates the contract bindings. - pub fn generate(self) -> Result { - let tokens = contract::expand(self.args)?; + /// Generate the contract bindings. + pub fn generate(self, contract: &Contract) -> Result { + let rustfmt = self.rustfmt; Ok(ContractBindings { - tokens, - options: self.options, + tokens: generate::expand(contract, self)?, + rustfmt, }) } } +impl Default for ContractBuilder { + fn default() -> Self { + ContractBuilder::new() + } +} + /// 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, + pub tokens: TokenStream, + + /// Format generated code using locally installed copy of `rustfmt`. + pub rustfmt: bool, } impl ContractBindings { - /// Writes the bindings to a given `Write`. - pub fn write(&self, mut w: W) -> Result<()> - where - W: Write, - { + /// 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 rustfmt(mut self, rustfmt: bool) -> Self { + self.rustfmt = rustfmt; + self + } + + /// Write the bindings to a given `Write`. + pub fn write(&self, mut w: impl Write) -> Result<()> { let source = { let raw = self.tokens.to_string(); - if self.options.rustfmt { + if self.rustfmt { rustfmt::format(&raw).unwrap_or(raw) } else { raw @@ -279,16 +233,14 @@ impl ContractBindings { Ok(()) } - /// Writes the bindings to the specified file. - pub fn write_to_file

(&self, path: P) -> Result<()> - where - P: AsRef, - { + /// Write the bindings to the specified file. + pub fn write_to_file(&self, path: impl AsRef) -> Result<()> { let file = File::create(path)?; - self.write(file) + let writer = BufWriter::new(file); + self.write(writer) } - /// Converts the bindings into its underlying token stream. This allows it + /// Convert 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 diff --git a/ethcontract-generate/src/rustfmt.rs b/ethcontract-generate/src/rustfmt.rs index dd42a12c..312a8136 100644 --- a/ethcontract-generate/src/rustfmt.rs +++ b/ethcontract-generate/src/rustfmt.rs @@ -5,10 +5,7 @@ use std::io::Write; use std::process::{Command, Stdio}; /// Format the raw input source string and return formatted output. -pub fn format(source: S) -> Result -where - S: AsRef, -{ +pub fn format(source: &str) -> Result { let mut rustfmt = Command::new("rustfmt") .args(&["--edition", "2018"]) .stdin(Stdio::piped()) @@ -20,7 +17,7 @@ where .stdin .as_mut() .ok_or_else(|| anyhow!("stdin was not created for `rustfmt` child process"))?; - stdin.write_all(source.as_ref().as_bytes())?; + stdin.write_all(source.as_bytes())?; } let output = rustfmt.wait_with_output()?; diff --git a/ethcontract-generate/src/source.rs b/ethcontract-generate/src/source.rs index 45fc34ba..06d220c8 100644 --- a/ethcontract-generate/src/source.rs +++ b/ethcontract-generate/src/source.rs @@ -1,4 +1,27 @@ -//! Module implements reading of contract artifacts from various sources. +//! Allows loading serialized artifacts from various sources. +//! +//! This module does not provide means for parsing artifacts. For that, +//! use facilities in [`ethcontract_common::artifact`]. +//! +//! # Examples +//! +//! Load artifact from local file: +//! +//! ```no_run +//! # use ethcontract_generate::Source; +//! let json = Source::local("build/contracts/IERC20.json") +//! .artifact_json() +//! .expect("failed to load an artifact"); +//! ``` +//! +//! Load artifact from an NPM package: +//! +//! ```no_run +//! # use ethcontract_generate::Source; +//! let json = Source::npm("npm:@openzeppelin/contracts@2.5.0/build/contracts/IERC20.json") +//! .artifact_json() +//! .expect("failed to load an artifact"); +//! ``` use crate::util; use anyhow::{anyhow, Context, Error, Result}; @@ -13,53 +36,69 @@ use url::Url; /// A source of an artifact JSON. #[derive(Clone, Debug, Eq, PartialEq)] pub enum Source { - /// An artifact or ABI located on the local file system. + /// File on the local file system. Local(PathBuf), - /// An artifact or ABI to be retrieved over HTTP(S). + + /// Resource in the internet, available via HTTP(S). Http(Url), - /// An address of a mainnet contract that has been verified on Etherscan.io. + + /// An address of a mainnet contract, available via [etherscan]. + /// + /// Artifacts loaded from etherstan can be parsed using + /// the [truffle loader]. + /// + /// Note that etherscan rate-limits requests to their API, to avoid this, + /// provide an etherscan API key via the `ETHERSCAN_API_KEY` + /// environment variable. + /// + /// [etherscan]: etherscan.io + /// [truffle loader]: ethcontract_common::artifact::truffle::TruffleLoader Etherscan(Address), - /// The package identifier of an npm package with a path to an artifact - /// or ABI to be retrieved from `unpkg.io`. + + /// The package identifier of an NPM package with a path to an artifact + /// or ABI to be retrieved from [unpkg]. + /// + /// [unpkg]: unpkg.io Npm(String), } impl Source { - /// Parses an artifact source from a string. + /// Parse an artifact source from a string. + /// + /// This method accepts the following: + /// + /// - relative path to a contract JSON file on the local filesystem, + /// for example `build/IERC20.json`. This relative path is rooted + /// in the current working directory. To specify the root for relative + /// paths, use [`with_root`] function; /// - /// 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 an - /// 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 an artifact JSON file. - /// - `http(s)://...` an HTTP url to a contract ABI or a 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.com`. - pub fn parse(source: S) -> Result - where - S: AsRef, - { + /// - absolute path to a contract JSON file on the local filesystem, + /// or a file URL, for example `/build/IERC20.json`, or the same path + /// using URL: `file:///build/IERC20.json`; + /// + /// - an HTTP(S) URL pointing to artifact JSON or contract ABI JSON; + /// + /// - a URL with `etherscan` scheme and a mainnet contract address. + /// For example `etherscan:0xC02AA...`. Alternatively, specify + /// an [etherscan] URL: `https://etherscan.io/address/0xC02AA...`. + /// The contract artifact or ABI will be retrieved through [`etherscan`]; + /// + /// - a URL with `npm` scheme, NPM package name, an optional version + /// and a path (defaulting to the latest version and `index.js`). + /// For example `npm:@openzeppelin/contracts/build/contracts/IERC20.json`. + /// The contract artifact or ABI will be retrieved through [`unpkg`]. + /// + /// [etherscan]: etherscan.io + /// [unpkg]: unpkg.io + pub fn parse(source: &str) -> Result { 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 + /// Parse an artifact source from a string and use the specified root + /// directory for resolving relative paths. See [`parse`] for more details /// on supported source strings. - pub fn with_root(root: P, source: S) -> Result - where - P: AsRef, - S: AsRef, - { + pub fn with_root(root: impl AsRef, source: &str) -> Result { let base = Url::from_directory_path(root) .map_err(|_| anyhow!("root path '{}' is not absolute"))?; let url = base.join(source.as_ref())?; @@ -81,43 +120,39 @@ impl Source { } } - /// Creates a local filesystem source from a path string. - pub fn local

(path: P) -> Self - where - P: AsRef, - { + /// Create a local filesystem source from a path string. + pub fn local(path: impl AsRef) -> Self { Source::Local(path.as_ref().into()) } - /// Creates an HTTP source from a URL. - pub fn http(url: S) -> Result - where - S: AsRef, - { - Ok(Source::Http(Url::parse(url.as_ref())?)) + /// Create an HTTP source from a URL. + pub fn http(url: &str) -> Result { + Ok(Source::Http(Url::parse(url)?)) } - /// Creates an Etherscan source from an address string. - pub fn etherscan(address: S) -> Result - where - S: AsRef, - { - let address = - util::parse_address(address).context("failed to parse address for Etherscan source")?; - Ok(Source::Etherscan(address)) + /// Create an [etherscan] source from contract address on mainnet. + /// + /// [etherscan]: etherscan.io + pub fn etherscan(address: &str) -> Result { + util::parse_address(address) + .context("failed to parse address for Etherscan source") + .map(Source::Etherscan) } - /// Creates an Etherscan source from an address string. - pub fn npm(package_path: S) -> Self - where - S: Into, - { + /// Create an NPM source from a package path. + pub fn npm(package_path: impl Into) -> Self { 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 - /// depending on the source type. + /// Retrieve the source JSON of the artifact. + /// + /// This will either read the JSON from the file system or retrieve + /// a contract ABI from the network, depending on the source type. + /// + /// Contract ABIs will be wrapped into a JSON object, so that you can load + /// them using the [truffle loader]. + /// + /// [truffle loader]: ethcontract_common::artifact::truffle::TruffleLoader pub fn artifact_json(&self) -> Result { match self { Source::Local(path) => get_local_contract(path), @@ -136,7 +171,6 @@ impl FromStr for Source { } } -/// Reads an artifact JSON file from the local filesystem. fn get_local_contract(path: &Path) -> Result { let path = if path.is_relative() { let absolute_path = path.canonicalize().with_context(|| { @@ -157,15 +191,12 @@ fn get_local_contract(path: &Path) -> Result { Ok(abi_or_artifact(json)) } -/// Retrieves an 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)) } -/// 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 { // NOTE: We do not retrieve the bytecode since deploying contracts with the // same bytecode is unreliable as the libraries have already linked and @@ -193,7 +224,6 @@ fn get_etherscan_contract(address: Address) -> Result { Ok(json) } -/// Retrieves an artifact or ABI from an npm package through `unpkg.com`. fn get_npm_contract(package: &str) -> Result { let unpkg_url = format!("https://unpkg.com/{}", package); let json = util::http_get(&unpkg_url) @@ -212,6 +242,7 @@ fn get_npm_contract(package: &str) -> Result { /// /// This needs to be done as currently the contract generation infrastructure /// depends on having an artifact. +// TODO(taminomara): add loader for plain ABIs? fn abi_or_artifact(json: String) -> String { if json.trim().starts_with('[') { format!(r#"{{"abi":{}}}"#, json.trim()) diff --git a/examples/generate/build.rs b/examples/generate/build.rs index ca968b9b..522cb89e 100644 --- a/examples/generate/build.rs +++ b/examples/generate/build.rs @@ -1,13 +1,16 @@ -use ethcontract_generate::{Address, Builder, TransactionHash}; -use std::env; -use std::path::Path; +use ethcontract_generate::loaders::TruffleLoader; +use ethcontract_generate::ContractBuilder; fn main() { - let dest = env::var("OUT_DIR").unwrap(); - Builder::new("../truffle/build/contracts/RustCoin.json") - .add_deployment(42, Address::zero(), Some(TransactionHash::zero().into())) - .generate() + let out_dir = std::env::var("OUT_DIR").unwrap(); + let dest = std::path::Path::new(&out_dir).join("rust_coin.rs"); + + let contract = TruffleLoader::new() + .load_contract_from_file("../truffle/build/contracts/RustCoin.json") + .unwrap(); + ContractBuilder::new() + .generate(&contract) .unwrap() - .write_to_file(Path::new(&dest).join("rust_coin.rs")) + .write_to_file(dest) .unwrap(); }