Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat: generate rust structs from solidity JSON ABI #378

Merged
merged 8 commits into from
Aug 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ethers-contract/ethers-contract-abigen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
14 changes: 14 additions & 0 deletions ethers-contract/ethers-contract-abigen/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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,

Expand Down Expand Up @@ -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::<RawAbi>(&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
Expand Down Expand Up @@ -146,6 +159,7 @@ impl Context {
human_readable,
abi_str: Literal::string(&abi_str),
abi_parser,
internal_structs,
contract_name,
method_aliases,
event_derives,
Expand Down
111 changes: 89 additions & 22 deletions ethers-contract/ethers-contract-abigen/src/contract/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Result<Vec<_>>>()?;

Ok(quote! { #( #functions )* })
}
}

#[allow(unused)]
fn expand_function(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());
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, &param.name);
let ty = self.expand_input_param(fun, &param.name, &param.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<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),
}
}

let input = expand_inputs(&function.inputs)?;
#[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());

let outputs = expand_fn_outputs(&function.outputs)?;
// TODO use structs
let outputs = expand_fn_outputs(&function.outputs)?;

let result = quote! { ethers_contract::builders::ContractCall<M, #outputs> };
let result = quote! { ethers_contract::builders::ContractCall<M, #outputs> };

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! {
let (input, arg) = self.expand_inputs_call_arg_with_structs(function)?;

#doc
pub fn #name(&self #input) -> #result {
self.0.method_hash(#selector, #arg)
.expect("method not found (this should never happen)")
}
})
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
Expand Down
Loading