Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contract mock system #583

Merged
merged 21 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.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"ethcontract-common",
"ethcontract-derive",
"ethcontract-generate",
"ethcontract-mock",
"examples",
"examples/documentation",
"examples/generate",
Expand Down
111 changes: 93 additions & 18 deletions ethcontract-generate/src/generate/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ fn expand_functions(cx: &Context) -> Result<TokenStream> {
.functions()
.map(|function| {
let signature = function.abi_signature();
expand_function(cx, function, aliases.remove(&signature))
.with_context(|| format!("error expanding function '{}'", signature))

let alias = aliases.remove(&signature);
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case()));
let signature = function.abi_signature();
let selector = expand_selector(function.selector());
let inputs = expand_inputs(&function.inputs)
.with_context(|| format!("error expanding function '{}'", signature))?;
let input_types = expand_input_types(&function.inputs)
.with_context(|| format!("error expanding function '{}'", signature))?;
let outputs = expand_outputs(&function.outputs)
.with_context(|| format!("error expanding function '{}'", signature))?;

Ok((function, name, selector, inputs, input_types, outputs))
})
.collect::<Result<Vec<_>>>()?;
if let Some(unused) = aliases.keys().next() {
Expand All @@ -40,13 +51,31 @@ fn expand_functions(cx: &Context) -> Result<TokenStream> {
));
}

let methods = functions
.iter()
.map(|(function, name, selector, inputs, _, outputs)| {
expand_function(cx, function, name, selector, inputs, outputs)
});

