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
|
||||
|
||||
- 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
|
||||
[#647](https://github.com/gakonst/ethers-rs/pull/647)
|
||||
- fix Etherscan conditional HTTP support
|
||||
|
|
|
@ -38,8 +38,7 @@ impl Context {
|
|||
let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect();
|
||||
let filter_methods = sorted_events
|
||||
.values()
|
||||
.map(std::ops::Deref::deref)
|
||||
.flatten()
|
||||
.flat_map(std::ops::Deref::deref)
|
||||
.map(|event| self.expand_filter(event))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
|
|
@ -28,8 +28,7 @@ impl Context {
|
|||
let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect();
|
||||
let functions = sorted_functions
|
||||
.values()
|
||||
.map(std::ops::Deref::deref)
|
||||
.flatten()
|
||||
.flat_map(std::ops::Deref::deref)
|
||||
.map(|function| {
|
||||
let signature = function.abi_signature();
|
||||
self.expand_function(function, aliases.get(&signature).cloned())
|
||||
|
@ -50,7 +49,7 @@ impl Context {
|
|||
alias: Option<&MethodAlias>,
|
||||
) -> Result<TokenStream> {
|
||||
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
|
||||
let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty());
|
||||
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
|
||||
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());
|
||||
for (idx, param) in fun.inputs.iter().enumerate() {
|
||||
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));
|
||||
}
|
||||
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
|
||||
fn expand_contract_call_args(&self, fun: &Function) -> Result<TokenStream> {
|
||||
let mut call_args = Vec::with_capacity(fun.inputs.len());
|
||||
|
@ -202,7 +219,8 @@ impl Context {
|
|||
Ok(call_args)
|
||||
}
|
||||
|
||||
fn expand_input_param(
|
||||
/// returns the Tokenstream for the corresponding rust type of the param
|
||||
fn expand_input_param_type(
|
||||
&self,
|
||||
fun: &Function,
|
||||
param: &str,
|
||||
|
@ -210,13 +228,13 @@ impl Context {
|
|||
) -> Result<TokenStream> {
|
||||
match kind {
|
||||
ParamType::Array(ty) => {
|
||||
let ty = self.expand_input_param(fun, param, ty)?;
|
||||
let ty = self.expand_input_param_type(fun, param, ty)?;
|
||||
Ok(quote! {
|
||||
::std::vec::Vec<#ty>
|
||||
})
|
||||
}
|
||||
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;
|
||||
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
|
||||
fn expand_function(
|
||||
&self,
|
||||
function: &Function,
|
||||
alias: Option<MethodAlias>,
|
||||
) -> Result<TokenStream> {
|
||||
let ethers_contract = ethers_contract_crate();
|
||||
|
||||
let name = expand_function_name(function, alias.as_ref());
|
||||
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 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 outputs = self.expand_outputs(function)?;
|
||||
|
||||
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };
|
||||
|
||||
let doc = util::expand_doc(&format!(
|
||||
"Calls the contract's `{}` (0x{}) function",
|
||||
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 {
|
||||
let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
|
||||
quote! { [#( #bytes ),*] }
|
||||
|
@ -575,6 +613,20 @@ mod tests {
|
|||
|
||||
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
|
||||
fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
|
||||
let names = inputs
|
||||
|
|
|
@ -312,6 +312,22 @@ impl InternalStructs {
|
|||
.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`
|
||||
pub fn rust_type_names(&self) -> &HashMap<String, String> {
|
||||
&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
|
||||
/// limits is constrained due to tokenization/detokenization support for tuples
|
||||
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()) {
|
||||
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
||||
|
|
|
@ -419,3 +419,32 @@ fn can_handle_case_sensitive_calls() {
|
|||
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.last_block > 0);
|
||||
assert!(oracle.suggested_base_fee > 0.0);
|
||||
assert!(oracle.gas_used_ratio.len() > 0);
|
||||
assert!(!oracle.gas_used_ratio.is_empty());
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -206,7 +206,7 @@ impl LedgerEthereum {
|
|||
let mut bytes = vec![depth as u8];
|
||||
for derivation_index in elements {
|
||||
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 {
|
||||
index |= 0x80000000;
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@ impl Solc {
|
|||
pub fn version_req(source: &Source) -> Result<VersionReq> {
|
||||
let version = utils::find_version_pragma(&source.content)
|
||||
.ok_or(SolcError::PragmaNotFound)?
|
||||
.replace(" ", ",");
|
||||
.replace(' ', ",");
|
||||
|
||||
// Somehow, Solidity semver without an operator is considered to be "exact",
|
||||
// but lack of operator automatically marks the operator as Caret, so we need
|
||||
|
|
Loading…
Reference in New Issue