From ba5f650dec178de200ae1bca081120f2f05e1287 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 16 Aug 2021 09:29:44 +0200 Subject: [PATCH] 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 --- Cargo.lock | 1 + .../ethers-contract-abigen/Cargo.toml | 1 + .../ethers-contract-abigen/src/contract.rs | 14 + .../src/contract/methods.rs | 119 ++++- .../src/contract/structs.rs | 496 ++++++++++++++++-- .../ethers-contract-abigen/src/lib.rs | 1 + .../ethers-contract-abigen/src/rawabi.rs | 48 ++ ethers-contract/tests/abigen.rs | 49 +- .../solidity-contracts/verifier_abi.json | 176 +++++++ ethers-core/src/abi/human_readable.rs | 32 +- ethers-core/src/abi/struct_def.rs | 113 +++- .../examples/contract_with_abi_and_structs.rs | 49 ++ 12 files changed, 963 insertions(+), 136 deletions(-) create mode 100644 ethers-contract/ethers-contract-abigen/src/rawabi.rs create mode 100644 ethers-contract/tests/solidity-contracts/verifier_abi.json create mode 100644 ethers/examples/contract_with_abi_and_structs.rs diff --git a/Cargo.lock b/Cargo.lock index 658cfbcc..f45666bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,7 @@ dependencies = [ "proc-macro2", "quote", "reqwest", + "serde", "serde_json", "syn", "url", diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 9da44463..27e2b86b 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -19,6 +19,7 @@ quote = "1.0" syn = "1.0.12" url = "2.1" serde_json = "1.0.61" +serde = { version = "1.0.124", features = ["derive"] } hex = { version = "0.4.2", default-features = false, features = ["std"] } reqwest = { version = "0.11.3", features = ["blocking"] } once_cell = { version = "1.8.0", default-features = false } diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index bf0be544..7ea18626 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -7,6 +7,8 @@ mod types; use super::util; use super::Abigen; +use crate::contract::structs::InternalStructs; +use crate::rawabi::RawAbi; use anyhow::{anyhow, Context as _, Result}; use ethers_core::abi::AbiParser; use ethers_core::{ @@ -30,6 +32,9 @@ pub(crate) struct Context { /// The parser used for human readable format abi_parser: AbiParser, + /// Contains all the solidity structs extracted from the JSON ABI. + internal_structs: InternalStructs, + /// Was the ABI in human readable format? human_readable: bool, @@ -118,6 +123,14 @@ impl Context { (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::(&abi_str).ok()) + .flatten() + .map(InternalStructs::new) + .unwrap_or_default(); + let contract_name = util::ident(&args.contract_name); // NOTE: We only check for duplicate signatures here, since if there are @@ -146,6 +159,7 @@ impl Context { human_readable, abi_str: Literal::string(&abi_str), abi_parser, + internal_structs, contract_name, method_aliases, event_derives, diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index dbb8e3a2..f328b71d 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -22,40 +22,107 @@ impl Context { .flatten() .map(|function| { 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)) }) .collect::>>()?; Ok(quote! { #( #functions )* }) } -} -#[allow(unused)] -fn expand_function(function: &Function, alias: Option) -> Result { - let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); - let selector = expand_selector(function.selector()); - - let input = expand_inputs(&function.inputs)?; - - let outputs = expand_fn_outputs(&function.outputs)?; - - let result = quote! { ethers_contract::builders::ContractCall }; - - let arg = expand_inputs_call_arg(&function.inputs); - 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)") + fn expand_inputs_call_arg_with_structs( + &self, + fun: &Function, + ) -> Result<(TokenStream, TokenStream)> { + let mut args = Vec::with_capacity(fun.inputs.len()); + let mut call_args = Vec::with_capacity(fun.inputs.len()); + for (i, param) in fun.inputs.iter().enumerate() { + let name = util::expand_input_name(i, ¶m.name); + let ty = self.expand_input_param(fun, ¶m.name, ¶m.kind)?; + args.push(quote! { #name: #ty }); + let call_arg = match param.kind { + // this is awkward edge case where the function inputs are a single struct + // we need to force this argument into a tuple so it gets expanded to `((#name,))` + // this is currently necessary because internally `flatten_tokens` is called which removes the outermost `tuple` level + // and since `((#name))` is not a rust tuple it doesn't get wrapped into another tuple that will be peeled off by `flatten_tokens` + ParamType::Tuple(_) if fun.inputs.len() == 1 => { + // make sure the tuple gets converted to `Token::Tuple` + quote! {(#name,)} + } + _ => name, + }; + call_args.push(call_arg); } - }) + 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 { + 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) -> Result { + 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 }; + + 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 diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs index cacb8e16..cc6bb164 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -1,13 +1,21 @@ //! Methods for expanding structs +use std::collections::{HashMap, VecDeque}; + use anyhow::{Context as _, Result}; use inflector::Inflector; use proc_macro2::{Literal, TokenStream}; 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::rawabi::{Component, RawAbi}; use crate::util; +use std::any::Any; impl Context { /// 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 /// failed already pub fn abi_structs(&self) -> Result { - let mut structs = Vec::with_capacity(self.abi_parser.structs.len()); - for (name, sol_struct) in &self.abi_parser.structs { + if self.human_readable { + 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 { + 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 { + 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::>() + .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 { + 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 param_types = Vec::with_capacity(sol_struct.fields().len()); for field in sol_struct.fields() { @@ -29,44 +131,19 @@ impl Context { fields.push(quote! { pub #field_name: #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 }); + let name = struct_ty.name(); let tuple = self .abi_parser .struct_tuples - .get(struct_ty.name()) - .context(format!("No types found for {}", struct_ty.name()))? + .get(name) + .context(format!("No types found for {}", name))? .clone(); - param_types.push(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 = ParamType::Tuple(tuple); - 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::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, - )); + param_types.push(struct_ty.as_param(tuple)); } FieldType::Mapping(_) => { return Err(anyhow::anyhow!( @@ -96,14 +173,349 @@ impl Context { let derives = &self.event_derives; let derives = quote! {#(#derives),*}; - structs.push(quote! { - #abi_signature_doc - #[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)] - pub struct #name { - #( #fields ),* - } - }); + structs.extend(quote! { + #abi_signature_doc + #[derive(Clone, Debug, Default, Eq, PartialEq, ethers::contract::EthAbiType, #derives)] + pub struct #name { + #( #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, + + /// (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 all structs the function returns + outputs: HashMap>, + + /// All the structs extracted from the abi with their identifier as key + structs: HashMap, + + /// solidity structs as tuples + struct_tuples: HashMap, + + /// Contains the names for the rust types (id -> rust type name) + rust_type_names: HashMap, +} + +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)> = + 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)>, + mut name: String, + mut projections: Vec, + 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) -> HashMap { + 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, 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::>>() + { + 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, + field_component: &Component, + kind: ParamType, +) -> Option { + 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 { + 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::(VERIFIER_ABI).unwrap(); + + let internal = InternalStructs::new(abi); + dbg!(internal.rust_type_names); } } diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index 716ac85b..3d3a159e 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -15,6 +15,7 @@ mod test_macros; mod contract; use contract::Context; +pub mod rawabi; mod rustfmt; mod source; mod util; diff --git a/ethers-contract/ethers-contract-abigen/src/rawabi.rs b/ethers-contract/ethers-contract-abigen/src/rawabi.rs new file mode 100644 index 00000000..fc04f49d --- /dev/null +++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs @@ -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; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Item { + #[serde(default)] + pub inputs: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub state_mutability: Option, + #[serde(rename = "type")] + pub type_field: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default)] + pub outputs: Vec, +} + +/// Either +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Component { + #[serde( + rename = "internalType", + default, + skip_serializing_if = "Option::is_none" + )] + pub internal_type: Option, + pub name: String, + #[serde(rename = "type")] + pub type_field: String, + #[serde(default)] + pub components: Vec, +} + +#[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::(VERIFIER_ABI).unwrap(); + } +} diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index 5e601724..fdf6cd8a 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -43,24 +43,33 @@ fn can_gen_structs_readable() { ); } -// NOTE(mattsse): There is currently a limitation with the `ethabi` crate's `Reader` -// that doesn't support arrays of tuples; https://github.com/gakonst/ethabi/pull/1 should fix this -// See also https://github.com/rust-ethereum/ethabi/issues/178 and -// https://github.com/rust-ethereum/ethabi/pull/186 +#[test] +fn can_gen_structs_with_arrays_readable() { + abigen!( + 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 can_gen_structs_with_arrays_readable() { -// abigen!( -// 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() -// ); -// } +fn assert_tokenizeable() {} + +#[test] +fn can_generate_internal_structs() { + abigen!( + VerifierContract, + "ethers-contract/tests/solidity-contracts/verifier_abi.json", + event_derives(serde::Deserialize, serde::Serialize) + ); + assert_tokenizeable::(); + assert_tokenizeable::(); + assert_tokenizeable::(); +} diff --git a/ethers-contract/tests/solidity-contracts/verifier_abi.json b/ethers-contract/tests/solidity-contracts/verifier_abi.json new file mode 100644 index 00000000..8721f0bd --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/verifier_abi.json @@ -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" + } +] diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index a88afb97..8b0de3ee 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -110,26 +110,7 @@ impl AbiParser { FieldType::Elementary(param) => tuple.push(param.clone()), FieldType::Struct(ty) => { if let Some(param) = self.struct_tuples.get(ty.name()).cloned() { - tuple.push(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, - )) + tuple.push(ty.as_param(ParamType::Tuple(param))) } else { resolved = false; break; @@ -331,13 +312,10 @@ impl AbiParser { .map(ParamType::Tuple) .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?; - match field { - FieldType::Struct(_) => Ok(tuple), - FieldType::StructArray(_) => Ok(ParamType::Array(Box::new(tuple))), - FieldType::FixedStructArray(_, size) => { - Ok(ParamType::FixedArray(Box::new(tuple), size)) - } - _ => bail!("Expected struct type"), + if let Some(field) = field.as_struct() { + Ok(field.as_param(tuple)) + } else { + bail!("Expected struct type") } } else { bail!("Failed determine event type `{}`", type_str) diff --git a/ethers-core/src/abi/struct_def.rs b/ethers-core/src/abi/struct_def.rs index 74b836f4..7c2ec6b4 100644 --- a/ethers-core/src/abi/struct_def.rs +++ b/ethers-core/src/abi/struct_def.rs @@ -6,11 +6,15 @@ use crate::abi::{param_type::Reader, ParamType}; /// A field declaration inside a struct #[derive(Debug, Clone, PartialEq)] pub struct FieldDeclaration { - name: String, - ty: FieldType, + pub name: String, + pub ty: FieldType, } impl FieldDeclaration { + pub fn new(name: String, ty: FieldType) -> Self { + Self { name, ty } + } + pub fn name(&self) -> &str { &self.name } @@ -24,13 +28,11 @@ impl FieldDeclaration { #[derive(Debug, Clone, PartialEq)] pub enum FieldType { /// Represents elementary types, see [`ParamType`] + /// + /// Note: tuples will be treated as rust tuples Elementary(ParamType), /// A non elementary type field, treated as user defined struct Struct(StructFieldType), - // Array of user defined type - StructArray(StructFieldType), - // Array with fixed size of user defined type - FixedStructArray(StructFieldType, usize), /// Mapping Mapping(Box), } @@ -42,9 +44,7 @@ impl FieldType { pub(crate) fn as_struct(&self) -> Option<&StructFieldType> { match self { - FieldType::Struct(s) - | FieldType::StructArray(s) - | FieldType::FixedStructArray(s, _) => Some(s), + FieldType::Struct(s) => Some(s), _ => None, } } @@ -72,8 +72,8 @@ pub struct StructFieldDeclaration { /// How the type of a struct field is referenced #[derive(Debug, Clone, PartialEq)] -pub struct StructFieldType { - /// The name of the struct +pub struct StructType { + /// The name of the struct (or rather the name of the rust type) name: String, /// All previous projections up until the name /// @@ -81,10 +81,63 @@ pub struct StructFieldType { projections: Vec, } -impl StructFieldType { +impl StructType { + pub fn new(name: String, projections: Vec) -> Self { + Self { name, projections } + } + pub fn name(&self) -> &str { &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), + // Array with fixed size of user defined type + FixedArray(Box, 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 /// @@ -97,10 +150,10 @@ impl StructFieldType { let mut chars = input.chars(); match chars.next() { None => { - return Ok(FieldType::Struct(StructFieldType { + return Ok(FieldType::Struct(StructFieldType::Type(StructType { name: ty, projections, - })) + }))) } Some(' ') | Some('\t') | Some('[') => { // array @@ -118,18 +171,23 @@ impl StructFieldType { } } Some(']') => { - let ty = StructFieldType { + let ty = StructType { name: ty, projections, }; return if size.is_empty() { - Ok(FieldType::StructArray(ty)) + Ok(FieldType::Struct(StructFieldType::Array(Box::new( + StructFieldType::Type(ty), + )))) } else { let size = size.parse().map_err(|_| { 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) => { @@ -157,8 +215,8 @@ impl StructFieldType { /// Represents a solidity struct #[derive(Debug, Clone, PartialEq)] pub struct SolStruct { - name: String, - fields: Vec, + pub name: String, + pub fields: Vec, } impl SolStruct { @@ -223,6 +281,19 @@ impl SolStruct { pub fn fields(&self) -> &Vec { &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 { + 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 @@ -386,10 +457,10 @@ mod tests { }, FieldDeclaration { name: "_other".to_string(), - ty: FieldType::Struct(StructFieldType { + ty: FieldType::Struct(StructFieldType::Type(StructType { name: "Inner".to_string(), projections: vec!["Some".to_string(), "Other".to_string()] - }), + })), }, ], } diff --git a/ethers/examples/contract_with_abi_and_structs.rs b/ethers/examples/contract_with_abi_and_structs.rs new file mode 100644 index 00000000..a771fb4f --- /dev/null +++ b/ethers/examples/contract_with_abi_and_structs.rs @@ -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> { + let ganache = Ganache::new().spawn(); + let provider = + Provider::::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(()) +}