let methods_attrs = quote! { #[derive(Clone)] };
let methods_struct = quote! {
struct Methods {
instance: self::ethcontract::dyns::DynInstance,
}
};

let signature_accessors =
functions
.iter()
.map(|(function, name, selector, _, input_types, outputs)| {
expand_signature_accessor(function, name, selector, input_types, outputs)
});

let signatures_attrs = quote! { #[derive(Clone, Copy)] };
let signatures_struct = quote! {
struct Signatures;
};

if functions.is_empty() {
// NOTE: The methods struct is still needed when there are no functions
// as it contains the the runtime instance. The code is setup this way
Expand All @@ -55,11 +84,19 @@ fn expand_functions(cx: &Context) -> Result<TokenStream> {
return Ok(quote! {
#methods_attrs
#methods_struct

#signatures_attrs
#signatures_struct
});
}

Ok(quote! {
impl Contract {
/// Returns an object that allows accessing typed method signatures.
pub fn signatures() -> Signatures {
Signatures
}

/// Retrieves a reference to type containing all the generated
/// contract methods. This can be used for methods where the name
/// would collide with a common method (like `at` or `deployed`).
Expand All @@ -68,13 +105,21 @@ fn expand_functions(cx: &Context) -> Result<TokenStream> {
}
}

/// Type containing signatures for all methods for generated contract type.
#signatures_attrs
pub #signatures_struct

impl Signatures {
#( #signature_accessors )*
}

/// Type containing all contract methods for generated contract type.
#methods_attrs
pub #methods_struct

#[allow(clippy::too_many_arguments, clippy::type_complexity)]
impl Methods {
#( #functions )*
#( #methods )*
}

impl std::ops::Deref for Contract {
Expand All @@ -86,10 +131,15 @@ fn expand_functions(cx: &Context) -> Result<TokenStream> {
})
}

fn expand_function(cx: &Context, function: &Function, alias: Option<Ident>) -> Result<TokenStream> {
let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case()));
fn expand_function(
cx: &Context,
function: &Function,
name: &Ident,
selector: &TokenStream,
inputs: &TokenStream,
outputs: &TokenStream,
) -> TokenStream {
let signature = function.abi_signature();
let selector = expand_selector(function.selector());

let doc_str = cx
.contract
Expand All @@ -102,8 +152,6 @@ fn expand_function(cx: &Context, function: &Function, alias: Option<Ident>) -> R
.unwrap_or("Generated by `ethcontract`");
let doc = util::expand_doc(doc_str);

let input = expand_inputs(&function.inputs)?;
let outputs = expand_fn_outputs(&function.outputs)?;
let (method, result_type_name) = match function.state_mutability {
StateMutability::Pure | StateMutability::View => {
(quote! { view_method }, quote! { DynViewMethodBuilder })
Expand All @@ -113,13 +161,32 @@ fn expand_function(cx: &Context, function: &Function, alias: Option<Ident>) -> R
let result = quote! { self::ethcontract::dyns::#result_type_name<#outputs> };
let arg = expand_inputs_call_arg(&function.inputs);

Ok(quote! {
quote! {
#doc
pub fn #name(&self #input) -> #result {
pub fn #name(&self #inputs) -> #result {
self.instance.#method(#selector, #arg)
.expect("generated call")
}
})
}
}

fn expand_signature_accessor(
function: &Function,
name: &Ident,
selector: &TokenStream,
input_types: &TokenStream,
outputs: &TokenStream,
) -> TokenStream {
let doc = util::expand_doc(&format!(
"Returns signature for method `{}`.",
function.signature()
));
quote! {
#doc
pub fn #name(&self) -> self::ethcontract::contract::Signature<#input_types, #outputs> {
self::ethcontract::contract::Signature::new(#selector)
}
}
}

pub(crate) fn expand_inputs(inputs: &[Param]) -> Result<TokenStream> {
Expand All @@ -135,6 +202,14 @@ pub(crate) fn expand_inputs(inputs: &[Param]) -> Result<TokenStream> {
Ok(quote! { #( , #params )* })
}

pub(crate) fn expand_input_types(inputs: &[Param]) -> Result<TokenStream> {
let params = inputs
.iter()
.map(|param| types::expand(&param.kind))
.collect::<Result<Vec<_>>>()?;
Ok(quote! { ( #( #params ,)* ) })
}

pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
let names = inputs
.iter()
Expand All @@ -143,7 +218,7 @@ pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
quote! { ( #( #names ,)* ) }
}

fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
fn expand_outputs(outputs: &[Param]) -> Result<TokenStream> {
match outputs.len() {
0 => Ok(quote! { () }),
1 => types::expand(&outputs[0].kind),
Expand Down Expand Up @@ -216,14 +291,14 @@ mod tests {
}

#[test]
fn expand_fn_outputs_empty() {
assert_quote!(expand_fn_outputs(&[],).unwrap(), { () });
fn expand_outputs_empty() {
assert_quote!(expand_outputs(&[],).unwrap(), { () });
}

#[test]
fn expand_fn_outputs_single() {
fn expand_outputs_single() {
assert_quote!(
expand_fn_outputs(&[Param {
expand_outputs(&[Param {
name: "a".to_string(),
kind: ParamType::Bool,
}])
Expand All @@ -233,9 +308,9 @@ mod tests {
}

#[test]
fn expand_fn_outputs_multiple() {
fn expand_outputs_multiple() {
assert_quote!(
expand_fn_outputs(&[
expand_outputs(&[
Param {
name: "a".to_string(),
kind: ParamType::Bool,
Expand Down
23 changes: 23 additions & 0 deletions ethcontract-mock/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "ethcontract-mock"
version = "0.14.0"
authors = ["Gnosis developers <[email protected]>"]
edition = "2018"
license = "MIT OR Apache-2.0"
repository = "https://github.com/gnosis/ethcontract-rs"
homepage = "https://github.com/gnosis/ethcontract-rs"
documentation = "https://docs.rs/ethcontract"
description = """
Tools for mocking ethereum contracts.
"""

[dependencies]
ethcontract = { version = "0.14.0", path = "../ethcontract" }
hex = "0.4"
mockall = "0.10"
rlp = "0.5"
predicates = "1.0"

[dev-dependencies]
tokio = { version = "1.6", features = ["macros"] }
ethcontract-derive = { version = "0.14.0", path = "../ethcontract-derive" }
25 changes: 25 additions & 0 deletions ethcontract-mock/src/details/default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Helpers for building default values for tokens.

use ethcontract::common::abi::{Bytes, Int, ParamType, Token, Uint};
use ethcontract::Address;

/// Builds a default value for the given solidity type.
pub fn default(ty: &ParamType) -> Token {
match ty {
ParamType::Address => Token::Address(Address::default()),
ParamType::Bytes => Token::Bytes(Bytes::default()),
ParamType::Int(_) => Token::Int(Int::default()),
ParamType::Uint(_) => Token::Uint(Uint::default()),
taminomara marked this conversation as resolved.
Show resolved Hide resolved
ParamType::Bool => Token::Bool(false),
ParamType::String => Token::String(String::default()),
ParamType::Array(_) => Token::Array(Vec::new()),
ParamType::FixedBytes(n) => Token::FixedBytes(vec![0; *n]),
ParamType::FixedArray(ty, n) => Token::FixedArray(vec![default(ty); *n]),
ParamType::Tuple(tys) => default_tuple(tys.iter()),
}
}

/// Builds a default value for the given solidity tuple type.
pub fn default_tuple<'a>(tys: impl Iterator<Item = &'a ParamType>) -> Token {
Token::Tuple(tys.map(default).collect())
}
Loading