#![deny(missing_docs)]
use crate::Args;
use anyhow::{anyhow, Result};
use ethcontract_common::truffle::abi::{Function, Param, ParamType};
use ethcontract_common::truffle::Artifact;
use inflector::Inflector;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::quote;
use std::fs;
use syn::Ident as SynIdent;
macro_rules! ident {
($name:expr) => {
Ident::new($name, proc_macro2::Span::call_site())
};
}
struct Context {
artifact_path: Literal,
artifact: Artifact,
runtime_crate: Ident,
}
impl Context {
fn from_args(args: &Args) -> Result<Context> {
let artifact_path = {
let full_path = fs::canonicalize(&args.artifact_path)?;
Literal::string(&full_path.to_string_lossy())
};
let artifact = Artifact::load(&args.artifact_path)?;
let runtime_crate = ident!(&args.runtime_crate_name);
Ok(Context {
artifact_path,
artifact,
runtime_crate,
})
}
}
pub(crate) fn expand_contract(args: &Args) -> Result<TokenStream> {
let cx = Context::from_args(args)?;
let ethcontract = &cx.runtime_crate;
let artifact_path = &cx.artifact_path;
let doc_str = cx
.artifact
.devdoc
.details
.as_ref()
.map(String::as_str)
.unwrap_or("Generated by `ethcontract`");
let doc = expand_doc(doc_str);
let contract_name = ident!(&cx.artifact.contract_name);
let deployed = expand_deployed(&cx);
let deploy = expand_deploy(&cx)?;
let functions = cx
.artifact
.abi
.functions()
.map(|function| expand_function(&cx, function))
.collect::<Result<Vec<_>>>()?;
Ok(quote! {
#doc
#[allow(non_camel_case_types)]
pub struct #contract_name {
instance: #ethcontract::DynInstance,
}
#[allow(dead_code)]
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
impl #contract_name {
pub fn artifact() -> &'static #ethcontract::Artifact {
use #ethcontract::foreign::lazy_static;
use #ethcontract::Artifact;
lazy_static! {
pub static ref ARTIFACT: Artifact = {
Artifact::from_json(include_str!(#artifact_path))
.expect("valid artifact JSON")
};
}
&ARTIFACT
}
pub fn at<F, T>(
web3: &#ethcontract::web3::api::Web3<T>,
address: #ethcontract::Address,
) -> Self
where
F: #ethcontract::web3::futures::Future<Item = #ethcontract::json::Value, Error = #ethcontract::web3::Error> + Send + 'static,
T: #ethcontract::web3::Transport<Out = F> + 'static,
{
use #ethcontract::Instance;
use #ethcontract::transport::DynTransport;
use #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::at(web3, abi, address);
Self{ instance }
}
pub fn address(&self) -> #ethcontract::Address {
self.instance.address()
}
pub fn defaults(&self) -> &#ethcontract::contract::MethodDefaults {
&self.instance.defaults
}
pub fn defaults_mut(&mut self) -> &mut #ethcontract::contract::MethodDefaults {
&mut self.instance.defaults
}
#deployed
#deploy
#( #functions )*
}
impl #ethcontract::contract::Deploy<#ethcontract::transport::DynTransport> for #contract_name {
fn deployed_at(
web3: #ethcontract::web3::api::Web3<#ethcontract::transport::DynTransport>,
abi: #ethcontract::truffle::Abi,
at: #ethcontract::Address,
) -> Self {
use #ethcontract::Instance;
assert_eq!(abi, Self::artifact().abi);
Self {
instance: Instance::at(web3, abi, at),
}
}
}
})
}
fn expand_deployed(cx: &Context) -> TokenStream {
if cx.artifact.networks.is_empty() {
return quote! {};
}
let ethcontract = &cx.runtime_crate;
quote! {
pub fn deployed<F, T>(
web3: &#ethcontract::web3::api::Web3<T>,
) -> #ethcontract::contract::DeployedFuture<#ethcontract::transport::DynTransport, Self>
where
F: #ethcontract::web3::futures::Future<Item = #ethcontract::json::Value, Error = #ethcontract::web3::Error> + Send + 'static,
T: #ethcontract::web3::Transport<Out = F> + 'static,
{
use #ethcontract::Artifact;
use #ethcontract::contract::DeployedFuture;
use #ethcontract::transport::DynTransport;
use #ethcontract::web3::api::Web3;
let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let artifact = { let artifact = Self::artifact();
Artifact {
abi: artifact.abi.clone(),
networks: artifact.networks.clone(),
..Artifact::empty()
}
};
DeployedFuture::from_args(web3, artifact)
}
}
}
fn expand_deploy(cx: &Context) -> Result<TokenStream> {
if cx.artifact.bytecode.is_empty() {
return Ok(quote! {});
}
let ethcontract = &cx.runtime_crate;
let doc = expand_doc("Generated by `ethcontract`");
let (input, arg) = match cx.artifact.abi.constructor() {
Some(contructor) => (
expand_inputs(cx, &contructor.inputs)?,
expand_inputs_call_arg(&contructor.inputs),
),
None => (quote! {}, quote! {()}),
};
let lib_params: Vec<_> = cx
.artifact
.bytecode
.undefined_libraries()
.map(|name| Param {
name: name.to_snake_case(),
kind: ParamType::Address,
})
.collect();
let lib_input = expand_inputs(cx, &lib_params)?;
let link = if !lib_params.is_empty() {
let link_libraries = cx
.artifact
.bytecode
.undefined_libraries()
.zip(lib_params.iter())
.map(|(name, lib_param)| {
let name = Literal::string(&name);
let address = ident!(&lib_param.name);
quote! {
artifact.bytecode.link(#name, #address).expect("valid library");
}
});
quote! {
let mut artifact = artifact;
#( #link_libraries )*
}
} else {
quote! {}
};
Ok(quote! {
#doc
pub fn builder<F, T>(
web3: &#ethcontract::web3::api::Web3<T> #lib_input #input ,
) -> #ethcontract::DynDeployBuilder<Self>
where
F: #ethcontract::web3::futures::Future<Item = #ethcontract::json::Value, Error = #ethcontract::web3::Error> + Send + 'static,
T: #ethcontract::web3::Transport<Out = F> + 'static,
{
use #ethcontract::{Artifact, DynTransport};
use #ethcontract::contract::DeployBuilder;
use #ethcontract::web3::api::Web3;
let transport = DynTransport::new(web3.transport().clone());
let web3 = Web3::new(transport);
let artifact = { let artifact = Self::artifact();
Artifact {
abi: artifact.abi.clone(),
bytecode: artifact.bytecode.clone(),
..Artifact::empty()
}
};
#link
DeployBuilder::new(web3, artifact, #arg).expect("valid deployment args")
}
})
}
fn expand_function(cx: &Context, function: &Function) -> Result<TokenStream> {
let ethcontract = &cx.runtime_crate;
let name = ident!(&function.name.to_snake_case());
let name_str = Literal::string(&function.name);
let signature = function_signature(&function);
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 = expand_doc(doc_str);
let input = expand_inputs(cx, &function.inputs)?;
let outputs = expand_fn_outputs(cx, &function)?;
let (method, result_type_name) = if function.constant {
(quote! { view_method }, quote! { DynViewMethodBuilder })
} else {
(quote! { method }, quote! { DynMethodBuilder })
};
let result = quote! { #ethcontract::#result_type_name<#outputs> };
let arg = expand_inputs_call_arg(&function.inputs);
Ok(quote! {
#doc
pub fn #name(&self #input) -> #result {
self.instance.#method(#name_str, #arg)
.expect("generated call")
}
})
}
fn function_signature(function: &Function) -> String {
let types = match function.inputs.len() {
0 => String::new(),
_ => {
let mut params = function.inputs.iter().map(|param| ¶m.kind);
let first = params.next().expect("at least one param").to_string();
params.fold(first, |acc, param| format!("{},{}", acc, param))
}
};
format!("{}({})", function.name, types)
}
fn expand_inputs(cx: &Context, inputs: &[Param]) -> Result<TokenStream> {
let params = inputs
.iter()
.enumerate()
.map(|(i, param)| {
let name = expand_input_name(i, ¶m.name);
let kind = expand_type(cx, ¶m.kind)?;
Ok(quote! { #name: #kind })
})
.collect::<Result<Vec<_>>>()?;
Ok(quote! { #( , #params )* })
}
fn expand_input_name(index: usize, name: &str) -> TokenStream {
let name_str = match name {
"" => format!("p{}", index),
n => n.to_snake_case(),
};
let name =
syn::parse_str::<SynIdent>(&name_str).unwrap_or_else(|_| ident!(&format!("{}_", name_str)));
quote! { #name }
}
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
let names = inputs
.iter()
.enumerate()
.map(|(i, param)| expand_input_name(i, ¶m.name));
quote! { ( #( #names ,)* ) }
}
fn expand_fn_outputs(cx: &Context, function: &Function) -> Result<TokenStream> {
match function.outputs.len() {
0 => Ok(quote! { () }),
1 => expand_type(cx, &function.outputs[0].kind),
_ => {
let types = function
.outputs
.iter()
.map(|param| expand_type(cx, ¶m.kind))
.collect::<Result<Vec<_>>>()?;
Ok(quote! { (#( #types ),*) })
}
}
}
fn expand_type(cx: &Context, kind: &ParamType) -> Result<TokenStream> {
let ethcontract = &cx.runtime_crate;
match kind {
ParamType::Address => Ok(quote! { #ethcontract::Address }),
ParamType::Bytes => Ok(quote! { Vec<u8> }),
ParamType::Int(n) | ParamType::Uint(n) => match n / 8 {
1..=8 => Ok(quote! { u64 }),
9..=16 => Ok(quote! { #ethcontract::web3::types::U128 }),
17..=32 => Ok(quote! { #ethcontract::U256 }),
_ => Err(anyhow!("unsupported solidity type int{}", n)),
},
ParamType::Bool => Ok(quote! { bool }),
ParamType::String => Ok(quote! { String }),
ParamType::Array(t) => {
let inner = expand_type(cx, t)?;
Ok(quote! { Vec<#inner> })
}
ParamType::FixedBytes(n) => {
let size = Literal::usize_unsuffixed(*n);
Ok(quote! { [u8; #size] })
}
ParamType::FixedArray(t, n) => {
let inner = expand_type(cx, t)?;
let size = Literal::usize_unsuffixed(*n);
Ok(quote! { [#inner; #size] })
}
}
}
fn expand_doc(s: &str) -> TokenStream {
let doc = Literal::string(s);
quote! {
#[doc = #doc]
}
}