feat: generate rust structs from solidity JSON ABI (#378)

* add raw abi model

* feat: simplify struct representation

* feat: add struct generation

* use structs as function input

* fix: failing test

* add example

* rustfmt
This commit is contained in:
Matthias Seitz 2021-08-16 09:29:44 +02:00 committed by GitHub
parent b38685c574
commit ba5f650dec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 963 additions and 136 deletions

1
Cargo.lock generated
View File

@ -846,6 +846,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"reqwest", "reqwest",
"serde",
"serde_json", "serde_json",
"syn", "syn",
"url", "url",

View File

@ -19,6 +19,7 @@ quote = "1.0"
syn = "1.0.12" syn = "1.0.12"
url = "2.1" url = "2.1"
serde_json = "1.0.61" serde_json = "1.0.61"
serde = { version = "1.0.124", features = ["derive"] }
hex = { version = "0.4.2", default-features = false, features = ["std"] } hex = { version = "0.4.2", default-features = false, features = ["std"] }
reqwest = { version = "0.11.3", features = ["blocking"] } reqwest = { version = "0.11.3", features = ["blocking"] }
once_cell = { version = "1.8.0", default-features = false } once_cell = { version = "1.8.0", default-features = false }

View File

@ -7,6 +7,8 @@ mod types;
use super::util; use super::util;
use super::Abigen; use super::Abigen;
use crate::contract::structs::InternalStructs;
use crate::rawabi::RawAbi;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use ethers_core::abi::AbiParser; use ethers_core::abi::AbiParser;
use ethers_core::{ use ethers_core::{
@ -30,6 +32,9 @@ pub(crate) struct Context {
/// The parser used for human readable format /// The parser used for human readable format
abi_parser: AbiParser, abi_parser: AbiParser,
/// Contains all the solidity structs extracted from the JSON ABI.
internal_structs: InternalStructs,
/// Was the ABI in human readable format? /// Was the ABI in human readable format?
human_readable: bool, human_readable: bool,
@ -118,6 +123,14 @@ impl Context {
(abi_parser.parse_str(&abi_str)?, true) (abi_parser.parse_str(&abi_str)?, true)
}; };
// try to extract all the solidity structs from the normal JSON ABI
// we need to parse the json abi again because we need the internalType fields which are omitted by ethabi.
let internal_structs = (!human_readable)
.then(|| serde_json::from_str::<RawAbi>(&abi_str).ok())
.flatten()
.map(InternalStructs::new)
.unwrap_or_default();
let contract_name = util::ident(&args.contract_name); let contract_name = util::ident(&args.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
@ -146,6 +159,7 @@ impl Context {
human_readable, human_readable,
abi_str: Literal::string(&abi_str), abi_str: Literal::string(&abi_str),
abi_parser, abi_parser,
internal_structs,
contract_name, contract_name,
method_aliases, method_aliases,
event_derives, event_derives,

View File

@ -22,40 +22,107 @@ impl Context {
.flatten() .flatten()
.map(|function| { .map(|function| {
let signature = function.abi_signature(); let signature = function.abi_signature();
expand_function(function, aliases.remove(&signature)) self.expand_function(function, aliases.remove(&signature))
.with_context(|| format!("error expanding function '{}'", signature)) .with_context(|| format!("error expanding function '{}'", signature))
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
Ok(quote! { #( #functions )* }) Ok(quote! { #( #functions )* })
} }
}
#[allow(unused)] fn expand_inputs_call_arg_with_structs(
fn expand_function(function: &Function, alias: Option<Ident>) -> Result<TokenStream> { &self,
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); fun: &Function,
let selector = expand_selector(function.selector()); ) -> Result<(TokenStream, TokenStream)> {
let mut args = Vec::with_capacity(fun.inputs.len());
let input = expand_inputs(&function.inputs)?; let mut call_args = Vec::with_capacity(fun.inputs.len());
for (i, param) in fun.inputs.iter().enumerate() {
let outputs = expand_fn_outputs(&function.outputs)?; let name = util::expand_input_name(i, &param.name);
let ty = self.expand_input_param(fun, &param.name, &param.kind)?;
let result = quote! { ethers_contract::builders::ContractCall<M, #outputs> }; args.push(quote! { #name: #ty });
let call_arg = match param.kind {
let arg = expand_inputs_call_arg(&function.inputs); // this is awkward edge case where the function inputs are a single struct
let doc = util::expand_doc(&format!( // we need to force this argument into a tuple so it gets expanded to `((#name,))`
"Calls the contract's `{}` (0x{}) function", // this is currently necessary because internally `flatten_tokens` is called which removes the outermost `tuple` level
function.name, // and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens`
hex::encode(function.selector()) ParamType::Tuple(_) if fun.inputs.len() == 1 => {
)); // make sure the tuple gets converted to `Token::Tuple`
Ok(quote! { quote! {(#name,)}
}
#doc _ => name,
pub fn #name(&self #input) -> #result { };
self.0.method_hash(#selector, #arg) call_args.push(call_arg);
.expect("method not found (this should never happen)")
} }
}) let args = quote! { #( , #args )* };
let call_args = match call_args.len() {
0 => quote! { () },
1 => quote! { #( #call_args )* },
_ => quote! { ( #(#call_args, )* ) },
};
Ok((args, call_args))
}
fn expand_input_param(
&self,
fun: &Function,
param: &str,
kind: &ParamType,
) -> Result<TokenStream> {
match kind {
ParamType::Array(ty) => {
let ty = self.expand_input_param(fun, param, ty)?;
Ok(quote! {
::std::vec::Vec<#ty>
})
}
ParamType::FixedArray(ty, size) => {
let ty = self.expand_input_param(fun, param, ty)?;
let size = *size;
Ok(quote! {[#ty; #size]})
}
ParamType::Tuple(_) => {
let ty = if let Some(rust_struct_name) = self
.internal_structs
.get_function_input_struct_type(&fun.name, param)
{
let ident = util::ident(rust_struct_name);
quote! {#ident}
} else {
types::expand(kind)?
};
Ok(ty)
}
_ => types::expand(kind),
}
}
#[allow(unused)]
fn expand_function(&self, function: &Function, alias: Option<Ident>) -> Result<TokenStream> {
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case()));
let selector = expand_selector(function.selector());
// TODO use structs
let outputs = expand_fn_outputs(&function.outputs)?;
let result = quote! { ethers_contract::builders::ContractCall<M, #outputs> };
let (input, arg) = self.expand_inputs_call_arg_with_structs(function)?;
let doc = util::expand_doc(&format!(
"Calls the contract's `{}` (0x{}) function",
function.name,
hex::encode(function.selector())
));
Ok(quote! {
#doc
pub fn #name(&self #input) -> #result {
self.0.method_hash(#selector, #arg)
.expect("method not found (this should never happen)")
}
})
}
} }
// converts the function params to name/type pairs // converts the function params to name/type pairs

View File

@ -1,13 +1,21 @@
//! Methods for expanding structs //! Methods for expanding structs
use std::collections::{HashMap, VecDeque};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use inflector::Inflector; use inflector::Inflector;
use proc_macro2::{Literal, TokenStream}; use proc_macro2::{Literal, TokenStream};
use quote::quote; use quote::quote;
use ethers_core::abi::{struct_def::FieldType, ParamType}; use ethers_core::abi::{
param_type::Reader,
struct_def::{FieldDeclaration, FieldType, StructFieldType, StructType},
ParamType, SolStruct,
};
use crate::contract::{types, Context}; use crate::contract::{types, Context};
use crate::rawabi::{Component, RawAbi};
use crate::util; use crate::util;
use std::any::Any;
impl Context { impl Context {
/// Generate corresponding types for structs parsed from a human readable ABI /// Generate corresponding types for structs parsed from a human readable ABI
@ -16,8 +24,102 @@ impl Context {
/// in fact present in the `AbiParser`, this is sound because `AbiParser::parse` would have /// in fact present in the `AbiParser`, this is sound because `AbiParser::parse` would have
/// failed already /// failed already
pub fn abi_structs(&self) -> Result<TokenStream> { pub fn abi_structs(&self) -> Result<TokenStream> {
let mut structs = Vec::with_capacity(self.abi_parser.structs.len()); if self.human_readable {
for (name, sol_struct) in &self.abi_parser.structs { self.gen_human_readable_structs()
} else {
self.gen_internal_structs()
}
}
/// Returns the `TokenStream` with all the internal structs extracted form the JSON ABI
fn gen_internal_structs(&self) -> Result<TokenStream> {
let mut structs = TokenStream::new();
let mut ids: Vec<_> = self.internal_structs.structs.keys().collect();
ids.sort();
for id in ids {
let sol_struct = &self.internal_structs.structs[id];
let struct_name = self
.internal_structs
.rust_type_names
.get(id)
.context(format!("No types found for {}", id))?;
let tuple = self
.internal_structs
.struct_tuples
.get(id)
.context(format!("No types found for {}", id))?
.clone();
structs.extend(self.expand_internal_struct(struct_name, sol_struct, tuple)?);
}
Ok(structs)
}
/// Expand all structs parsed from the internal types of the JSON ABI
fn expand_internal_struct(
&self,
name: &str,
sol_struct: &SolStruct,
tuple: ParamType,
) -> Result<TokenStream> {
let mut fields = Vec::with_capacity(sol_struct.fields().len());
for field in sol_struct.fields() {
let field_name = util::ident(&field.name().to_snake_case());
match field.r#type() {
FieldType::Elementary(ty) => {
let ty = types::expand(ty)?;
fields.push(quote! { pub #field_name: #ty });
}
FieldType::Struct(struct_ty) => {
let ty = expand_struct_type(struct_ty);
fields.push(quote! { pub #field_name: #ty });
}
FieldType::Mapping(_) => {
return Err(anyhow::anyhow!(
"Mapping types in struct `{}` are not supported {:?}",
name,
field
));
}
}
}
let sig = if let ParamType::Tuple(ref tokens) = tuple {
tokens
.iter()
.map(|kind| kind.to_string())
.collect::<Vec<_>>()
.join(",")
} else {
"".to_string()
};
let abi_signature = format!("{}({})", name, sig,);
let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature));
let name = util::ident(name);
// use the same derives as for events
let derives = &self.event_derives;
let derives = quote! {#(#derives),*};
Ok(quote! {
#abi_signature_doc
#[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)]
pub struct #name {
#( #fields ),*
}
})
}
/// Expand all structs parsed from the human readable ABI
fn gen_human_readable_structs(&self) -> Result<TokenStream> {
let mut structs = TokenStream::new();
let mut names: Vec<_> = self.abi_parser.structs.keys().collect();
names.sort();
for name in names {
let sol_struct = &self.abi_parser.structs[name];
let mut fields = Vec::with_capacity(sol_struct.fields().len()); let mut fields = Vec::with_capacity(sol_struct.fields().len());
let mut param_types = Vec::with_capacity(sol_struct.fields().len()); let mut param_types = Vec::with_capacity(sol_struct.fields().len());
for field in sol_struct.fields() { for field in sol_struct.fields() {
@ -29,44 +131,19 @@ impl Context {
fields.push(quote! { pub #field_name: #ty }); fields.push(quote! { pub #field_name: #ty });
} }
FieldType::Struct(struct_ty) => { FieldType::Struct(struct_ty) => {
let ty = util::ident(struct_ty.name()); let ty = expand_struct_type(struct_ty);
fields.push(quote! { pub #field_name: #ty }); fields.push(quote! { pub #field_name: #ty });
let name = struct_ty.name();
let tuple = self let tuple = self
.abi_parser .abi_parser
.struct_tuples .struct_tuples
.get(struct_ty.name()) .get(name)
.context(format!("No types found for {}", struct_ty.name()))? .context(format!("No types found for {}", name))?
.clone(); .clone();
param_types.push(ParamType::Tuple(tuple)); let tuple = ParamType::Tuple(tuple);
}
FieldType::StructArray(struct_ty) => {
let ty = util::ident(struct_ty.name());
fields.push(quote! { pub #field_name: ::std::vec::Vec<#ty> });
let tuple = self param_types.push(struct_ty.as_param(tuple));
.abi_parser
.struct_tuples
.get(struct_ty.name())
.context(format!("No types found for {}", struct_ty.name()))?
.clone();
param_types.push(ParamType::Array(Box::new(ParamType::Tuple(tuple))));
}
FieldType::FixedStructArray(struct_ty, len) => {
let ty = util::ident(struct_ty.name());
let size = Literal::usize_unsuffixed(*len);
fields.push(quote! { pub #field_name: [#ty; #size] });
let tuple = self
.abi_parser
.struct_tuples
.get(struct_ty.name())
.context(format!("No types found for {}", struct_ty.name()))?
.clone();
param_types.push(ParamType::FixedArray(
Box::new(ParamType::Tuple(tuple)),
*len,
));
} }
FieldType::Mapping(_) => { FieldType::Mapping(_) => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@ -96,14 +173,349 @@ impl Context {
let derives = &self.event_derives; let derives = &self.event_derives;
let derives = quote! {#(#derives),*}; let derives = quote! {#(#derives),*};
structs.push(quote! { structs.extend(quote! {
#abi_signature_doc #abi_signature_doc
#[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)] #[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)]
pub struct #name { pub struct #name {
#( #fields ),* #( #fields ),*
} }
}); });
} }
Ok(quote! {#( #structs )*}) Ok(structs)
}
}
/// Helper to match `ethabi::Param`s with structs and nested structs
///
/// This is currently used to get access to all the unique solidity structs used as function in/output until `ethabi` supports it as well.
#[derive(Debug, Clone, Default)]
pub struct InternalStructs {
/// All unique internal types that are function inputs or outputs
top_level_internal_types: HashMap<String, Component>,
/// (function name, param name) -> struct which are the identifying properties we get the name from ethabi.
function_params: HashMap<(String, String), String>,
/// (function name) -> Vec<structs> all structs the function returns
outputs: HashMap<String, Vec<String>>,
/// All the structs extracted from the abi with their identifier as key
structs: HashMap<String, SolStruct>,
/// solidity structs as tuples
struct_tuples: HashMap<String, ParamType>,
/// Contains the names for the rust types (id -> rust type name)
rust_type_names: HashMap<String, String>,
}
impl InternalStructs {
pub fn new(abi: RawAbi) -> Self {
let mut top_level_internal_types = HashMap::new();
let mut function_params = HashMap::new();
let mut outputs = HashMap::new();
let mut structs = HashMap::new();
for item in abi
.into_iter()
.filter(|item| item.type_field == "constructor" || item.type_field == "function")
{
if let Some(name) = item.name {
for input in item.inputs {
if let Some(ty) = input
.internal_type
.as_deref()
.filter(|ty| ty.starts_with("struct "))
.map(struct_type_identifier)
{
function_params.insert((name.clone(), input.name.clone()), ty.to_string());
top_level_internal_types.insert(ty.to_string(), input);
}
}
let mut output_structs = Vec::new();
for output in item.outputs {
if let Some(ty) = output
.internal_type
.as_deref()
.filter(|ty| ty.starts_with("struct "))
.map(struct_type_identifier)
{
output_structs.push(ty.to_string());
top_level_internal_types.insert(ty.to_string(), output);
}
}
outputs.insert(name, output_structs);
}
}
// turn each top level internal type (function input/output) and their nested types
// into a struct will create all structs
for component in top_level_internal_types.values() {
insert_structs(&mut structs, component);
}
// determine the `ParamType` representation of each struct
let struct_tuples = resolve_struct_tuples(&structs);
// name -> (id, projections)
let mut type_names: HashMap<String, (String, Vec<String>)> =
HashMap::with_capacity(structs.len());
for id in structs.keys() {
let name = struct_type_name(id).to_pascal_case();
let projections = struct_type_projections(id);
insert_rust_type_name(&mut type_names, name, projections, id.clone());
}
Self {
top_level_internal_types,
function_params,
outputs,
structs,
struct_tuples,
rust_type_names: type_names
.into_iter()
.map(|(rust_name, (id, _))| (id, rust_name))
.collect(),
}
}
/// Returns the name of the rust type that will be generated if the given input is a struct
/// NOTE: this does not account for arrays or fixed arrays
pub fn get_function_input_struct_type(&self, function: &str, input: &str) -> Option<&str> {
let key = (function.to_string(), input.to_string());
self.function_params
.get(&key)
.and_then(|id| self.rust_type_names.get(id))
.map(String::as_str)
}
}
/// This will determine the name of the rust type and will make sure that possible collisions are resolved by adjusting the actual Rust name of the structure, e.g. `LibraryA.Point` and `LibraryB.Point` to `LibraryAPoint` and `LibraryBPoint`.
fn insert_rust_type_name(
type_names: &mut HashMap<String, (String, Vec<String>)>,
mut name: String,
mut projections: Vec<String>,
id: String,
) {
if let Some((other_id, mut other_projections)) = type_names.remove(&name) {
let mut other_name = name.clone();
// name collision `A.name` `B.name`, rename to `AName`, `BName`
if !other_projections.is_empty() {
other_name = format!(
"{}{}",
other_projections.remove(0).to_pascal_case(),
other_name
);
}
insert_rust_type_name(type_names, other_name, other_projections, other_id);
if !projections.is_empty() {
name = format!("{}{}", projections.remove(0).to_pascal_case(), name);
}
insert_rust_type_name(type_names, name, projections, id);
} else {
type_names.insert(name, (id, projections));
}
}
/// Tries to determine the `ParamType::Tuple` for every struct.
///
/// If a structure has nested structures, these must be determined first, essentially starting with structures consisting of only elementary types before moving on to higher level structures, for example `Proof {point: Point}, Point {x:int, y:int}` start by converting Point into a tuple of `x` and `y` and then substituting `point` with this within `Proof`.
fn resolve_struct_tuples(all_structs: &HashMap<String, SolStruct>) -> HashMap<String, ParamType> {
let mut params = HashMap::new();
let mut structs: VecDeque<_> = all_structs.iter().collect();
// keep track of how often we retried nested structs
let mut sequential_retries = 0;
'outer: while let Some((id, ty)) = structs.pop_front() {
if sequential_retries > structs.len() {
break;
}
if let Some(tuple) = ty.as_tuple() {
params.insert(id.to_string(), tuple);
} else {
// try to substitute all nested struct types with their `ParamTypes`
let mut struct_params = Vec::with_capacity(ty.fields.len());
for field in ty.fields() {
match field.ty {
FieldType::Elementary(ref param) => {
struct_params.push(param.clone());
}
FieldType::Struct(ref field_ty) => {
// nested struct
let ty_id = field_ty.identifier();
if let Some(nested) = params.get(&ty_id).cloned() {
match field_ty {
StructFieldType::Type(_) => struct_params.push(nested),
StructFieldType::Array(_) => {
struct_params.push(ParamType::Array(Box::new(nested)));
}
StructFieldType::FixedArray(_, size) => {
struct_params
.push(ParamType::FixedArray(Box::new(nested), *size));
}
}
} else {
// struct field needs to be resolved first
structs.push_back((id, ty));
sequential_retries += 1;
continue 'outer;
}
}
_ => {
unreachable!("mapping types are unsupported")
}
}
}
params.insert(id.to_string(), ParamType::Tuple(struct_params));
}
// we resolved a new param, so we can try all again
sequential_retries = 0;
}
params
}
/// turns the tuple component into a struct if it's still missing in the map, including all inner structs
fn insert_structs(structs: &mut HashMap<String, SolStruct>, tuple: &Component) {
if let Some(internal_ty) = tuple.internal_type.as_ref() {
let ident = struct_type_identifier(internal_ty);
if structs.contains_key(ident) {
return;
}
if let Some(fields) = tuple
.components
.iter()
.map(|f| {
Reader::read(&f.type_field)
.ok()
.and_then(|kind| field(structs, f, kind))
})
.collect::<Option<Vec<_>>>()
{
let s = SolStruct {
name: ident.to_string(),
fields,
};
structs.insert(ident.to_string(), s);
}
}
}
/// Determines the type of the field component
fn field(
structs: &mut HashMap<String, SolStruct>,
field_component: &Component,
kind: ParamType,
) -> Option<FieldDeclaration> {
match kind {
ParamType::Array(ty) => {
let FieldDeclaration { ty, .. } = field(structs, field_component, *ty)?;
match ty {
FieldType::Elementary(kind) => {
// this arm represents all the elementary types like address, uint...
Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Elementary(ParamType::Array(Box::new(kind))),
))
}
FieldType::Struct(ty) => Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Struct(StructFieldType::Array(Box::new(ty))),
)),
_ => {
unreachable!("no mappings types support as function inputs or outputs")
}
}
}
ParamType::FixedArray(ty, size) => {
let FieldDeclaration { ty, .. } = field(structs, field_component, *ty)?;
match ty {
FieldType::Elementary(kind) => {
// this arm represents all the elementary types like address, uint...
Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Elementary(ParamType::FixedArray(Box::new(kind), size)),
))
}
FieldType::Struct(ty) => Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Struct(StructFieldType::FixedArray(Box::new(ty), size)),
)),
_ => {
unreachable!("no mappings types support as function inputs or outputs")
}
}
}
ParamType::Tuple(_) => {
insert_structs(structs, field_component);
let internal_type = field_component.internal_type.as_ref()?;
let ty = struct_type_identifier(internal_type);
// split the identifier into the name and all projections:
// `A.B.C.name` -> name, [A,B,C]
let mut idents = ty.rsplit('.');
let name = idents.next().unwrap().to_string();
let projections = idents.rev().map(str::to_string).collect();
Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Struct(StructFieldType::Type(StructType::new(name, projections))),
))
}
elementary => Some(FieldDeclaration::new(
field_component.name.clone(),
FieldType::Elementary(elementary),
)),
}
}
/// `struct Pairing.G2Point[]` -> `G2Point`
fn struct_type_name(name: &str) -> &str {
struct_type_identifier(name).rsplit('.').next().unwrap()
}
/// `Pairing.G2Point` -> `Pairing.G2Point`
fn struct_type_identifier(name: &str) -> &str {
name.trim_start_matches("struct ")
.split('[')
.next()
.unwrap()
}
/// `struct Pairing.Nested.G2Point[]` -> `[Pairing, Nested]`
fn struct_type_projections(name: &str) -> Vec<String> {
let id = struct_type_identifier(name);
let mut iter = id.rsplit('.');
iter.next();
iter.rev().map(str::to_string).collect()
}
/// Expands to the rust struct type
fn expand_struct_type(struct_ty: &StructFieldType) -> TokenStream {
match struct_ty {
StructFieldType::Type(ty) => {
let ty = util::ident(ty.name());
quote! {#ty}
}
StructFieldType::Array(ty) => {
let ty = expand_struct_type(&*ty);
quote! {::std::vec::Vec<#ty>}
}
StructFieldType::FixedArray(ty, size) => {
let ty = expand_struct_type(&*ty);
quote! { [#ty; #size]}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_determine_structs() {
const VERIFIER_ABI: &str =
include_str!("../../../tests/solidity-contracts/verifier_abi.json");
let abi = serde_json::from_str::<RawAbi>(VERIFIER_ABI).unwrap();
let internal = InternalStructs::new(abi);
dbg!(internal.rust_type_names);
} }
} }

View File

@ -15,6 +15,7 @@ mod test_macros;
mod contract; mod contract;
use contract::Context; use contract::Context;
pub mod rawabi;
mod rustfmt; mod rustfmt;
mod source; mod source;
mod util; mod util;

View File

@ -0,0 +1,48 @@
//! This is a basic representation of a contract ABI that does no post processing but contains the raw content of the ABI.
#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
/// Contract ABI as a list of items where each item can be a function, constructor or event
pub type RawAbi = Vec<Item>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Item {
#[serde(default)]
pub inputs: Vec<Component>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_mutability: Option<String>,
#[serde(rename = "type")]
pub type_field: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub outputs: Vec<Component>,
}
/// Either
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Component {
#[serde(
rename = "internalType",
default,
skip_serializing_if = "Option::is_none"
)]
pub internal_type: Option<String>,
pub name: String,
#[serde(rename = "type")]
pub type_field: String,
#[serde(default)]
pub components: Vec<Component>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_parse_raw_abi() {
const VERIFIER_ABI: &str = include_str!("../../tests/solidity-contracts/verifier_abi.json");
let _ = serde_json::from_str::<RawAbi>(VERIFIER_ABI).unwrap();
}
}

View File

@ -43,24 +43,33 @@ fn can_gen_structs_readable() {
); );
} }
// NOTE(mattsse): There is currently a limitation with the `ethabi` crate's `Reader` #[test]
// that doesn't support arrays of tuples; https://github.com/gakonst/ethabi/pull/1 should fix this fn can_gen_structs_with_arrays_readable() {
// See also https://github.com/rust-ethereum/ethabi/issues/178 and abigen!(
// https://github.com/rust-ethereum/ethabi/pull/186 SimpleContract,
r#"[
struct Value {address addr; string value;}
struct Addresses {address[] addr; string s;}
event ValueChanged(Value indexed old, Value newValue, Addresses[] _a)
]"#,
event_derives(serde::Deserialize, serde::Serialize)
);
assert_eq!(
"ValueChanged((address,string),(address,string),(address[],string)[])",
ValueChangedFilter::abi_signature()
);
}
// #[test] fn assert_tokenizeable<T: Tokenizable>() {}
// fn can_gen_structs_with_arrays_readable() {
// abigen!( #[test]
// SimpleContract, fn can_generate_internal_structs() {
// r#"[ abigen!(
// struct Value {address addr; string value;} VerifierContract,
// struct Addresses {address[] addr; string s;} "ethers-contract/tests/solidity-contracts/verifier_abi.json",
// event ValueChanged(Value indexed old, Value newValue, Addresses[] _a) event_derives(serde::Deserialize, serde::Serialize)
// ]"#, );
// event_derives(serde::Deserialize, serde::Serialize) assert_tokenizeable::<VerifyingKey>();
// ); assert_tokenizeable::<G1Point>();
// assert_eq!( assert_tokenizeable::<G2Point>();
// "ValueChanged((address,string),(address,string),(address[],string)[])", }
// ValueChangedFilter::abi_signature()
// );
// }

View File

@ -0,0 +1,176 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "uint256[]",
"name": "input",
"type": "uint256[]"
},
{
"components": [
{
"components": [
{
"internalType": "uint256",
"name": "X",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "Y",
"type": "uint256"
}
],
"internalType": "struct Pairing.G1Point",
"name": "A",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256[2]",
"name": "X",
"type": "uint256[2]"
},
{
"internalType": "uint256[2]",
"name": "Y",
"type": "uint256[2]"
}
],
"internalType": "struct Pairing.G2Point",
"name": "B",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256",
"name": "X",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "Y",
"type": "uint256"
}
],
"internalType": "struct Pairing.G1Point",
"name": "C",
"type": "tuple"
}
],
"internalType": "struct Verifier.Proof",
"name": "proof",
"type": "tuple"
},
{
"components": [
{
"components": [
{
"internalType": "uint256",
"name": "X",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "Y",
"type": "uint256"
}
],
"internalType": "struct Pairing.G1Point",
"name": "alfa1",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256[2]",
"name": "X",
"type": "uint256[2]"
},
{
"internalType": "uint256[2]",
"name": "Y",
"type": "uint256[2]"
}
],
"internalType": "struct Pairing.G2Point",
"name": "beta2",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256[2]",
"name": "X",
"type": "uint256[2]"
},
{
"internalType": "uint256[2]",
"name": "Y",
"type": "uint256[2]"
}
],
"internalType": "struct Pairing.G2Point",
"name": "gamma2",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256[2]",
"name": "X",
"type": "uint256[2]"
},
{
"internalType": "uint256[2]",
"name": "Y",
"type": "uint256[2]"
}
],
"internalType": "struct Pairing.G2Point",
"name": "delta2",
"type": "tuple"
},
{
"components": [
{
"internalType": "uint256",
"name": "X",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "Y",
"type": "uint256"
}
],
"internalType": "struct Pairing.G1Point[]",
"name": "IC",
"type": "tuple[]"
}
],
"internalType": "struct Verifier.VerifyingKey",
"name": "vk",
"type": "tuple"
}
],
"name": "verify",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]

View File

@ -110,26 +110,7 @@ impl AbiParser {
FieldType::Elementary(param) => tuple.push(param.clone()), FieldType::Elementary(param) => tuple.push(param.clone()),
FieldType::Struct(ty) => { FieldType::Struct(ty) => {
if let Some(param) = self.struct_tuples.get(ty.name()).cloned() { if let Some(param) = self.struct_tuples.get(ty.name()).cloned() {
tuple.push(ParamType::Tuple(param)) tuple.push(ty.as_param(ParamType::Tuple(param)))
} else {
resolved = false;
break;
}
}
FieldType::StructArray(ty) => {
if let Some(param) = self.struct_tuples.get(ty.name()).cloned() {
tuple.push(ParamType::Array(Box::new(ParamType::Tuple(param))))
} else {
resolved = false;
break;
}
}
FieldType::FixedStructArray(ty, size) => {
if let Some(param) = self.struct_tuples.get(ty.name()).cloned() {
tuple.push(ParamType::FixedArray(
Box::new(ParamType::Tuple(param)),
*size,
))
} else { } else {
resolved = false; resolved = false;
break; break;
@ -331,13 +312,10 @@ impl AbiParser {
.map(ParamType::Tuple) .map(ParamType::Tuple)
.ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?; .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?;
match field { if let Some(field) = field.as_struct() {
FieldType::Struct(_) => Ok(tuple), Ok(field.as_param(tuple))
FieldType::StructArray(_) => Ok(ParamType::Array(Box::new(tuple))), } else {
FieldType::FixedStructArray(_, size) => { bail!("Expected struct type")
Ok(ParamType::FixedArray(Box::new(tuple), size))
}
_ => bail!("Expected struct type"),
} }
} else { } else {
bail!("Failed determine event type `{}`", type_str) bail!("Failed determine event type `{}`", type_str)

View File

@ -6,11 +6,15 @@ use crate::abi::{param_type::Reader, ParamType};
/// A field declaration inside a struct /// A field declaration inside a struct
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct FieldDeclaration { pub struct FieldDeclaration {
name: String, pub name: String,
ty: FieldType, pub ty: FieldType,
} }
impl FieldDeclaration { impl FieldDeclaration {
pub fn new(name: String, ty: FieldType) -> Self {
Self { name, ty }
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
@ -24,13 +28,11 @@ impl FieldDeclaration {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum FieldType { pub enum FieldType {
/// Represents elementary types, see [`ParamType`] /// Represents elementary types, see [`ParamType`]
///
/// Note: tuples will be treated as rust tuples
Elementary(ParamType), Elementary(ParamType),
/// A non elementary type field, treated as user defined struct /// A non elementary type field, treated as user defined struct
Struct(StructFieldType), Struct(StructFieldType),
// Array of user defined type
StructArray(StructFieldType),
// Array with fixed size of user defined type
FixedStructArray(StructFieldType, usize),
/// Mapping /// Mapping
Mapping(Box<MappingType>), Mapping(Box<MappingType>),
} }
@ -42,9 +44,7 @@ impl FieldType {
pub(crate) fn as_struct(&self) -> Option<&StructFieldType> { pub(crate) fn as_struct(&self) -> Option<&StructFieldType> {
match self { match self {
FieldType::Struct(s) FieldType::Struct(s) => Some(s),
| FieldType::StructArray(s)
| FieldType::FixedStructArray(s, _) => Some(s),
_ => None, _ => None,
} }
} }
@ -72,8 +72,8 @@ pub struct StructFieldDeclaration {
/// How the type of a struct field is referenced /// How the type of a struct field is referenced
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct StructFieldType { pub struct StructType {
/// The name of the struct /// The name of the struct (or rather the name of the rust type)
name: String, name: String,
/// All previous projections up until the name /// All previous projections up until the name
/// ///
@ -81,10 +81,63 @@ pub struct StructFieldType {
projections: Vec<String>, projections: Vec<String>,
} }
impl StructFieldType { impl StructType {
pub fn new(name: String, projections: Vec<String>) -> Self {
Self { name, projections }
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
}
/// Represents the type of a field in a struct
#[derive(Debug, Clone, PartialEq)]
pub enum StructFieldType {
/// A non elementary type field, represents a user defined struct
Type(StructType),
// Array of user defined type
Array(Box<StructFieldType>),
// Array with fixed size of user defined type
FixedArray(Box<StructFieldType>, usize),
}
impl StructFieldType {
pub fn name(&self) -> &str {
match self {
StructFieldType::Type(ty) => &ty.name,
StructFieldType::Array(ty) => ty.name(),
StructFieldType::FixedArray(ty, _) => ty.name(),
}
}
pub fn projections(&self) -> &[String] {
match self {
StructFieldType::Type(ty) => &ty.projections,
StructFieldType::Array(ty) => ty.projections(),
StructFieldType::FixedArray(ty, _) => ty.projections(),
}
}
pub fn identifier(&self) -> String {
let name = self.name();
let path = self.projections().join(".");
if path.is_empty() {
name.to_string()
} else {
format!("{}.{}", path, name)
}
}
pub fn as_param(&self, tuple: ParamType) -> ParamType {
match self {
StructFieldType::Type(_) => tuple,
StructFieldType::Array(ty) => ty.as_param(ParamType::Array(Box::new(tuple))),
StructFieldType::FixedArray(ty, size) => {
ty.as_param(ParamType::FixedArray(Box::new(tuple), *size))
}
}
}
/// Parse a struct field declaration /// Parse a struct field declaration
/// ///
@ -97,10 +150,10 @@ impl StructFieldType {
let mut chars = input.chars(); let mut chars = input.chars();
match chars.next() { match chars.next() {
None => { None => {
return Ok(FieldType::Struct(StructFieldType { return Ok(FieldType::Struct(StructFieldType::Type(StructType {
name: ty, name: ty,
projections, projections,
})) })))
} }
Some(' ') | Some('\t') | Some('[') => { Some(' ') | Some('\t') | Some('[') => {
// array // array
@ -118,18 +171,23 @@ impl StructFieldType {
} }
} }
Some(']') => { Some(']') => {
let ty = StructFieldType { let ty = StructType {
name: ty, name: ty,
projections, projections,
}; };
return if size.is_empty() { return if size.is_empty() {
Ok(FieldType::StructArray(ty)) Ok(FieldType::Struct(StructFieldType::Array(Box::new(
StructFieldType::Type(ty),
))))
} else { } else {
let size = size.parse().map_err(|_| { let size = size.parse().map_err(|_| {
format_err!("Illegal array size `{}` at `{}`", size, input) format_err!("Illegal array size `{}` at `{}`", size, input)
})?; })?;
Ok(FieldType::FixedStructArray(ty, size)) Ok(FieldType::Struct(StructFieldType::FixedArray(
Box::new(StructFieldType::Type(ty)),
size,
)))
}; };
} }
Some(c) => { Some(c) => {
@ -157,8 +215,8 @@ impl StructFieldType {
/// Represents a solidity struct /// Represents a solidity struct
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct SolStruct { pub struct SolStruct {
name: String, pub name: String,
fields: Vec<FieldDeclaration>, pub fields: Vec<FieldDeclaration>,
} }
impl SolStruct { impl SolStruct {
@ -223,6 +281,19 @@ impl SolStruct {
pub fn fields(&self) -> &Vec<FieldDeclaration> { pub fn fields(&self) -> &Vec<FieldDeclaration> {
&self.fields &self.fields
} }
/// If the struct only consists of elementary fields, this will return `ParamType::Tuple` with all those fields
pub fn as_tuple(&self) -> Option<ParamType> {
let mut params = Vec::with_capacity(self.fields.len());
for field in self.fields() {
if let FieldType::Elementary(ref param) = field.ty {
params.push(param.clone())
} else {
return None;
}
}
Some(ParamType::Tuple(params))
}
} }
/// Strips the identifier of field declaration from the input and returns it /// Strips the identifier of field declaration from the input and returns it
@ -386,10 +457,10 @@ mod tests {
}, },
FieldDeclaration { FieldDeclaration {
name: "_other".to_string(), name: "_other".to_string(),
ty: FieldType::Struct(StructFieldType { ty: FieldType::Struct(StructFieldType::Type(StructType {
name: "Inner".to_string(), name: "Inner".to_string(),
projections: vec!["Some".to_string(), "Other".to_string()] projections: vec!["Some".to_string(), "Other".to_string()]
}), })),
}, },
], ],
} }

View File

@ -0,0 +1,49 @@
//! Main entry point for ContractMonitor
use ethers::{prelude::*, utils::Ganache};
use std::{convert::TryFrom, sync::Arc, time::Duration};
abigen!(
VerifierContract,
"ethers-contract/tests/solidity-contracts/verifier_abi.json"
);
/// This example only demonstrates how to use generated structs for solidity functions that
/// have structs as input.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let ganache = Ganache::new().spawn();
let provider =
Provider::<Http>::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64));
let wallet: LocalWallet = ganache.keys()[0].clone().into();
let client = SignerMiddleware::new(provider, wallet);
let client = Arc::new(client);
let contract = VerifierContract::new(Address::zero(), client);
// NOTE: this is all just dummy data
let g1 = G1Point {
x: U256::zero(),
y: U256::zero(),
};
let g2 = G2Point {
x: [U256::zero(), U256::zero()],
y: [U256::zero(), U256::zero()],
};
let vk = VerifyingKey {
alfa_1: g1.clone(),
beta_2: g2.clone(),
gamma_2: g2.clone(),
delta_2: g2.clone(),
ic: vec![g1.clone()],
};
let proof = Proof {
a: g1.clone(),
b: g2,
c: g1,
};
let _ = contract.verify(vec![], proof, vk);
Ok(())
}