feat(abigen): use structs for outputs (#664)
* feat(abigen): use structs for outputs * update CHANGELOG * make clippy happy * fix lints
This commit is contained in:
parent
f0a8f01465
commit
59cf991828
|
@ -94,6 +94,8 @@
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
|
- Substitute output tuples with rust struct types for function calls
|
||||||
|
[#664](https://github.com/gakonst/ethers-rs/pull/664)
|
||||||
- Add AbiType implementation during EthAbiType expansion
|
- Add AbiType implementation during EthAbiType expansion
|
||||||
[#647](https://github.com/gakonst/ethers-rs/pull/647)
|
[#647](https://github.com/gakonst/ethers-rs/pull/647)
|
||||||
- fix Etherscan conditional HTTP support
|
- fix Etherscan conditional HTTP support
|
||||||
|
|
|
@ -38,8 +38,7 @@ impl Context {
|
||||||
let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect();
|
let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect();
|
||||||
let filter_methods = sorted_events
|
let filter_methods = sorted_events
|
||||||
.values()
|
.values()
|
||||||
.map(std::ops::Deref::deref)
|
.flat_map(std::ops::Deref::deref)
|
||||||
.flatten()
|
|
||||||
.map(|event| self.expand_filter(event))
|
.map(|event| self.expand_filter(event))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,7 @@ impl Context {
|
||||||
let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect();
|
let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect();
|
||||||
let functions = sorted_functions
|
let functions = sorted_functions
|
||||||
.values()
|
.values()
|
||||||
.map(std::ops::Deref::deref)
|
.flat_map(std::ops::Deref::deref)
|
||||||
.flatten()
|
|
||||||
.map(|function| {
|
.map(|function| {
|
||||||
let signature = function.abi_signature();
|
let signature = function.abi_signature();
|
||||||
self.expand_function(function, aliases.get(&signature).cloned())
|
self.expand_function(function, aliases.get(&signature).cloned())
|
||||||
|
@ -50,7 +49,7 @@ impl Context {
|
||||||
alias: Option<&MethodAlias>,
|
alias: Option<&MethodAlias>,
|
||||||
) -> Result<TokenStream> {
|
) -> Result<TokenStream> {
|
||||||
let call_name = expand_call_struct_name(function, alias);
|
let call_name = expand_call_struct_name(function, alias);
|
||||||
let fields = self.expand_input_pairs(function)?;
|
let fields = self.expand_input_params(function)?;
|
||||||
// expand as a tuple if all fields are anonymous
|
// expand as a tuple if all fields are anonymous
|
||||||
let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty());
|
let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty());
|
||||||
let call_type_definition = if all_anonymous_fields {
|
let call_type_definition = if all_anonymous_fields {
|
||||||
|
@ -163,16 +162,34 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expands to the `name : type` pairs of the function's inputs
|
/// Expands to the `name : type` pairs of the function's inputs
|
||||||
fn expand_input_pairs(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
|
fn expand_input_params(&self, fun: &Function) -> Result<Vec<(TokenStream, TokenStream)>> {
|
||||||
let mut args = Vec::with_capacity(fun.inputs.len());
|
let mut args = Vec::with_capacity(fun.inputs.len());
|
||||||
for (idx, param) in fun.inputs.iter().enumerate() {
|
for (idx, param) in fun.inputs.iter().enumerate() {
|
||||||
let name = util::expand_input_name(idx, ¶m.name);
|
let name = util::expand_input_name(idx, ¶m.name);
|
||||||
let ty = self.expand_input_param(fun, ¶m.name, ¶m.kind)?;
|
let ty = self.expand_input_param_type(fun, ¶m.name, ¶m.kind)?;
|
||||||
args.push((name, ty));
|
args.push((name, ty));
|
||||||
}
|
}
|
||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Expands to the return type of a function
|
||||||
|
fn expand_outputs(&self, fun: &Function) -> Result<TokenStream> {
|
||||||
|
let mut outputs = Vec::with_capacity(fun.outputs.len());
|
||||||
|
for param in fun.outputs.iter() {
|
||||||
|
let ty = self.expand_output_param_type(fun, param, ¶m.kind)?;
|
||||||
|
outputs.push(ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
let return_ty = match outputs.len() {
|
||||||
|
0 => quote! { () },
|
||||||
|
1 => outputs[0].clone(),
|
||||||
|
_ => {
|
||||||
|
quote! { (#( #outputs ),*) }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(return_ty)
|
||||||
|
}
|
||||||
|
|
||||||
/// Expands the arguments for the call that eventually calls the contract
|
/// Expands the arguments for the call that eventually calls the contract
|
||||||
fn expand_contract_call_args(&self, fun: &Function) -> Result<TokenStream> {
|
fn expand_contract_call_args(&self, fun: &Function) -> Result<TokenStream> {
|
||||||
let mut call_args = Vec::with_capacity(fun.inputs.len());
|
let mut call_args = Vec::with_capacity(fun.inputs.len());
|
||||||
|
@ -202,7 +219,8 @@ impl Context {
|
||||||
Ok(call_args)
|
Ok(call_args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_input_param(
|
/// returns the Tokenstream for the corresponding rust type of the param
|
||||||
|
fn expand_input_param_type(
|
||||||
&self,
|
&self,
|
||||||
fun: &Function,
|
fun: &Function,
|
||||||
param: &str,
|
param: &str,
|
||||||
|
@ -210,13 +228,13 @@ impl Context {
|
||||||
) -> Result<TokenStream> {
|
) -> Result<TokenStream> {
|
||||||
match kind {
|
match kind {
|
||||||
ParamType::Array(ty) => {
|
ParamType::Array(ty) => {
|
||||||
let ty = self.expand_input_param(fun, param, ty)?;
|
let ty = self.expand_input_param_type(fun, param, ty)?;
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
::std::vec::Vec<#ty>
|
::std::vec::Vec<#ty>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ParamType::FixedArray(ty, size) => {
|
ParamType::FixedArray(ty, size) => {
|
||||||
let ty = self.expand_input_param(fun, param, ty)?;
|
let ty = self.expand_input_param_type(fun, param, ty)?;
|
||||||
let size = *size;
|
let size = *size;
|
||||||
Ok(quote! {[#ty; #size]})
|
Ok(quote! {[#ty; #size]})
|
||||||
}
|
}
|
||||||
|
@ -235,27 +253,61 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns the Tokenstream for the corresponding rust type of the output param
|
||||||
|
fn expand_output_param_type(
|
||||||
|
&self,
|
||||||
|
fun: &Function,
|
||||||
|
param: &Param,
|
||||||
|
kind: &ParamType,
|
||||||
|
) -> Result<TokenStream> {
|
||||||
|
match kind {
|
||||||
|
ParamType::Array(ty) => {
|
||||||
|
let ty = self.expand_output_param_type(fun, param, ty)?;
|
||||||
|
Ok(quote! {
|
||||||
|
::std::vec::Vec<#ty>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ParamType::FixedArray(ty, size) => {
|
||||||
|
let ty = self.expand_output_param_type(fun, param, ty)?;
|
||||||
|
let size = *size;
|
||||||
|
Ok(quote! {[#ty; #size]})
|
||||||
|
}
|
||||||
|
ParamType::Tuple(_) => {
|
||||||
|
let ty = if let Some(rust_struct_name) =
|
||||||
|
param.internal_type.as_ref().and_then(|s| {
|
||||||
|
self.internal_structs.get_function_output_struct_type(&fun.name, s)
|
||||||
|
}) {
|
||||||
|
let ident = util::ident(rust_struct_name);
|
||||||
|
quote! {#ident}
|
||||||
|
} else {
|
||||||
|
types::expand(kind)?
|
||||||
|
};
|
||||||
|
Ok(ty)
|
||||||
|
}
|
||||||
|
_ => types::expand(kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Expands a single function with the given alias
|
/// Expands a single function with the given alias
|
||||||
fn expand_function(
|
fn expand_function(
|
||||||
&self,
|
&self,
|
||||||
function: &Function,
|
function: &Function,
|
||||||
alias: Option<MethodAlias>,
|
alias: Option<MethodAlias>,
|
||||||
) -> Result<TokenStream> {
|
) -> Result<TokenStream> {
|
||||||
|
let ethers_contract = ethers_contract_crate();
|
||||||
|
|
||||||
let name = expand_function_name(function, alias.as_ref());
|
let name = expand_function_name(function, alias.as_ref());
|
||||||
let selector = expand_selector(function.selector());
|
let selector = expand_selector(function.selector());
|
||||||
|
|
||||||
// TODO use structs
|
|
||||||
let outputs = expand_fn_outputs(&function.outputs)?;
|
|
||||||
|
|
||||||
let ethers_contract = ethers_contract_crate();
|
|
||||||
|
|
||||||
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
|
|
||||||
|
|
||||||
let contract_args = self.expand_contract_call_args(function)?;
|
let contract_args = self.expand_contract_call_args(function)?;
|
||||||
let function_params =
|
let function_params =
|
||||||
self.expand_input_pairs(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
|
self.expand_input_params(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty });
|
||||||
let function_params = quote! { #( , #function_params )* };
|
let function_params = quote! { #( , #function_params )* };
|
||||||
|
|
||||||
|
let outputs = self.expand_outputs(function)?;
|
||||||
|
|
||||||
|
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
|
||||||
|
|
||||||
let doc = util::expand_doc(&format!(
|
let doc = util::expand_doc(&format!(
|
||||||
"Calls the contract's `{}` (0x{}) function",
|
"Calls the contract's `{}` (0x{}) function",
|
||||||
function.name,
|
function.name,
|
||||||
|
@ -478,20 +530,6 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
|
|
||||||
match outputs.len() {
|
|
||||||
0 => Ok(quote! { () }),
|
|
||||||
1 => types::expand(&outputs[0].kind),
|
|
||||||
_ => {
|
|
||||||
let types = outputs
|
|
||||||
.iter()
|
|
||||||
.map(|param| types::expand(¶m.kind))
|
|
||||||
.collect::<Result<Vec<_>>>()?;
|
|
||||||
Ok(quote! { (#( #types ),*) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expand_selector(selector: Selector) -> TokenStream {
|
fn expand_selector(selector: Selector) -> TokenStream {
|
||||||
let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
|
let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
|
||||||
quote! { [#( #bytes ),*] }
|
quote! { [#( #bytes ),*] }
|
||||||
|
@ -575,6 +613,20 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
|
||||||
|
match outputs.len() {
|
||||||
|
0 => Ok(quote! { () }),
|
||||||
|
1 => types::expand(&outputs[0].kind),
|
||||||
|
_ => {
|
||||||
|
let types = outputs
|
||||||
|
.iter()
|
||||||
|
.map(|param| types::expand(¶m.kind))
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
Ok(quote! { (#( #types ),*) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// packs the argument in a tuple to be used for the contract call
|
// packs the argument in a tuple to be used for the contract call
|
||||||
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
|
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
|
||||||
let names = inputs
|
let names = inputs
|
||||||
|
|
|
@ -312,6 +312,22 @@ impl InternalStructs {
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the name of the rust type that will be generated if the given output is a struct
|
||||||
|
/// NOTE: this does not account for arrays or fixed arrays
|
||||||
|
pub fn get_function_output_struct_type(
|
||||||
|
&self,
|
||||||
|
function: &str,
|
||||||
|
internal_type: &str,
|
||||||
|
) -> Option<&str> {
|
||||||
|
self.outputs
|
||||||
|
.get(function)
|
||||||
|
.and_then(|outputs| {
|
||||||
|
outputs.iter().find(|s| s.as_str() == struct_type_identifier(internal_type))
|
||||||
|
})
|
||||||
|
.and_then(|id| self.rust_type_names.get(id))
|
||||||
|
.map(String::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the mapping table of abi `internal type identifier -> rust type`
|
/// Returns the mapping table of abi `internal type identifier -> rust type`
|
||||||
pub fn rust_type_names(&self) -> &HashMap<String, String> {
|
pub fn rust_type_names(&self) -> &HashMap<String, String> {
|
||||||
&self.rust_type_names
|
&self.rust_type_names
|
||||||
|
|
|
@ -191,7 +191,7 @@ impl<M: Middleware> Multicall<M> {
|
||||||
/// If more than the maximum number of supported calls are added. The maximum
|
/// If more than the maximum number of supported calls are added. The maximum
|
||||||
/// limits is constrained due to tokenization/detokenization support for tuples
|
/// limits is constrained due to tokenization/detokenization support for tuples
|
||||||
pub fn add_call<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &mut Self {
|
pub fn add_call<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &mut Self {
|
||||||
assert!(!(self.calls.len() >= 16), "Cannot support more than {} calls", 16);
|
assert!(self.calls.len() < 16, "Cannot support more than {} calls", 16);
|
||||||
|
|
||||||
match (call.tx.to(), call.tx.data()) {
|
match (call.tx.to(), call.tx.data()) {
|
||||||
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
||||||
|
|
|
@ -419,3 +419,32 @@ fn can_handle_case_sensitive_calls() {
|
||||||
let _ = contract.index();
|
let _ = contract.index();
|
||||||
let _ = contract.INDEX();
|
let _ = contract.INDEX();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn can_abiencoderv2_output() {
|
||||||
|
abigen!(AbiEncoderv2Test, "ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json",);
|
||||||
|
let ganache = ethers_core::utils::Ganache::new().spawn();
|
||||||
|
let from = ganache.addresses()[0];
|
||||||
|
let provider = Provider::try_from(ganache.endpoint())
|
||||||
|
.unwrap()
|
||||||
|
.with_sender(from)
|
||||||
|
.interval(std::time::Duration::from_millis(10));
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
|
let contract = "AbiencoderV2Test";
|
||||||
|
let path = "./tests/solidity-contracts/Abiencoderv2Test.sol";
|
||||||
|
let compiled = Solc::default().compile_source(path).unwrap();
|
||||||
|
let compiled = compiled.get(path, contract).unwrap();
|
||||||
|
let factory = ethers_contract::ContractFactory::new(
|
||||||
|
compiled.abi.unwrap().clone(),
|
||||||
|
compiled.bytecode().unwrap().clone(),
|
||||||
|
client.clone(),
|
||||||
|
);
|
||||||
|
let addr = factory.deploy(()).unwrap().legacy().send().await.unwrap().address();
|
||||||
|
|
||||||
|
let contract = AbiEncoderv2Test::new(addr, client.clone());
|
||||||
|
let person = Person { name: "Alice".to_string(), age: 20u64.into() };
|
||||||
|
|
||||||
|
let res = contract.default_person().call().await.unwrap();
|
||||||
|
assert_eq!(res, person);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
pragma solidity >=0.6.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
contract AbiencoderV2Test {
|
||||||
|
struct Person {
|
||||||
|
string name;
|
||||||
|
uint age;
|
||||||
|
}
|
||||||
|
function defaultPerson() public pure returns (Person memory) {
|
||||||
|
return Person("Alice", 20);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
[{"inputs":[],"name":"defaultPerson","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"age","type":"uint256"}],"internalType":"struct Hello.Person","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"}]
|
|
@ -123,7 +123,7 @@ mod tests {
|
||||||
assert!(oracle.fast_gas_price > 0);
|
assert!(oracle.fast_gas_price > 0);
|
||||||
assert!(oracle.last_block > 0);
|
assert!(oracle.last_block > 0);
|
||||||
assert!(oracle.suggested_base_fee > 0.0);
|
assert!(oracle.suggested_base_fee > 0.0);
|
||||||
assert!(oracle.gas_used_ratio.len() > 0);
|
assert!(!oracle.gas_used_ratio.is_empty());
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,7 +206,7 @@ impl LedgerEthereum {
|
||||||
let mut bytes = vec![depth as u8];
|
let mut bytes = vec![depth as u8];
|
||||||
for derivation_index in elements {
|
for derivation_index in elements {
|
||||||
let hardened = derivation_index.contains('\'');
|
let hardened = derivation_index.contains('\'');
|
||||||
let mut index = derivation_index.replace("'", "").parse::<u32>().unwrap();
|
let mut index = derivation_index.replace('\'', "").parse::<u32>().unwrap();
|
||||||
if hardened {
|
if hardened {
|
||||||
index |= 0x80000000;
|
index |= 0x80000000;
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,7 +202,7 @@ impl Solc {
|
||||||
pub fn version_req(source: &Source) -> Result<VersionReq> {
|
pub fn version_req(source: &Source) -> Result<VersionReq> {
|
||||||
let version = utils::find_version_pragma(&source.content)
|
let version = utils::find_version_pragma(&source.content)
|
||||||
.ok_or(SolcError::PragmaNotFound)?
|
.ok_or(SolcError::PragmaNotFound)?
|
||||||
.replace(" ", ",");
|
.replace(' ', ",");
|
||||||
|
|
||||||
// Somehow, Solidity semver without an operator is considered to be "exact",
|
// Somehow, Solidity semver without an operator is considered to be "exact",
|
||||||
// but lack of operator automatically marks the operator as Caret, so we need
|
// but lack of operator automatically marks the operator as Caret, so we need
|
||||||
|
|
Loading…
Reference in New Issue