From 55f2544a88cfeaf0495d2e46521478885dea6f7d Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:43:39 +0200 Subject: [PATCH 01/21] Implement `Tokenize` for `Token` --- ethcontract/src/tokens.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ethcontract/src/tokens.rs b/ethcontract/src/tokens.rs index e8d2a9c7..7e2df8eb 100644 --- a/ethcontract/src/tokens.rs +++ b/ethcontract/src/tokens.rs @@ -26,7 +26,7 @@ pub enum Error { #[error("expected a different token type")] TypeMismatch, /// Tokenize::from_token is called with integer that doesn't fit in the rust type. - #[error("abi integer is does not fit rust integer")] + #[error("abi integer does not fit rust integer")] IntegerMismatch, /// Tokenize::from_token token is fixed bytes with wrong length. #[error("expected a different number of fixed bytes")] @@ -57,6 +57,19 @@ pub trait Tokenize { )] pub struct Bytes(pub T); +impl Tokenize for Token { + fn from_token(token: Token) -> Result + where + Self: Sized, + { + Ok(token) + } + + fn into_token(self) -> Token { + self + } +} + impl Tokenize for Bytes> { fn from_token(token: Token) -> Result where From 9df5a84e4420542254341b961b4130122435c4c9 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:44:02 +0200 Subject: [PATCH 02/21] Add `Signature` that will allow better type inference for mocks --- ethcontract/src/contract.rs | 46 +++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/ethcontract/src/contract.rs b/ethcontract/src/contract.rs index 08e99c16..932fb944 100644 --- a/ethcontract/src/contract.rs +++ b/ethcontract/src/contract.rs @@ -1,4 +1,4 @@ -//! Abtraction for interacting with ethereum smart contracts. Provides methods +//! Abstraction for interacting with ethereum smart contracts. Provides methods //! for sending transactions to contracts as well as querying current contract //! state. @@ -26,6 +26,35 @@ pub use self::event::{ StreamEvent, Topic, }; pub use self::method::{MethodBuilder, MethodDefaults, ViewMethodBuilder}; +use std::marker::PhantomData; + +/// Method signature with additional info about method's input and output types. +/// +/// Additional type parameters are used to help with type inference +/// for instance's [`method`] and [`view_method`] functions. +/// +/// [`method`]: `Instance::method` +/// [`view_method`]: `Instance::view_method` +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +pub struct Signature(pub H32, pub std::marker::PhantomData<(P, R)>); + +impl Signature { + /// Wraps raw signature. + pub fn new(signature: H32) -> Self { + Signature(signature, PhantomData) + } + + /// Unwraps raw signature. + pub fn into_inner(self) -> H32 { + self.0 + } +} + +impl From for Signature { + fn from(signature: H32) -> Self { + Signature::new(signature) + } +} /// Represents a contract instance at an address. Provides methods for /// contract interaction. @@ -163,17 +192,22 @@ impl Instance { /// Returns a method builder to setup a call or transaction on a smart /// contract method. Note that calls just get evaluated on a node but do not /// actually commit anything to the block chain. - pub fn method(&self, signature: H32, params: P) -> AbiResult> + pub fn method( + &self, + signature: impl Into>, + params: P, + ) -> AbiResult> where P: Tokenize, R: Tokenize, { + let signature = signature.into().into_inner(); let signature = signature.as_ref(); let function = self .methods .get(signature) .map(|(name, index)| &self.abi.functions[name][*index]) - .ok_or_else(|| AbiError::InvalidName(hex::encode(&signature)))?; + .ok_or_else(|| AbiError::InvalidName(hex::encode(signature)))?; let tokens = match params.into_token() { ethcontract_common::abi::Token::Tuple(tokens) => tokens, _ => unreachable!("function arguments are always tuples"), @@ -195,7 +229,11 @@ impl Instance { /// Returns a view method builder to setup a call to a smart contract. View /// method builders can't actually send transactions and only query contract /// state. - pub fn view_method(&self, signature: H32, params: P) -> AbiResult> + pub fn view_method( + &self, + signature: impl Into>, + params: P, + ) -> AbiResult> where P: Tokenize, R: Tokenize, From fc1f786eebf3fb52c9866fbd0162086dd26cb582 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:44:31 +0200 Subject: [PATCH 03/21] Export method signatures for generated contracts --- ethcontract-generate/src/generate/methods.rs | 111 ++++++++++++++++--- 1 file changed, 93 insertions(+), 18 deletions(-) diff --git a/ethcontract-generate/src/generate/methods.rs b/ethcontract-generate/src/generate/methods.rs index 55845480..0db5e9ed 100644 --- a/ethcontract-generate/src/generate/methods.rs +++ b/ethcontract-generate/src/generate/methods.rs @@ -29,8 +29,19 @@ fn expand_functions(cx: &Context) -> Result { .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::>>()?; if let Some(unused) = aliases.keys().next() { @@ -40,6 +51,12 @@ fn expand_functions(cx: &Context) -> Result { )); } + 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 { @@ -47,6 +64,18 @@ fn expand_functions(cx: &Context) -> Result { } }; + 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 @@ -55,11 +84,19 @@ fn expand_functions(cx: &Context) -> Result { 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`). @@ -68,13 +105,21 @@ fn expand_functions(cx: &Context) -> Result { } } + /// 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 { @@ -86,10 +131,15 @@ fn expand_functions(cx: &Context) -> Result { }) } -fn expand_function(cx: &Context, function: &Function, alias: Option) -> Result { - 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 @@ -102,8 +152,6 @@ fn expand_function(cx: &Context, function: &Function, alias: Option) -> 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 }) @@ -113,13 +161,32 @@ fn expand_function(cx: &Context, function: &Function, alias: Option) -> 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 { @@ -135,6 +202,14 @@ pub(crate) fn expand_inputs(inputs: &[Param]) -> Result { Ok(quote! { #( , #params )* }) } +pub(crate) fn expand_input_types(inputs: &[Param]) -> Result { + let params = inputs + .iter() + .map(|param| types::expand(¶m.kind)) + .collect::>>()?; + Ok(quote! { ( #( #params ,)* ) }) +} + pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { let names = inputs .iter() @@ -143,7 +218,7 @@ pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { quote! { ( #( #names ,)* ) } } -fn expand_fn_outputs(outputs: &[Param]) -> Result { +fn expand_outputs(outputs: &[Param]) -> Result { match outputs.len() { 0 => Ok(quote! { () }), 1 => types::expand(&outputs[0].kind), @@ -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, }]) @@ -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, From 21de911461a0e325056e7b481d245b38ab52f29c Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:45:47 +0200 Subject: [PATCH 04/21] Add ethcontract-mock crate --- Cargo.toml | 1 + ethcontract-mock/Cargo.toml | 23 +++++++++++++++++++++++ ethcontract-mock/src/lib.rs | 4 ++++ 3 files changed, 28 insertions(+) create mode 100644 ethcontract-mock/Cargo.toml create mode 100644 ethcontract-mock/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 4875665f..2c35b353 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "ethcontract-common", "ethcontract-derive", "ethcontract-generate", + "ethcontract-mock", "examples", "examples/documentation", "examples/generate", diff --git a/ethcontract-mock/Cargo.toml b/ethcontract-mock/Cargo.toml new file mode 100644 index 00000000..ded61661 --- /dev/null +++ b/ethcontract-mock/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ethcontract-mock" +version = "0.14.0" +authors = ["Gnosis developers "] +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" } diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs new file mode 100644 index 00000000..fd732921 --- /dev/null +++ b/ethcontract-mock/src/lib.rs @@ -0,0 +1,4 @@ +#![deny(missing_docs, unsafe_code)] + +//! This crate allows emulating ethereum node with a limited number +//! of supported RPC calls, enabling you to mock ethereum contracts. From 9d73363c215bbc23d002428141dd16dc856fb1f4 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:48:15 +0200 Subject: [PATCH 05/21] Add trait that converts tuple of predicates into a predicate over tuples --- ethcontract-mock/src/lib.rs | 5 ++ ethcontract-mock/src/predicate.rs | 76 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 ethcontract-mock/src/predicate.rs diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index fd732921..864596b2 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -2,3 +2,8 @@ //! This crate allows emulating ethereum node with a limited number //! of supported RPC calls, enabling you to mock ethereum contracts. + +#[doc(no_inline)] +pub use ethcontract::contract::Signature; + +mod predicate; diff --git a/ethcontract-mock/src/predicate.rs b/ethcontract-mock/src/predicate.rs new file mode 100644 index 00000000..b497dc9d --- /dev/null +++ b/ethcontract-mock/src/predicate.rs @@ -0,0 +1,76 @@ +//! Helpers for working with predicates. +//! +//! Note: contents of this module are meant to be used via the [`Into`] trait. +//! They are not a part of public API. + +use predicates::reflection::{Child, PredicateReflection}; +use predicates::Predicate; + +/// This trait allows converting tuples of predicates into predicates that +/// accept tuples. That is, if `T = (T1, T2, ...)`, this trait can convert +/// a tuple of predicates `(Predicate, Predicate, ...)` +/// into a `Predicate<(T1, T2, ...)>`. +pub trait TuplePredicate { + /// Concrete implementation of a tuple predicate, depends on tuple length. + type P: Predicate; + + /// Given that `self` is a tuple of predicates `ps = (p1, p2, ...)`, + /// returns a predicate that accepts a tuple `ts = (t1, t2, ...)` + /// and applies predicates element-wise: `ps.0(ts.0) && ps.1(ts.1) && ...`. + fn into_predicate(self) -> Self::P; +} + +pub mod detail { + use super::*; + + macro_rules! impl_tuple_predicate { + ($name: ident, $count: expr, $( $t: ident : $p: ident : $n: tt, )*) => { + pub struct $name<$($t, $p: Predicate<$t>, )*>(($($p, )*), std::marker::PhantomData<($($t, )*)>); + + impl<$($t, $p: Predicate<$t>, )*> std::fmt::Display for $name<$($t, $p, )*> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "element-wise tuple predicate") + } + } + + impl<$($t, $p: Predicate<$t>, )*> PredicateReflection for $name<$($t, $p, )*> { + fn children(&self) -> Box> + '_> { + let params = vec![$(predicates::reflection::Child::new(stringify!($n), &self.0.$n), )*]; + Box::new(params.into_iter()) + } + } + + impl<$($t, $p: Predicate<$t>, )*> Predicate<($($t, )*)> for $name<$($t, $p, )*> { + #[allow(unused_variables)] + fn eval(&self, variable: &($($t, )*)) -> bool { + $(self.0.$n.eval(&variable.$n) && )* true + } + } + + impl<$($t, $p: Predicate<$t>, )*> TuplePredicate<($($t, )*)> for ($($p, )*) { + type P = $name<$($t, $p, )*>; + fn into_predicate(self) -> Self::P { + $name(self, std::marker::PhantomData) + } + } + } + } + + impl_tuple_predicate!(TuplePredicate0, 0,); + impl_tuple_predicate!(TuplePredicate1, 1, T0:P0:0, ); + impl_tuple_predicate!(TuplePredicate2, 2, T0:P0:0, T1:P1:1, ); + impl_tuple_predicate!(TuplePredicate3, 3, T0:P0:0, T1:P1:1, T2:P2:2, ); + impl_tuple_predicate!(TuplePredicate4, 4, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, ); + impl_tuple_predicate!(TuplePredicate5, 5, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, ); + impl_tuple_predicate!(TuplePredicate6, 6, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, ); + impl_tuple_predicate!(TuplePredicate7, 7, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, ); + impl_tuple_predicate!(TuplePredicate8, 8, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, ); + impl_tuple_predicate!(TuplePredicate9, 9, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, ); + impl_tuple_predicate!(TuplePredicate10, 10, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, ); + impl_tuple_predicate!(TuplePredicate11, 11, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, ); + impl_tuple_predicate!(TuplePredicate12, 12, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, ); + impl_tuple_predicate!(TuplePredicate13, 13, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, ); + impl_tuple_predicate!(TuplePredicate14, 14, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, ); + impl_tuple_predicate!(TuplePredicate15, 15, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, T14:P14:14, ); + impl_tuple_predicate!(TuplePredicate16, 16, T0:P0:0, T1:P1:1, T2:P2:2, T3:P3:3, T4:P4:4, T5:P5:5, T6:P6:6, T7:P7:7, T8:P8:8, T9:P9:9, T10:P10:10, T11:P11:11, T12:P12:12, T13:P13:13, T14:P14:14, T15:P15:15, ); +} From e006e6ab623ea30e9caea260b6267d402e0af509 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:48:32 +0200 Subject: [PATCH 06/21] Add custom range implementation --- ethcontract-mock/src/range.rs | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 ethcontract-mock/src/range.rs diff --git a/ethcontract-mock/src/range.rs b/ethcontract-mock/src/range.rs new file mode 100644 index 00000000..14482df9 --- /dev/null +++ b/ethcontract-mock/src/range.rs @@ -0,0 +1,93 @@ +//! Helpers for working with rust's ranges. +//! +//! Note: contents of this module are meant to be used via the [`Into`] trait. +//! They are not a part of public API. + +use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive}; + +/// A type that represents a rust's range, i.e., a struct produced +/// by range syntax like `..`, `a..`, `..b`, `..=c`, `d..e`, or `f..=g`. +/// +/// Each of the above range types is represented by a distinct `std` struct. +/// Standard library does not export a single struct to represent all of them, +/// so we have to implement it ourselves. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct TimesRange(usize, usize); + +impl TimesRange { + /// Checks if expectation can be called if it was already called + /// this number of times. + pub fn can_call(&self, x: usize) -> bool { + x + 1 < self.1 + } + + /// Checks if the given element is contained by this range. + pub fn contains(&self, x: usize) -> bool { + self.0 <= x && x < self.1 + } + + /// Checks if this range contains exactly one element. + pub fn is_exact(&self) -> bool { + (self.1 - self.0) == 1 + } + + /// Returns lower bound on this range. + pub fn lower_bound(&self) -> usize { + self.0 + } + + /// Returns upper bound on this range. + pub fn upper_bound(&self) -> usize { + self.1 + } +} + +impl Default for TimesRange { + fn default() -> TimesRange { + TimesRange(0, usize::MAX) + } +} + +impl From for TimesRange { + fn from(n: usize) -> TimesRange { + TimesRange(n, n + 1) + } +} + +impl From> for TimesRange { + fn from(r: Range) -> TimesRange { + assert!(r.end > r.start, "backwards range"); + TimesRange(r.start, r.end) + } +} + +impl From> for TimesRange { + fn from(r: RangeFrom) -> TimesRange { + TimesRange(r.start, usize::MAX) + } +} + +impl From for TimesRange { + fn from(_: RangeFull) -> TimesRange { + TimesRange(0, usize::MAX) + } +} + +impl From> for TimesRange { + fn from(r: RangeInclusive) -> TimesRange { + assert!(r.end() >= r.start(), "backwards range"); + TimesRange(*r.start(), *r.end() + 1) + } +} + +impl From> for TimesRange { + fn from(r: RangeTo) -> TimesRange { + TimesRange(0, r.end) + } +} + +impl From> for TimesRange { + fn from(r: RangeToInclusive) -> TimesRange { + TimesRange(0, r.end + 1) + } +} From 85070c5b489e59a81ee0f46f68e72ce1627f4507 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:54:20 +0200 Subject: [PATCH 07/21] Add mock interface --- ethcontract-mock/src/lib.rs | 321 ++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 864596b2..fd7acc9c 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -3,7 +3,328 @@ //! This crate allows emulating ethereum node with a limited number //! of supported RPC calls, enabling you to mock ethereum contracts. +use crate::predicate::TuplePredicate; +use crate::range::TimesRange; +use ethcontract::common::hash::H32; +use ethcontract::common::Abi; +use ethcontract::dyns::{DynInstance, DynTransport, DynWeb3}; +use ethcontract::tokens::Tokenize; +use ethcontract::{Address, U256}; +use std::marker::PhantomData; + #[doc(no_inline)] pub use ethcontract::contract::Signature; mod predicate; +mod range; + +/// Mock ethereum node. +#[derive(Clone)] +pub struct Mock { +} + +impl Mock { + /// Creates a new mock chain. + pub fn new(chain_id: u64) -> Self { + todo!() + } + + /// Creates a `Web3` object that can be used to interact with + /// the mocked chain. + pub fn web3(&self) -> DynWeb3 { + todo!() + } + + /// Creates a `Transport` object that can be used to interact with + /// the mocked chain. + pub fn transport(&self) -> DynTransport { + todo!() + } + + /// Deploys a new mocked contract and returns an object that allows + /// configuring expectations for contract methods. + pub fn deploy(&self, abi: Abi) -> Contract { + todo!() + } + + /// Updates gas price that is returned by RPC call `eth_gasPrice`. + /// + /// Mock node does not simulate gas consumption, so this value does not + /// affect anything if you don't call `eth_gasPrice`. + pub fn update_gas_price(&self, gas_price: u64) { + todo!() + } + + /// Verifies that all expectations on all contracts have been met, + /// then clears all expectations. + pub fn checkpoint(&self) { + todo!() + } +} + +impl std::fmt::Debug for Mock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Mock") + } +} + +/// A mocked contract deployed by the mock node. +/// +/// This struct allows setting up expectations on which contract methods +/// will be called, with what arguments, in what order, etc. +pub struct Contract { +} + +impl Contract { + /// Creates a `Web3` object that can be used to interact with + /// the mocked chain on which this contract is deployed. + pub fn web3(&self) -> DynWeb3 { + todo!() + } + + /// Creates a `Transport` object that can be used to interact with + /// the mocked chain. + pub fn transport(&self) -> DynTransport { + todo!() + } + + /// Creates a contract `Instance` that can be used to interact with + /// this contract. + pub fn instance(&self) -> DynInstance { + todo!() + } + + /// Consumes this object and transforms it into a contract `Instance` + /// that can be used to interact with this contract. + pub fn into_instance(self) -> DynInstance { + todo!() + } + + /// Returns a reference to the contract's ABI. + pub fn abi(&self) -> &Abi { + todo!() + } + + /// Returns contract's address. + pub fn address(&self) -> Address { + todo!() + } + + /// Adds a new expectation for contract method. See [`Expectation`]. + pub fn expect( + &self, + signature: impl Into>, + ) -> Expectation { + todo!() + } + + /// Adds a new expectation for contract method that only matches view calls. + /// + /// This is an equivalent of [`expect`] followed by [`allow_transactions`]`(false)`. + /// + /// [`expect`]: Contract::expect + /// [`allow_transactions`]: Expectation::allow_transactions + pub fn expect_call( + &self, + signature: impl Into>, + ) -> Expectation { + self.expect(signature).allow_transactions(false) + } + + /// Adds a new expectation for contract method that only matches transactions. + /// + /// This is an equivalent of [`expect`] followed by [`allow_calls`]`(false)`. + /// + /// [`expect`]: Contract::expect + /// [`allow_calls`]: Expectation::allow_calls + pub fn expect_transaction( + &self, + signature: impl Into>, + ) -> Expectation { + self.expect(signature).allow_calls(false) + } + + /// Verifies that all expectations on this contract have been met, + /// then clears all expectations. + pub fn checkpoint(&self) { + todo!() + } +} + +/// Expectation for contract method. +pub struct Expectation { + _ph: PhantomData<(P, R)>, +} + +impl Expectation { + /// Specifies how many times this expectation can be called. + pub fn times(self, times: impl Into) -> Self { + todo!() + } + + /// Indicates that this expectation can be called exactly zero times. + /// + /// See [`times`] for more info. + /// + /// [`times`]: Expectation::times + pub fn never(self) -> Self { + self.times(0) + } + + /// Indicates that this expectation can be called exactly one time. + /// + /// See [`times`] for more info. + /// + /// [`times`]: Expectation::times + pub fn once(self) -> Self { + self.times(1) + } + + /// Adds this expectation to a sequence. + pub fn in_sequence(self, sequence: &mut mockall::Sequence) -> Self { + todo!() + } + + /// Sets number of blocks that should be mined on top of the transaction + /// block. This method can be useful when there are custom transaction + /// confirmation settings. + pub fn confirmations(self, confirmations: u64) -> Self { + todo!() + } + + /// Sets predicate for this expectation. + pub fn predicate(self, pred: T) -> Self + where + T: TuplePredicate

+ Send + 'static, + >::P: Send, + { + todo!() + } + + /// Sets predicate function for this expectation. This function accepts + /// a tuple of method's arguments and returns `true` if this + /// expectation should be called. See [`predicate`] for more info. + /// + /// This method will overwrite any predicate that was set before. + /// + /// [`predicate`]: Expectation::predicate + pub fn predicate_fn(self, pred: impl Fn(&P) -> bool + Send + 'static) -> Self { + todo!() + } + + /// Sets predicate function for this expectation. This function accepts + /// a [call context] and a tuple of method's arguments and returns `true` + /// if this expectation should be called. See [`predicate`] for more info. + /// + /// This method will overwrite any predicate that was set before. + /// + /// [call context]: CallContext + /// [`predicate`]: Expectation::predicate + pub fn predicate_fn_ctx( + self, + pred: impl Fn(&CallContext, &P) -> bool + Send + 'static, + ) -> Self { + todo!() + } + + /// Indicates that this expectation only applies to view calls. + /// + /// This method will not override predicates set by [`predicate`] and + /// similar methods. + /// + /// See also [`Contract::expect_call`]. + /// + /// [`predicate`]: Expectation::predicate + pub fn allow_calls(self, allow_calls: bool) -> Self { + todo!() + } + + /// Indicates that this expectation only applies to transactions. + /// + /// This method will not override predicates set by [`predicate`] and + /// similar methods. + /// + /// See also [`Contract::expect_transaction`]. + /// + /// [`predicate`]: Expectation::predicate + pub fn allow_transactions(self, allow_transactions: bool) -> Self { + todo!() + } + + /// Sets return value of the method. + /// + /// By default, call to this expectation will result in solidity's default + /// value for the method's return type. This method allows specifying + /// a custom return value. + /// + /// This method will overwrite any return value or callback + /// that was set before. + pub fn returns(self, returns: R) -> Self { + todo!() + } + + /// Sets callback function that will be used to calculate return value + /// of the method. This function accepts a tuple of method's arguments + /// and returns method's result or [`Err`] if transaction + /// should be reverted. + pub fn returns_fn(self, returns: impl Fn(P) -> Result + Send + 'static) -> Self { + todo!() + } + + /// Sets callback function that will be used to calculate return value + /// of the method. This function accepts a [call context] and a tuple + /// of method's arguments and returns method's result or [`Err`] + /// if transaction should be reverted. + pub fn returns_fn_ctx( + self, + returns: impl Fn(&CallContext, P) -> Result + Send + 'static, + ) -> Self { + todo!() + } + + /// Sets return value of the method to an error, meaning that calls to this + /// expectation result in reverted transaction. + pub fn returns_error(self, error: String) -> Self { + todo!() + } + + /// Sets return value of the method to a default value for its solidity type. + /// See [`returns`] for more info. + pub fn returns_default(self) -> Self { + todo!() + } +} + +/// Information about method call that's being processed. +pub struct CallContext { + /// If `true`, this is a view call, otherwise this is a transaction. + pub is_view_call: bool, + + /// Account that issued a view call or a transaction. + /// + /// Can be zero in case of a view call. + pub from: Address, + + /// Address of the current contract. + pub to: Address, + + /// Current nonce of the account that issued a view call or a transaction. + pub nonce: U256, + + /// Maximum gas amount that this operation is allowed to spend. + /// + /// Mock node does not simulate gas consumption, so this value does not + /// affect anything if you don't check it in your test code. + pub gas: U256, + + /// Gas price for this view call or transaction. + /// + /// Mock node does not simulate gas consumption, so this value does not + /// affect anything if you don't check it in your test code. + pub gas_price: U256, + + /// Amount of ETH that's transferred with the call. + /// + /// This value is only non-zero if the method is payable. + pub value: U256, +} From 7be665fb802aaa8349a38066bde7433fec28cd00 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:56:39 +0200 Subject: [PATCH 08/21] Add module for mock implementation --- ethcontract-mock/src/details/mod.rs | 1 + ethcontract-mock/src/lib.rs | 1 + 2 files changed, 2 insertions(+) create mode 100644 ethcontract-mock/src/details/mod.rs diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs new file mode 100644 index 00000000..258f0235 --- /dev/null +++ b/ethcontract-mock/src/details/mod.rs @@ -0,0 +1 @@ +//! Implementation details of mock node. diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index fd7acc9c..0a72a212 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -15,6 +15,7 @@ use std::marker::PhantomData; #[doc(no_inline)] pub use ethcontract::contract::Signature; +mod details; mod predicate; mod range; From 3424d84ddb08a72e281f32432cd54ca178bbcdd0 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:57:33 +0200 Subject: [PATCH 09/21] Add function to calculate default value for a solidity type --- ethcontract-mock/src/details/default.rs | 25 +++++++++++++++++++++++++ ethcontract-mock/src/details/mod.rs | 2 ++ 2 files changed, 27 insertions(+) create mode 100644 ethcontract-mock/src/details/default.rs diff --git a/ethcontract-mock/src/details/default.rs b/ethcontract-mock/src/details/default.rs new file mode 100644 index 00000000..da3bc042 --- /dev/null +++ b/ethcontract-mock/src/details/default.rs @@ -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()), + 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) -> Token { + Token::Tuple(tys.map(default).collect()) +} diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index 258f0235..8f5d212e 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1 +1,3 @@ //! Implementation details of mock node. + +mod default; From c1b08c15d61a3b0c59723b54f55d8eabb8d28e4e Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:58:32 +0200 Subject: [PATCH 10/21] Add function to verify transaction signature --- ethcontract-mock/src/details/mod.rs | 2 + ethcontract-mock/src/details/sign.rs | 90 +++++++++++++++++++++ ethcontract-mock/src/details/transaction.rs | 25 ++++++ 3 files changed, 117 insertions(+) create mode 100644 ethcontract-mock/src/details/sign.rs create mode 100644 ethcontract-mock/src/details/transaction.rs diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index 8f5d212e..de19a7ef 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1,3 +1,5 @@ //! Implementation details of mock node. mod default; +mod sign; +mod transaction; diff --git a/ethcontract-mock/src/details/sign.rs b/ethcontract-mock/src/details/sign.rs new file mode 100644 index 00000000..f668d96e --- /dev/null +++ b/ethcontract-mock/src/details/sign.rs @@ -0,0 +1,90 @@ +//! Helpers to work with signed transactions. + +use crate::details::transaction::Transaction; +use ethcontract::common::abi::ethereum_types::BigEndianHash; +use ethcontract::web3::signing; +use ethcontract::web3::types::{Address, H256, U256}; + +/// Parses and verifies raw transaction, including chain ID. +/// +/// Panics if transaction is malformed or if verification fails. +pub fn verify(raw_tx: &[u8], node_chain_id: u64) -> Transaction { + let rlp = rlp::Rlp::new(raw_tx); + + fn err() -> ! { + panic!("invalid transaction data"); + } + fn res(r: Result) -> T { + r.unwrap_or_else(|_| err()) + } + + if !matches!(rlp.prototype(), Ok(rlp::Prototype::List(9))) { + err(); + } + + if res(rlp.at(3)).size() == 0 { + // TODO: + // + // We could support deployments via RPC calls by introducing + // something like `expect_deployment` method to `Mock` struct. + panic!("mock client does not support deploying contracts via transaction, use `Mock::deploy` instead"); + } + + let nonce: U256 = res(rlp.val_at(0)); + let gas_price: U256 = res(rlp.val_at(1)); + let gas: U256 = res(rlp.val_at(2)); + let to: Address = res(rlp.val_at(3)); + let value: U256 = res(rlp.val_at(4)); + let data: Vec = res(rlp.val_at(5)); + let v: u64 = res(rlp.val_at(6)); + let r = H256::from_uint(&res(rlp.val_at(7))); + let s = H256::from_uint(&res(rlp.val_at(8))); + + let (chain_id, standard_v) = match v { + v if v >= 35 => ((v - 35) / 2, (v - 25) % 2), + 27 | 28 => panic!("transactions must use eip-155 signatures"), + _ => panic!("invalid transaction signature, v value is out of range"), + }; + + if chain_id != node_chain_id { + panic!("invalid transaction signature, chain id mismatch"); + } + + let msg_hash = { + let mut rlp = rlp::RlpStream::new(); + + rlp.begin_list(9); + rlp.append(&nonce); + rlp.append(&gas_price); + rlp.append(&gas); + rlp.append(&to); + rlp.append(&value); + rlp.append(&data); + rlp.append(&chain_id); + rlp.append(&0u8); + rlp.append(&0u8); + + signing::keccak256(rlp.as_raw()) + }; + + let signature = { + let mut signature = [0u8; 64]; + signature[..32].copy_from_slice(r.as_bytes()); + signature[32..].copy_from_slice(s.as_bytes()); + signature + }; + + let from = signing::recover(&msg_hash, &signature, standard_v as _) + .unwrap_or_else(|_| panic!("invalid transaction signature, verification failed")); + + Transaction { + from, + to, + nonce, + gas, + gas_price, + value, + data, + hash: signing::keccak256(raw_tx).into(), + } +} diff --git a/ethcontract-mock/src/details/transaction.rs b/ethcontract-mock/src/details/transaction.rs new file mode 100644 index 00000000..a06473d0 --- /dev/null +++ b/ethcontract-mock/src/details/transaction.rs @@ -0,0 +1,25 @@ +//! Common transaction types. + +use ethcontract::{Address, H256, U256}; + +/// Basic transaction parameters. +pub struct Transaction { + pub from: Address, + pub to: Address, + pub nonce: U256, + pub gas: U256, + pub gas_price: U256, + pub value: U256, + pub data: Vec, + pub hash: H256, +} + +/// Transaction execution result. +pub struct TransactionResult { + /// Result of a method call, error if call is aborted. + pub result: Result, String>, + + /// How many blocks should be mined on top of transaction's block + /// for confirmation to be successful. + pub confirmations: u64, +} From b82795dbf0279a29828ad928439a4048ac303acf Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 09:59:17 +0200 Subject: [PATCH 11/21] Add a helper to parse RPC call arguments --- ethcontract-mock/src/details/mod.rs | 1 + ethcontract-mock/src/details/parse.rs | 97 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 ethcontract-mock/src/details/parse.rs diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index de19a7ef..a0015f82 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1,5 +1,6 @@ //! Implementation details of mock node. mod default; +mod parse; mod sign; mod transaction; diff --git a/ethcontract-mock/src/details/parse.rs b/ethcontract-mock/src/details/parse.rs new file mode 100644 index 00000000..6dc719d9 --- /dev/null +++ b/ethcontract-mock/src/details/parse.rs @@ -0,0 +1,97 @@ +//! Helpers to parse RPC call arguments. + +use ethcontract::json::{from_value, Value}; +use ethcontract::jsonrpc::serde::Deserialize; +use ethcontract::web3::types::BlockNumber; +use std::fmt::Display; + +/// A helper to parse RPC call arguments. +/// +/// RPC call arguments are parsed from JSON string into an array +/// of `Value`s before they're passed to method handlers. +/// This struct helps to transform `Value`s into actual rust types, +/// while also handling optional arguments. +pub struct Parser { + name: &'static str, + args: Vec, + current: usize, +} + +impl Parser { + /// Create new parser. + pub fn new(name: &'static str, args: Vec) -> Self { + Parser { + name, + args, + current: 0, + } + } + + /// Parse an argument. + pub fn arg Deserialize<'b>>(&mut self) -> T { + if let Some(arg) = self.args.get_mut(self.current) { + self.current += 1; + let val = from_value(std::mem::take(arg)); + self.res(val) + } else { + panic!("not enough arguments for rpc call {:?}", self.name); + } + } + + /// Parse an optional argument, return `None` if arguments list is exhausted. + pub fn arg_opt Deserialize<'b>>(&mut self) -> Option { + if self.current < self.args.len() { + Some(self.arg()) + } else { + None + } + } + + /// Parse an optional argument with a block number. + /// + /// Since [`BlockNumber`] does not implement [`Deserialize`], + /// we can't use [`arg`] to parse it, so we use this helper method. + pub fn block_number_opt(&mut self) -> Option { + let value = self.arg_opt(); + value.map(|value| self.parse_block_number(value)) + } + + /// Finish parsing arguments. + /// + /// If there are unparsed arguments, report them as extraneous. + pub fn done(self) { + // nothing here, actual check is in the `drop` method. + } + + // Helper for parsing block numbers. + fn parse_block_number(&self, value: Value) -> BlockNumber { + match value.as_str() { + Some("latest") => BlockNumber::Latest, + Some("earliest") => BlockNumber::Earliest, + Some("pending") => BlockNumber::Pending, + Some(number) => BlockNumber::Number(self.res(number.parse())), + None => self.err("block number should be a string"), + } + } + + // Unwraps `Result`, adds info with current arg index and rpc name. + fn res(&self, res: Result) -> T { + res.unwrap_or_else(|err| self.err(err)) + } + + // Panics, adds info with current arg index and rpc name. + fn err(&self, err: E) -> ! { + panic!( + "argument {} for rpc call {:?} is invalid: {}", + self.current, self.name, err + ) + } +} + +impl Drop for Parser { + fn drop(&mut self) { + if !std::thread::panicking() && self.current < self.args.len() { + panic!("too many arguments for rpc call {:?}", self.name); + } + } +} From 05c7bee434bdb97ef049b546f981a3f3aec6c37e Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:06:41 +0200 Subject: [PATCH 12/21] Implement skeleton data structures for mock transport --- ethcontract-mock/src/details/mod.rs | 213 ++++++++++++++++++++++++++++ ethcontract-mock/src/lib.rs | 25 +++- 2 files changed, 231 insertions(+), 7 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index a0015f82..a470504b 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1,6 +1,219 @@ //! Implementation details of mock node. +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use ethcontract::common::abi::{Function, Token}; +use ethcontract::common::hash::H32; +use ethcontract::common::{Abi, FunctionExt}; +use ethcontract::tokens::Tokenize; +use ethcontract::web3::types::TransactionReceipt; +use ethcontract::web3::RequestId; +use ethcontract::{Address, H160, H256}; + +use crate::range::TimesRange; +use crate::CallContext; +use std::any::Any; + mod default; mod parse; mod sign; mod transaction; + +/// Mock transport. +#[derive(Clone)] +pub struct MockTransport { + /// Mutable state. + state: Arc>, +} + +/// Internal transport state, protected by a mutex. +struct MockTransportState { + /// Chain ID. + chain_id: u64, + + /// Current gas price. + gas_price: u64, + + /// This counter is used to keep track of prepared calls. + request_id: RequestId, + + /// This counter is used to keep track of mined blocks. + block: u64, + + /// This counter is used to generate contract addresses. + address: u64, + + /// Nonce for account. + nonce: HashMap, + + /// Deployed mocked contracts. + contracts: HashMap, + + /// Receipts for already performed transactions. + receipts: HashMap, +} + +impl MockTransport { + /// Creates a new transport. + pub fn new(chain_id: u64) -> Self { + MockTransport { + state: Arc::new(Mutex::new(MockTransportState { + chain_id, + gas_price: 1, + request_id: 0, + block: 0, + address: 0, + nonce: HashMap::new(), + contracts: HashMap::new(), + receipts: HashMap::new(), + })), + } + } + + /// Deploys a new contract with the given ABI. + pub fn deploy(&self, abi: &Abi) -> Address { + let mut state = self.state.lock().unwrap(); + + state.address += 1; + let address = H160::from_low_u64_be(state.address); + + state.contracts.insert(address, Contract::new(address, abi)); + + address + } + + pub fn update_gas_price(&self, gas_price: u64) { + let mut state = self.state.lock().unwrap(); + state.gas_price = gas_price; + } +} + +impl std::fmt::Debug for MockTransport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("MockTransport") + } +} + +/// A mocked contract instance. +struct Contract { + address: Address, + methods: HashMap, +} + +impl Contract { + fn new(address: Address, abi: &Abi) -> Self { + let mut methods = HashMap::new(); + + for functions in abi.functions.values() { + for function in functions { + methods.insert(function.selector(), Method::new(address, function.clone())); + } + } + + Contract { address, methods } + } +} + +struct Method { + /// Description for this method. + description: String, + + /// ABI of this method. + function: Function, + + /// Incremented whenever `expectations` vector is cleared to invalidate + /// expectations API handle. + generation: usize, + + /// Expectation for this method. + expectations: Vec>, +} + +impl Method { + /// Creates new method. + fn new(address: Address, function: Function) -> Self { + let description = format!("{:?} on contract {:#x}", function.abi_signature(), address); + + Method { + description, + function, + generation: 0, + expectations: Vec::new(), + } + } +} + +trait ExpectationApi: Send { + /// Convert this expectation to `Any` for downcast. + fn as_any(&mut self) -> &mut dyn Any; +} + +struct Expectation { + /// How many times should this expectation be called. + times: TimesRange, + + /// How many times was it actually called. + used: usize, + + /// Indicates that predicate for this expectation has been called at least + /// once. Expectations shouldn't be changed after that happened. + checked: bool, + + /// How many blocks should node skip for confirmation to be successful. + confirmations: u64, + + /// Only consider this expectation if predicate returns `true`. + predicate: Predicate

, + + /// Should this expectation match view calls? + allow_calls: bool, + + /// Should this expectation match transactions? + allow_transactions: bool, + + /// Function to generate method's return value. + returns: Returns, + + /// Handle for when this expectation belongs to a sequence. + sequence: Option, +} + +impl Expectation { + fn new() -> Self { + Expectation { + times: TimesRange::default(), + used: 0, + checked: false, + confirmations: 0, + predicate: Predicate::None, + allow_calls: true, + allow_transactions: true, + returns: Returns::Default, + sequence: None, + } + } +} + +impl ExpectationApi + for Expectation +{ + fn as_any(&mut self) -> &mut dyn Any { + self + } +} + +enum Predicate { + None, + Predicate(Box + Send>), + Function(Box bool + Send>), + TxFunction(Box bool + Send>), +} + +enum Returns { + Default, + Error(String), + Const(Token), + Function(Box Result + Send>), + TxFunction(Box Result + Send>), +} diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 0a72a212..49e6b663 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -22,12 +22,15 @@ mod range; /// Mock ethereum node. #[derive(Clone)] pub struct Mock { + transport: details::MockTransport, } impl Mock { /// Creates a new mock chain. pub fn new(chain_id: u64) -> Self { - todo!() + Mock { + transport: details::MockTransport::new(chain_id), + } } /// Creates a `Web3` object that can be used to interact with @@ -45,7 +48,12 @@ impl Mock { /// Deploys a new mocked contract and returns an object that allows /// configuring expectations for contract methods. pub fn deploy(&self, abi: Abi) -> Contract { - todo!() + let address = self.transport.deploy(&abi); + Contract { + transport: self.transport.clone(), + address, + abi, + } } /// Updates gas price that is returned by RPC call `eth_gasPrice`. @@ -53,7 +61,7 @@ impl Mock { /// Mock node does not simulate gas consumption, so this value does not /// affect anything if you don't call `eth_gasPrice`. pub fn update_gas_price(&self, gas_price: u64) { - todo!() + self.transport.update_gas_price(gas_price); } /// Verifies that all expectations on all contracts have been met, @@ -74,6 +82,9 @@ impl std::fmt::Debug for Mock { /// This struct allows setting up expectations on which contract methods /// will be called, with what arguments, in what order, etc. pub struct Contract { + transport: details::MockTransport, + address: Address, + abi: Abi, } impl Contract { @@ -92,23 +103,23 @@ impl Contract { /// Creates a contract `Instance` that can be used to interact with /// this contract. pub fn instance(&self) -> DynInstance { - todo!() + DynInstance::at(self.web3(), self.abi.clone(), self.address) } /// Consumes this object and transforms it into a contract `Instance` /// that can be used to interact with this contract. pub fn into_instance(self) -> DynInstance { - todo!() + DynInstance::at(self.web3(), self.abi, self.address) } /// Returns a reference to the contract's ABI. pub fn abi(&self) -> &Abi { - todo!() + &self.abi } /// Returns contract's address. pub fn address(&self) -> Address { - todo!() + self.address } /// Adds a new expectation for contract method. See [`Expectation`]. From 957b6b6473c23ab4651e24488f1b1189aeb603e9 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:10:16 +0200 Subject: [PATCH 13/21] Implement simple RPC calls --- ethcontract-mock/src/details/mod.rs | 112 +++++++++++++++++++++++++++- ethcontract-mock/src/lib.rs | 8 +- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index a470504b..b084e2ee 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1,15 +1,20 @@ //! Implementation details of mock node. use std::collections::HashMap; +use std::future::ready; use std::sync::{Arc, Mutex}; use ethcontract::common::abi::{Function, Token}; use ethcontract::common::hash::H32; use ethcontract::common::{Abi, FunctionExt}; +use ethcontract::jsonrpc::serde::Serialize; +use ethcontract::jsonrpc::serde_json::to_value; +use ethcontract::jsonrpc::{Call, MethodCall, Params, Value}; use ethcontract::tokens::Tokenize; -use ethcontract::web3::types::TransactionReceipt; -use ethcontract::web3::RequestId; -use ethcontract::{Address, H160, H256}; +use ethcontract::web3::types::{TransactionReceipt, U256, U64}; +use ethcontract::web3::{helpers, Error, RequestId, Transport}; +use ethcontract::{Address, BlockNumber, H160, H256}; +use parse::Parser; use crate::range::TimesRange; use crate::CallContext; @@ -89,6 +94,107 @@ impl MockTransport { } } +impl Transport for MockTransport { + type Out = std::future::Ready>; + + /// Prepares an RPC call for given method with parameters. + /// + /// We don't have to deal with network issues, so we are relaxed about + /// request IDs, idempotency checks and so on. + fn prepare(&self, method: &str, params: Vec) -> (RequestId, Call) { + let mut state = self.state.lock().unwrap(); + + let id = state.request_id; + state.request_id += 1; + + let request = helpers::build_request(id, method, params); + + (id, request) + } + + /// Executes a prepared RPC call. + fn send(&self, _: RequestId, request: Call) -> Self::Out { + let MethodCall { method, params, .. } = match request { + Call::MethodCall(method_call) => method_call, + Call::Notification(_) => panic!("rpc notifications are not supported"), + _ => panic!("unknown or invalid rpc call type"), + }; + + let params = match params { + Params::None => Vec::new(), + Params::Array(array) => array, + Params::Map(_) => panic!("passing arguments by map is not supported"), + }; + + let result = match method.as_str() { + "eth_blockNumber" => { + let name = "eth_blockNumber"; + self.block_number(Parser::new(name, params)) + } + "eth_chainId" => { + let name = "eth_chainId"; + self.chain_id(Parser::new(name, params)) + } + "eth_getTransactionCount" => { + let name = "eth_getTransactionCount"; + self.transaction_count(Parser::new(name, params)) + } + "eth_gasPrice" => { + let name = "eth_gasPrice"; + self.gas_price(Parser::new(name, params)) + } + unsupported => panic!("mock node does not support rpc method {:?}", unsupported), + }; + + ready(result) + } +} + +impl MockTransport { + fn block_number(&self, args: Parser) -> Result { + args.done(); + + let state = self.state.lock().unwrap(); + Self::ok(&U64::from(state.block)) + } + + fn chain_id(&self, args: Parser) -> Result { + args.done(); + + let state = self.state.lock().unwrap(); + Self::ok(&U256::from(state.chain_id)) + } + + fn transaction_count(&self, mut args: Parser) -> Result { + let address: Address = args.arg(); + let block: Option = args.block_number_opt(); + args.done(); + + let block = block.unwrap_or(BlockNumber::Pending); + let state = self.state.lock().unwrap(); + let transaction_count = match block { + BlockNumber::Earliest => 0, + BlockNumber::Number(n) if n == 0.into() => 0, + BlockNumber::Number(n) if n != state.block.into() => { + panic!("mock node does not support returning transaction count for specific block number"); + } + _ => state.nonce.get(&address).copied().unwrap_or(0), + }; + Self::ok(&U256::from(transaction_count)) + } + + fn gas_price(&self, args: Parser) -> Result { + args.done(); + + let state = self.state.lock().unwrap(); + Self::ok(&U256::from(state.gas_price)) + } + + fn ok(t: T) -> Result { + Ok(to_value(t).unwrap()) + } +} + impl std::fmt::Debug for MockTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("MockTransport") diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 49e6b663..05d7091b 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -36,13 +36,13 @@ impl Mock { /// Creates a `Web3` object that can be used to interact with /// the mocked chain. pub fn web3(&self) -> DynWeb3 { - todo!() + DynWeb3::new(self.transport()) } /// Creates a `Transport` object that can be used to interact with /// the mocked chain. pub fn transport(&self) -> DynTransport { - todo!() + DynTransport::new(self.transport.clone()) } /// Deploys a new mocked contract and returns an object that allows @@ -91,13 +91,13 @@ impl Contract { /// Creates a `Web3` object that can be used to interact with /// the mocked chain on which this contract is deployed. pub fn web3(&self) -> DynWeb3 { - todo!() + DynWeb3::new(self.transport()) } /// Creates a `Transport` object that can be used to interact with /// the mocked chain. pub fn transport(&self) -> DynTransport { - todo!() + DynTransport::new(self.transport.clone()) } /// Creates a contract `Instance` that can be used to interact with From 2fcf77ae4927c8ad3316e46e1a24c792ab762e92 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:34:50 +0200 Subject: [PATCH 14/21] Implement method to add new expectations --- ethcontract-mock/src/details/mod.rs | 54 +++++++++++++++++++++++++++-- ethcontract-mock/src/lib.rs | 30 +++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index b084e2ee..4a102804 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -1,21 +1,26 @@ //! Implementation details of mock node. use std::collections::HashMap; +use std::convert::TryFrom; use std::future::ready; use std::sync::{Arc, Mutex}; -use ethcontract::common::abi::{Function, Token}; +use ethcontract::common::abi::{Function, StateMutability, Token}; use ethcontract::common::hash::H32; use ethcontract::common::{Abi, FunctionExt}; use ethcontract::jsonrpc::serde::Serialize; use ethcontract::jsonrpc::serde_json::to_value; use ethcontract::jsonrpc::{Call, MethodCall, Params, Value}; use ethcontract::tokens::Tokenize; -use ethcontract::web3::types::{TransactionReceipt, U256, U64}; +use ethcontract::web3::types::{ + Bytes, CallRequest, TransactionReceipt, TransactionRequest, U256, U64, +}; use ethcontract::web3::{helpers, Error, RequestId, Transport}; use ethcontract::{Address, BlockNumber, H160, H256}; use parse::Parser; +use sign::verify; +use crate::details::transaction::TransactionResult; use crate::range::TimesRange; use crate::CallContext; use std::any::Any; @@ -92,6 +97,31 @@ impl MockTransport { let mut state = self.state.lock().unwrap(); state.gas_price = gas_price; } + + pub fn expect( + &self, + address: Address, + signature: H32, + ) -> (usize, usize) { + let mut state = self.state.lock().unwrap(); + let method = state.method(address, signature); + method.expect::() + } +} + +impl MockTransportState { + /// Returns contract at the given address, panics if contract does not exist. + fn contract(&mut self, address: Address) -> &mut Contract { + match self.contracts.get_mut(&address) { + Some(contract) => contract, + None => panic!("there is no mocked contract with address {:#x}", address), + } + } + + /// Returns contract's method. + fn method(&mut self, address: Address, signature: H32) -> &mut Method { + self.contract(address).method(signature) + } } impl Transport for MockTransport { @@ -219,6 +249,17 @@ impl Contract { Contract { address, methods } } + + fn method(&mut self, signature: H32) -> &mut Method { + match self.methods.get_mut(&signature) { + Some(method) => method, + None => panic!( + "contract {:#x} doesn't have method with signature 0x{}", + self.address, + hex::encode(signature) + ), + } + } } struct Method { @@ -248,6 +289,15 @@ impl Method { expectations: Vec::new(), } } + + /// Adds new expectation. + fn expect( + &mut self, + ) -> (usize, usize) { + let index = self.expectations.len(); + self.expectations.push(Box::new(Expectation::::new())); + (index, self.generation) + } } trait ExpectationApi: Send { diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 05d7091b..a6cbe17f 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -123,11 +123,34 @@ impl Contract { } /// Adds a new expectation for contract method. See [`Expectation`]. + /// + /// Generic parameters are used to specify which rust types should be used + /// to encode and decode method's arguments and return value. If you're + /// using generated contracts, they will be inferred automatically. + /// If not, you may have to specify them manually. + /// + /// # Notes + /// + /// Expectations generated by this method will allow both view calls + /// and transactions. This is usually undesired, so prefer using + /// [`expect_call`] and [`expect_transaction`] instead. + /// + /// [`expect_call`]: Contract::expect_call + /// [`expect_transaction`]: Contract::expect_transaction pub fn expect( &self, signature: impl Into>, ) -> Expectation { - todo!() + let signature = signature.into().into_inner(); + let (index, generation) = self.transport.expect::(self.address, signature); + Expectation { + transport: self.transport.clone(), + address: self.address, + signature, + index, + generation, + _ph: PhantomData, + } } /// Adds a new expectation for contract method that only matches view calls. @@ -165,6 +188,11 @@ impl Contract { /// Expectation for contract method. pub struct Expectation { + transport: details::MockTransport, + address: Address, + signature: H32, + index: usize, + generation: usize, _ph: PhantomData<(P, R)>, } From ad631e4f96bc13fbdf716fd9bcf5da1ccdecedf4 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:37:11 +0200 Subject: [PATCH 15/21] Implement transaction processing --- ethcontract-mock/src/details/mod.rs | 337 +++++++++++++++++++++++++++- 1 file changed, 336 insertions(+), 1 deletion(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index 4a102804..9fd8d64b 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -173,6 +173,26 @@ impl Transport for MockTransport { let name = "eth_gasPrice"; self.gas_price(Parser::new(name, params)) } + "eth_estimateGas" => { + let name = "eth_estimateGas"; + self.estimate_gas(Parser::new(name, params)) + } + "eth_call" => { + let name = "eth_call"; + self.call(Parser::new(name, params)) + } + "eth_sendTransaction" => { + let name = "eth_sendTransaction"; + self.send_transaction(Parser::new(name, params)) + } + "eth_sendRawTransaction" => { + let name = "eth_sendRawTransaction"; + self.send_raw_transaction(Parser::new(name, params)) + } + "eth_getTransactionReceipt" => { + let name = "eth_getTransactionReceipt"; + self.get_transaction_receipt(Parser::new(name, params)) + } unsupported => panic!("mock node does not support rpc method {:?}", unsupported), }; @@ -220,6 +240,187 @@ impl MockTransport { Self::ok(&U256::from(state.gas_price)) } + fn estimate_gas(&self, mut args: Parser) -> Result { + let request: CallRequest = args.arg(); + let block: Option = args.block_number_opt(); + args.done(); + + let state = self.state.lock().unwrap(); + + let block = block.unwrap_or(BlockNumber::Pending); + match block { + BlockNumber::Earliest => { + panic!("mock node does not support executing methods on earliest block"); + } + BlockNumber::Number(n) if n != state.block.into() => { + panic!("mock node does not support executing methods on non-last block"); + } + _ => (), + } + + match request.to { + None => panic!("call's 'to' field is empty"), + Some(to) => to, + }; + + // TODO: + // + // We could look up contract's method, match an expectation, + // and see if the expectation defines gas price. + // + // So, for example, this code: + // + // ``` + // contract + // .expect_method(signature) + // .with(matcher) + // .gas(100); + // ``` + // + // Indicates that call to the method with the given signature + // requires 100 gas. + // + // When estimating gas, we'll check all expectation as if we're + // executing a method, but we won't mark any expectation as fulfilled. + + Self::ok(&U256::from(1)) + } + + fn call(&self, mut args: Parser) -> Result { + let request: CallRequest = args.arg(); + let block: Option = args.block_number_opt(); + + let mut state = self.state.lock().unwrap(); + + let block = block.unwrap_or(BlockNumber::Pending); + match block { + BlockNumber::Earliest => { + panic!("mock node does not support executing methods on earliest block"); + } + BlockNumber::Number(n) if n != state.block.into() => { + panic!("mock node does not support executing methods on non-last block"); + } + _ => (), + } + + let from = request.from.unwrap_or_default(); + let to = match request.to { + None => panic!("call's 'to' field is empty"), + Some(to) => to, + }; + + let nonce = state.nonce.get(&from).copied().unwrap_or(0); + + let gas_price = state.gas_price; + + let contract = state.contract(to); + + let context = CallContext { + is_view_call: true, + from: request.from.unwrap_or_default(), + to, + nonce: U256::from(nonce), + gas: request.gas.unwrap_or_else(|| U256::from(1)), + gas_price: request.gas.unwrap_or_else(|| U256::from(gas_price)), + value: request.value.unwrap_or_default(), + }; + + let data = request.data.unwrap_or_default(); + + let result = contract.process_tx(context, &data.0); + + match result.result { + Ok(data) => Self::ok(Bytes(data)), + Err(err) => Err(Error::Rpc(ethcontract::jsonrpc::Error { + code: ethcontract::jsonrpc::ErrorCode::ServerError(0), + message: format!("execution reverted: {}", err), + data: None, + })), + } + } + + fn send_transaction(&self, mut args: Parser) -> Result { + let _request: TransactionRequest = args.arg(); + args.done(); + + // TODO: + // + // We could support signing if user adds accounts with their private + // keys during mock setup. + + panic!("mock node can't sign transactions, use offline signing with private key"); + } + + fn send_raw_transaction(&self, mut args: Parser) -> Result { + let raw_tx: Bytes = args.arg(); + args.done(); + + let mut state = self.state.lock().unwrap(); + + let tx = verify(&raw_tx.0, state.chain_id); + + let nonce = state.nonce.entry(tx.from).or_insert(0); + if *nonce != tx.nonce.as_u64() { + panic!( + "nonce mismatch for account {:#x}: expected {}, actual {}", + tx.from, + tx.nonce.as_u64(), + nonce + ); + } + *nonce += 1; + + let contract = state.contract(tx.to); + + let context = CallContext { + is_view_call: false, + from: tx.from, + to: tx.to, + nonce: tx.nonce, + gas: tx.gas, + gas_price: tx.gas_price, + value: tx.value, + }; + + let result = contract.process_tx(context, &tx.data); + + state.block += 1; + + let receipt = TransactionReceipt { + transaction_hash: tx.hash, + transaction_index: U64::from(0), + block_hash: None, + block_number: Some(U64::from(state.block)), + from: tx.from, + to: Some(tx.to), + cumulative_gas_used: U256::from(1), + gas_used: None, + contract_address: None, + logs: vec![], + status: Some(U64::from(result.result.is_ok() as u64)), + root: None, + logs_bloom: Default::default(), + transaction_type: None, + }; + + state.receipts.insert(tx.hash, receipt); + + state.block += result.confirmations; + + Self::ok(tx.hash) + } + + fn get_transaction_receipt(&self, mut args: Parser) -> Result { + let transaction: H256 = args.arg(); + args.done(); + + let state = self.state.lock().unwrap(); + + Self::ok(state.receipts.get(&transaction).unwrap_or_else(|| { + panic!("there is no transaction with hash {:#x}", transaction); + })) + } + fn ok(t: T) -> Result { Ok(to_value(t).unwrap()) } @@ -260,6 +461,21 @@ impl Contract { ), } } + + fn process_tx(&mut self, tx: CallContext, data: &[u8]) -> TransactionResult { + // TODO: + // + // We could support receive/fallback functions if data is empty. + + if data.len() < 4 { + panic!("transaction has invalid call data"); + } + + let signature = H32::try_from(&data[0..4]).unwrap(); + let method = self.method(signature); + + method.process_tx(tx, data) + } } struct Method { @@ -298,11 +514,57 @@ impl Method { self.expectations.push(Box::new(Expectation::::new())); (index, self.generation) } + + /// Executes a transaction or a call. + fn process_tx(&mut self, tx: CallContext, data: &[u8]) -> TransactionResult { + if !tx.value.is_zero() && self.function.state_mutability != StateMutability::Payable { + panic!( + "call to non-payable {} with non-zero value {}", + self.description, tx.value, + ) + } + + let params = self + .function + .decode_input(&data[4..]) + .unwrap_or_else(|e| panic!("unable to decode input for {}: {:?}", self.description, e)); + + for expectation in self.expectations.iter_mut() { + if expectation.is_active() { + // We clone `params` for each expectation, which could potentially + // be inefficient. We assume, however, that in most cases there + // are only a few expectations for a method, and they are likely + // to be filtered out by `is_active`. + if let Some(result) = + expectation.process_tx(&tx, &self.description, &self.function, params.clone()) + { + return result; + } + } + } + + panic!("unexpected call to {}", self.description) + } } trait ExpectationApi: Send { /// Convert this expectation to `Any` for downcast. fn as_any(&mut self) -> &mut dyn Any; + + /// Checks if this expectation is active, i.e., still can be called. + fn is_active(&self) -> bool; + + /// Matches and processes this transaction. + /// + /// If transaction matches this expectation, processes it and returns + /// its result. Otherwise, returns `None`. + fn process_tx( + &mut self, + tx: &CallContext, + description: &str, + function: &Function, + params: Vec, + ) -> Option; } struct Expectation { @@ -352,11 +614,59 @@ impl Expectation ExpectationApi - for Expectation +for Expectation { fn as_any(&mut self) -> &mut dyn Any { self } + + fn is_active(&self) -> bool { + self.times.can_call(self.used) + } + + fn process_tx( + &mut self, + tx: &CallContext, + description: &str, + function: &Function, + params: Vec, + ) -> Option { + self.checked = true; + + if tx.is_view_call && !self.allow_calls || !tx.is_view_call && !self.allow_transactions { + return None; + } + + if !self.times.can_call(self.used) { + return None; + } + + let param = P::from_token(Token::Tuple(params)) + .unwrap_or_else(|e| panic!("unable to decode input for {}: {:?}", description, e)); + + if !self.predicate.can_call(tx, ¶m) { + return None; + } + + self.used += 1; + if let Some(sequence) = &self.sequence { + sequence.verify(description); + + if self.used == self.times.lower_bound() { + sequence.satisfy(); + } + } + + let result = self + .returns + .process_tx(function, tx, param) + .map(|result| ethcontract::common::abi::encode(&[result])); + + Some(TransactionResult { + result, + confirmations: self.confirmations, + }) + } } enum Predicate { @@ -366,6 +676,17 @@ enum Predicate { TxFunction(Box bool + Send>), } +impl Predicate

{ + fn can_call(&self, tx: &CallContext, param: &P) -> bool { + match self { + Predicate::None => true, + Predicate::Predicate(p) => p.eval(param), + Predicate::Function(f) => f(param), + Predicate::TxFunction(f) => f(tx, param), + } + } +} + enum Returns { Default, Error(String), @@ -373,3 +694,17 @@ enum Returns { Function(Box Result + Send>), TxFunction(Box Result + Send>), } + +impl Returns { + fn process_tx(&self, function: &Function, tx: &CallContext, param: P) -> Result { + match self { + Returns::Default => Ok(default::default_tuple( + function.inputs.iter().map(|i| &i.kind), + )), + Returns::Error(error) => Err(error.clone()), + Returns::Const(token) => Ok(token.clone()), + Returns::Function(f) => f(param).map(Tokenize::into_token), + Returns::TxFunction(f) => f(tx, param).map(Tokenize::into_token), + } + } +} From 3d53aa2033a7ca9db7cc8516c5d9d1da00a8decf Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:39:57 +0200 Subject: [PATCH 16/21] Implement methods for configuring expectations --- ethcontract-mock/src/details/mod.rs | 233 ++++++++++++++++++++++++++++ ethcontract-mock/src/lib.rs | 122 +++++++++++++-- 2 files changed, 339 insertions(+), 16 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index 9fd8d64b..a9e32782 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -107,6 +107,198 @@ impl MockTransport { let method = state.method(address, signature); method.expect::() } + + pub fn times( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + times: TimesRange, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + + if expectation.sequence.is_some() && !times.is_exact() { + panic!("only expectations with an exact call count can be in a sequences") + } + if expectation.sequence.is_some() && times.lower_bound() == 0 { + panic!("expectation in a sequences should be called at least once") + } + + expectation.times = times; + } + + pub fn in_sequence( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + sequence: &mut mockall::Sequence, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + + if !expectation.times.is_exact() { + panic!("only expectations with an exact call count can be in a sequences") + } + if expectation.times.lower_bound() == 0 { + panic!("expectation in a sequences should be called at least once") + } + if expectation.sequence.is_some() { + panic!("expectation can't be in multiple sequences") + } + + expectation.sequence = Some(sequence.next_handle()); + } + + pub fn confirmations( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + confirmations: u64, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.confirmations = confirmations; + } + + pub fn predicate( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + pred: Box + Send>, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.predicate = Predicate::Predicate(pred); + } + + pub fn predicate_fn( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + pred: Box bool + Send>, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.predicate = Predicate::Function(pred); + } + + pub fn predicate_fn_ctx( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + pred: Box bool + Send>, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.predicate = Predicate::TxFunction(pred); + } + + pub fn allow_calls( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + allow_calls: bool, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.allow_calls = allow_calls; + } + + pub fn allow_transactions( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + allow_transactions: bool, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.allow_transactions = allow_transactions; + } + + pub fn returns( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + returns: R, + ) { + // Convert `R` into `Token` here because `Token` is `Clone` while `R` is not. + // We need to clone result const if method is called multiple times. + let token = returns.into_token(); + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.returns = Returns::Const(token); + } + + pub fn returns_fn( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + returns: Box Result + Send>, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.returns = Returns::Function(returns); + } + + pub fn returns_fn_ctx( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + returns: Box Result + Send>, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.returns = Returns::TxFunction(returns); + } + + pub fn returns_error( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + error: String, + ) { + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.returns = Returns::Error(error); + } + + pub fn returns_default( + &self, + address: Address, + signature: H32, + index: usize, + generation: usize, + ) { + // Convert `R` into `Token` here because `Token` is `Clone` while `R` is not. + // We need to clone result const if method is called multiple times. + let mut state = self.state.lock().unwrap(); + let expectation = state.expectation::(address, signature, index, generation); + expectation.returns = Returns::Default; + } } impl MockTransportState { @@ -122,6 +314,19 @@ impl MockTransportState { fn method(&mut self, address: Address, signature: H32) -> &mut Method { self.contract(address).method(signature) } + + /// Returns contract's expectation. + fn expectation( + &mut self, + address: Address, + signature: H32, + index: usize, + generation: usize, + ) -> &mut Expectation { + self.contract(address) + .method(signature) + .expectation(index, generation) + } } impl Transport for MockTransport { @@ -515,6 +720,34 @@ impl Method { (index, self.generation) } + /// Returns an expectation. + fn expectation( + &mut self, + index: usize, + generation: usize, + ) -> &mut Expectation { + if generation != self.generation { + panic!("old expectations are not valid after checkpoint"); + } + + let expectation: &mut Expectation = self + .expectations + .get_mut(index) + .unwrap() + .as_any() + .downcast_mut() + .unwrap(); + + if expectation.checked { + panic!( + "can't modify expectation for {} because it was already in use", + self.description + ) + } + + expectation + } + /// Executes a transaction or a call. fn process_tx(&mut self, tx: CallContext, data: &[u8]) -> TransactionResult { if !tx.value.is_zero() && self.function.state_mutability != StateMutability::Payable { diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index a6cbe17f..279f90f1 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -199,7 +199,14 @@ pub struct Expectation Expectation { /// Specifies how many times this expectation can be called. pub fn times(self, times: impl Into) -> Self { - todo!() + self.transport.times::( + self.address, + self.signature, + self.index, + self.generation, + times.into(), + ); + self } /// Indicates that this expectation can be called exactly zero times. @@ -222,23 +229,44 @@ impl Expectation Self { - todo!() + self.transport.in_sequence::( + self.address, + self.signature, + self.index, + self.generation, + sequence, + ); + self } /// Sets number of blocks that should be mined on top of the transaction /// block. This method can be useful when there are custom transaction /// confirmation settings. pub fn confirmations(self, confirmations: u64) -> Self { - todo!() + self.transport.confirmations::( + self.address, + self.signature, + self.index, + self.generation, + confirmations, + ); + self } /// Sets predicate for this expectation. pub fn predicate(self, pred: T) -> Self - where - T: TuplePredicate

+ Send + 'static, - >::P: Send, + where + T: TuplePredicate

+ Send + 'static, + >::P: Send, { - todo!() + self.transport.predicate::( + self.address, + self.signature, + self.index, + self.generation, + Box::new(pred.into_predicate()), + ); + self } /// Sets predicate function for this expectation. This function accepts @@ -249,7 +277,14 @@ impl Expectation bool + Send + 'static) -> Self { - todo!() + self.transport.predicate_fn::( + self.address, + self.signature, + self.index, + self.generation, + Box::new(pred), + ); + self } /// Sets predicate function for this expectation. This function accepts @@ -264,7 +299,14 @@ impl Expectation bool + Send + 'static, ) -> Self { - todo!() + self.transport.predicate_fn_ctx::( + self.address, + self.signature, + self.index, + self.generation, + Box::new(pred), + ); + self } /// Indicates that this expectation only applies to view calls. @@ -276,7 +318,14 @@ impl Expectation Self { - todo!() + self.transport.allow_calls::( + self.address, + self.signature, + self.index, + self.generation, + allow_calls, + ); + self } /// Indicates that this expectation only applies to transactions. @@ -288,7 +337,14 @@ impl Expectation Self { - todo!() + self.transport.allow_transactions::( + self.address, + self.signature, + self.index, + self.generation, + allow_transactions, + ); + self } /// Sets return value of the method. @@ -300,7 +356,14 @@ impl Expectation Self { - todo!() + self.transport.returns::( + self.address, + self.signature, + self.index, + self.generation, + returns, + ); + self } /// Sets callback function that will be used to calculate return value @@ -308,7 +371,14 @@ impl Expectation Result + Send + 'static) -> Self { - todo!() + self.transport.returns_fn::( + self.address, + self.signature, + self.index, + self.generation, + Box::new(returns), + ); + self } /// Sets callback function that will be used to calculate return value @@ -319,19 +389,39 @@ impl Expectation Result + Send + 'static, ) -> Self { - todo!() + self.transport.returns_fn_ctx::( + self.address, + self.signature, + self.index, + self.generation, + Box::new(returns), + ); + self } /// Sets return value of the method to an error, meaning that calls to this /// expectation result in reverted transaction. pub fn returns_error(self, error: String) -> Self { - todo!() + self.transport.returns_error::( + self.address, + self.signature, + self.index, + self.generation, + error, + ); + self } /// Sets return value of the method to a default value for its solidity type. /// See [`returns`] for more info. pub fn returns_default(self) -> Self { - todo!() + self.transport.returns_default::( + self.address, + self.signature, + self.index, + self.generation, + ); + self } } From ccc47fae9db93246d68b0a85b71545c5929d152e Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:41:03 +0200 Subject: [PATCH 17/21] Implement expectations verification and checkpoints --- ethcontract-mock/src/details/mod.rs | 60 +++++++++++++++++++++++++++++ ethcontract-mock/src/lib.rs | 4 +- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index a9e32782..87db3baa 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -98,6 +98,13 @@ impl MockTransport { state.gas_price = gas_price; } + pub fn checkpoint(&self) { + let mut state = self.state.lock().unwrap(); + for contract in state.contracts.values_mut() { + contract.checkpoint(); + } + } + pub fn expect( &self, address: Address, @@ -108,6 +115,12 @@ impl MockTransport { method.expect::() } + pub fn contract_checkpoint(&self, address: Address) { + let mut state = self.state.lock().unwrap(); + let contract = state.contract(address); + contract.checkpoint(); + } + pub fn times( &self, address: Address, @@ -681,6 +694,20 @@ impl Contract { method.process_tx(tx, data) } + + fn checkpoint(&mut self) { + for method in self.methods.values_mut() { + method.checkpoint(); + } + } +} + +impl Drop for Contract { + fn drop(&mut self) { + if !std::thread::panicking() { + self.checkpoint(); + } + } } struct Method { @@ -778,6 +805,14 @@ impl Method { panic!("unexpected call to {}", self.description) } + + fn checkpoint(&mut self) { + for expectation in self.expectations.iter_mut() { + expectation.verify(&self.description); + } + self.generation += 1; + self.expectations.clear(); + } } trait ExpectationApi: Send { @@ -798,6 +833,9 @@ trait ExpectationApi: Send { function: &Function, params: Vec, ) -> Option; + + /// Verifies that this expectation is satisfied. + fn verify(&self, description: &str); } struct Expectation { @@ -900,6 +938,28 @@ for Expectation confirmations: self.confirmations, }) } + + fn verify(&self, description: &str) { + if !self.times.contains(self.used) { + panic!( + "{} was called {} {}, but it was expected to be called {} {} {}", + description, + self.used, + if self.used == 1 { "time" } else { "times" }, + if self.times.is_exact() { + "exactly" + } else { + "at least" + }, + self.times.lower_bound(), + if self.times.lower_bound() == 1 { + "time" + } else { + "times" + } + ) + } + } } enum Predicate { diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 279f90f1..6e07baea 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -67,7 +67,7 @@ impl Mock { /// Verifies that all expectations on all contracts have been met, /// then clears all expectations. pub fn checkpoint(&self) { - todo!() + self.transport.checkpoint(); } } @@ -182,7 +182,7 @@ impl Contract { /// Verifies that all expectations on this contract have been met, /// then clears all expectations. pub fn checkpoint(&self) { - todo!() + self.transport.contract_checkpoint(self.address); } } From 15226f6f70a53d83c889bbd925bb196a6737df29 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:41:37 +0200 Subject: [PATCH 18/21] Add documentation --- ethcontract-mock/src/lib.rs | 490 ++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 6e07baea..4cd3a7e6 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -2,6 +2,220 @@ //! This crate allows emulating ethereum node with a limited number //! of supported RPC calls, enabling you to mock ethereum contracts. +//! +//! Create a new deployment using the [`Mock::deploy`] function. +//! +//! Configure contract's behaviour using [`Contract::expect_transaction`] +//! and [`Contract::expect_call`]. +//! +//! Finally, create an ethcontract's [`Instance`] by calling [`Contract::instance`], +//! then use said instance in your tests. +//! +//! # Example +//! +//! Let's mock [voting contract] from solidity examples. +//! +//! First, we create a mock node and deploy a new mocked contract: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let abi = voting_abi(); +//! let mock = Mock::new(/* chain_id = */ 1337); +//! let contract = mock.deploy(abi); +//! # Ok(()) +//! # } +//! ``` +//! +//! Then we set up expectations for method calls: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let abi = voting_abi(); +//! # let account = account_for("Alice"); +//! # let mock = Mock::new(1337); +//! # let contract = mock.deploy(abi); +//! // We'll need to know method signatures and types. +//! let vote: Signature<(U256,), ()> = [1, 33, 185, 63].into(); +//! let winning_proposal: Signature<(), U256> = [96, 159, 241, 189].into(); +//! +//! // We expect some transactions calling the `vote` method. +//! contract +//! .expect_transaction(vote); +//! +//! // We also expect calls to `winning_proposal` that will return +//! // a value of `1`. +//! contract +//! .expect_call(winning_proposal) +//! .returns(1.into()); +//! # Ok(()) +//! # } +//! ``` +//! +//! Finally, we create a dynamic instance and work with it as usual: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let abi = voting_abi(); +//! # let account = account_for("Alice"); +//! # let mock = Mock::new(1337); +//! # let contract = mock.deploy(abi); +//! # let vote: Signature<(U256,), ()> = [1, 33, 185, 63].into(); +//! # let winning_proposal: Signature<(), U256> = [96, 159, 241, 189].into(); +//! # contract.expect_transaction(vote); +//! # contract.expect_call(winning_proposal).returns(1.into()); +//! let instance = contract.instance(); +//! +//! instance +//! .method(vote, (1.into(),))? +//! .from(account) +//! .send() +//! .await?; +//! +//! let winning_proposal_index = instance +//! .view_method(winning_proposal, ())? +//! .call() +//! .await?; +//! assert_eq!(winning_proposal_index, 1.into()); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Describing expectations +//! +//! The mocked contracts have an interface similar to the one +//! of the [`mockall`] crate. +//! +//! For each contract's method that you expect to be called during a test, +//! call [`Contract::expect_transaction`] or [`Contract::expect_call`] +//! and set up the created [`Expectation`] with functions such as [`returns`], +//! [`times`], [`in_sequence`]. For greater flexibility, you can have +//! multiple expectations attached to the same method. +//! +//! See [`Expectation`] for more info and examples. +//! +//! # Interacting with mocked contracts +//! +//! After contract's behaviour is programmed, you can call +//! [`Contract::instance`] to create an ethcontract's [`Instance`]. +//! +//! You can also get contract's address and send RPC calls directly +//! through [`web3`]. +//! +//! Specifically, mock node supports `eth_call`, `eth_sendRawTransaction`, +//! and `eth_getTransactionReceipt`. +//! +//! At the moment, mock node can't sign transactions on its own, +//! so `eth_sendTransaction` is not supported. Also, deploying contracts +//! via `eth_sendRawTransaction` is not possible yet. +//! +//! # Mocking generated contracts +//! +//! Overall, generated contracts are similar to the dynamic ones: +//! they are deployed with [`Mock::deploy`] and configured with +//! [`Contract::expect_call`] and [`Contract::expect_transaction`]. +//! +//! You can get generated contract's ABI using the `raw_contract` function. +//! +//! Generated [method signatures] are available through the `signatures` +//! function. +//! +//! Finally, type-safe instance can be created using the `at` method. +//! +//! Here's an example of mocking an ERC20-compatible contract. +//! +//! First, we create a mock node and deploy a new mocked contract: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # /* +//! ethcontract::contract!("ERC20.json"); +//! # */ +//! # ethcontract::contract!( +//! # "../examples/truffle/build/contracts/IERC20.json", +//! # contract = IERC20 as ERC20, +//! # ); +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! let mock = Mock::new(/* chain_id = */ 1337); +//! let contract = mock.deploy(ERC20::raw_contract().abi.clone()); +//! # Ok(()) +//! # } +//! ``` +//! +//! Then we set up expectations using the generated method signatures: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # ethcontract::contract!( +//! # "../examples/truffle/build/contracts/IERC20.json", +//! # contract = IERC20 as ERC20, +//! # ); +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let account = account_for("Alice"); +//! # let recipient = address_for("Bob"); +//! # let mock = Mock::new(1337); +//! # let contract = mock.deploy(ERC20::raw_contract().abi.clone()); +//! contract +//! .expect_transaction(ERC20::signatures().transfer()) +//! .once() +//! .returns(true); +//! # let instance = ERC20::at(&mock.web3(), contract.address()); +//! # instance.transfer(recipient, 100.into()).from(account).send().await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! Finally, we use mock contract's address to interact with the mock node: +//! +//! ``` +//! # include!("test/doctest/common.rs"); +//! # ethcontract::contract!( +//! # "../examples/truffle/build/contracts/IERC20.json", +//! # contract = IERC20 as ERC20, +//! # ); +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let account = account_for("Alice"); +//! # let recipient = address_for("Bob"); +//! # let mock = Mock::new(1337); +//! # let contract = mock.deploy(ERC20::raw_contract().abi.clone()); +//! # contract.expect_transaction(ERC20::signatures().transfer()); +//! let instance = ERC20::at(&mock.web3(), contract.address()); +//! instance +//! .transfer(recipient, 100.into()) +//! .from(account) +//! .send() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Mocking gas and gas estimation +//! +//! Mock node allows you to customize value returned from `eth_gasPrice` +//! RPC call. Use [`Mock::update_gas_price`] to set a new gas price. +//! +//! Estimating gas consumption with `eth_estimateGas` is not supported at the +//! moment. For now, calls to `eth_estimateGas` always return `1`. +//! +//! [`web3-rs`]: ethcontract::web3 +//! [`web3`]: ethcontract::web3 +//! [`expect_call`]: Contract::expect_call +//! [`expect_transaction`]: Contract::expect_transaction +//! [`returns`]: Expectation::returns +//! [`times`]: Expectation::times +//! [`in_sequence`]: Expectation::in_sequence +//! [`Instance`]: ethcontract::Instance +//! [voting contract]: https://docs.soliditylang.org/en/v0.8.6/solidity-by-example.html#voting +//! [method signatures]: Signature use crate::predicate::TuplePredicate; use crate::range::TimesRange; @@ -20,6 +234,21 @@ mod predicate; mod range; /// Mock ethereum node. +/// +/// This struct implements a virtual ethereum node with a limited number +/// of supported RPC calls. You can interact with it via the standard +/// transport from `web3`. +/// +/// The main feature of this struct is deploying mocked contracts +/// and interacting with them. Create new mocked contract with a call +/// to [`deploy`] function. Then use the returned struct to set up +/// expectations on contract methods, get deployed contract's address +/// and [`Instance`] and make actual calls to it. +/// +/// Deploying contracts with an RPC call is not supported at the moment. +/// +/// [`deploy`]: Mock::deploy +/// [`Instance`]: ethcontract::Instance #[derive(Clone)] pub struct Mock { transport: details::MockTransport, @@ -66,6 +295,15 @@ impl Mock { /// Verifies that all expectations on all contracts have been met, /// then clears all expectations. + /// + /// Sometimes its useful to validate all expectations mid-test, + /// throw them away, and add new ones. That’s what checkpoints do. + /// See [mockall documentation] for more info. + /// + /// Note that all expectations returned from [`Contract::expect`] method + /// become invalid after checkpoint. Modifying them will result in panic. + /// + /// [mockall documentation]: https://docs.rs/mockall/#checkpoints pub fn checkpoint(&self) { self.transport.checkpoint(); } @@ -181,12 +419,56 @@ impl Contract { /// Verifies that all expectations on this contract have been met, /// then clears all expectations. + /// + /// Sometimes its useful to validate all expectations mid-test, + /// throw them away, and add new ones. That’s what checkpoints do. + /// See [mockall documentation] for more info. + /// + /// Note that all expectations returned from [`expect`] method + /// become invalid after checkpoint. Modifying them will result in panic. + /// + /// [mockall documentation]: https://docs.rs/mockall/#checkpoints + /// [`expect`]: Contract::expect pub fn checkpoint(&self) { self.transport.contract_checkpoint(self.address); } } /// Expectation for contract method. +/// +/// A method could have multiple expectations associated with it. +/// Each expectation specifies how the method should be called, how many times, +/// with what arguments, etc. +/// +/// When a method gets called, mock node determines if the call is expected +/// or not. It goes through each of the method's expectations in order they +/// were created, searching for the first expectation that matches the call. +/// +/// If a suitable expectation is found, it is used to determine method's +/// return value and other transaction properties. If not, the call +/// is considered unexpected, and mock node panics. +/// +/// To determine if a particular expectation should be used for the given call, +/// mock node uses two of the expectation's properties: +/// +/// - [`predicate`] checks if method's arguments and transaction properties +/// match a certain criteria; +/// - [times limiter] is used to limit number of times a single expectation +/// can be used. +/// +/// To determine result of a method call, [`returns`] property is used. +/// +/// # Notes +/// +/// Expectations can't be changed after they were used. That is, if you try +/// to modify an expectation after making any calls to its contract method, +/// mock node will panic. This happens because modifying an already-used +/// expectation may break node's internal state. Adding new expectations +/// at any time is fine, though. +/// +/// [`predicate`]: Expectation::predicate +/// [times limiter]: Expectation::times +/// [`returns`]: Expectation::returns pub struct Expectation { transport: details::MockTransport, address: Address, @@ -198,6 +480,112 @@ pub struct Expectation Expectation { /// Specifies how many times this expectation can be called. + /// + /// By default, each expectation can be called any number of times, + /// including zero. This method allows specifying a more precise range. + /// + /// For example, use `times(1)` to indicate that the expectation + /// should be called exactly [`once`]. Or use `times(1..)` to indicate + /// that it should be called at least once. Any range syntax is accepted. + /// + /// If the expectation gets called less that the specified number + /// of times, the test panics. + /// + /// If it gets called enough number of times, expectation is considered + /// satisfied. It becomes inactive and is no longer checked when processing + /// new method calls. + /// + /// # Examples + /// + /// Consider a method with two expectations: + /// + /// ``` + /// # include!("test/doctest/common.rs"); + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let contract = contract(); + /// # let signature = signature(); + /// contract + /// .expect_call(signature) + /// .times(1..=2); + /// contract + /// .expect_call(signature); + /// # contract.instance().view_method(signature, (0, 0))?.call().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// The first two calls to this method will be dispatched to the first + /// expectation. Then first expectation will become satisfied, and all + /// other calls will be dispatched to the second one. + /// + /// # Notes + /// + /// When expectation becomes satisfied, previous expectations + /// are not altered and may still be unsatisfied. This is important + /// when you have expectations with predicates: + /// + /// ``` + /// # include!("test/doctest/common.rs"); + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let contract = contract(); + /// # let signature = signature(); + /// contract + /// .expect_call(signature) + /// .predicate_fn(|(a, b)| a == b) + /// .times(1..=2); + /// contract + /// .expect_call(signature) + /// .times(1); + /// contract + /// .expect_call(signature) + /// .times(..); + /// # contract.instance().view_method(signature, (0, 0))?.call().await?; + /// # contract.instance().view_method(signature, (0, 1))?.call().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Here, first expectation can be called one or two times, second + /// expectation can be called exactly once, and third expectation + /// can be called arbitrary number of times. + /// + /// Now, consider the following sequence of calls: + /// + /// ``` + /// # include!("test/doctest/common.rs"); + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # let contract = contract(); + /// # let signature = signature(); + /// # let instance = contract.instance(); + /// # contract.expect(signature); + /// instance + /// .method(signature, (1, 1))? + /// .call() + /// .await?; + /// instance + /// .method(signature, (2, 3))? + /// .call() + /// .await?; + /// instance + /// .method(signature, (5, 5))? + /// .call() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// First call gets dispatched to the first expectation. Second call + /// can't be dispatched to the first expectation because of its predicate, + /// so it gets dispatched to the second one. Now, one may assume that + /// the third call will be dispatched to the third expectation. However, + /// first expectation can be called one more time, so it is not satisfied + /// yet. Because of this, third call gets dispatched + /// to the first expectation. + /// + /// [`once`]: Expectation::once pub fn times(self, times: impl Into) -> Self { self.transport.times::( self.address, @@ -228,6 +616,20 @@ impl Expectation Self { self.transport.in_sequence::( self.address, @@ -254,6 +656,54 @@ impl Expectation Result<(), Box> { + /// # let contract = contract(); + /// # let signature = signature(); + /// contract + /// .expect_call(signature) + /// .predicate((predicate::eq(1), predicate::eq(1))) + /// .returns(1); + /// contract + /// .expect_call(signature) + /// .predicate_fn(|(a, b)| a > b) + /// .returns(2); + /// contract + /// .expect_call(signature) + /// .returns(3); + /// # Ok(()) + /// # } + /// ``` + /// + /// Here, we have three expectations, resulting in the following behaviour. + /// If both arguments are equal to `1`, method returns `1`. + /// Otherwise, if the first argument is greater than the second one, method + /// returns `2`. Otherwise, it returns `3`. + /// + /// # Notes + /// + /// Having multiple predicates shines for complex setups that involve + /// [call sequences] and [limiting number of expectation uses]. + /// For simpler setups like the one above, [`returns_fn`] may be more + /// clear and concise, and also more efficient. + /// + /// [call sequences]: Expectation::in_sequence + /// [limiting number of expectation uses]: Expectation::times + /// [`returns_fn`]: Expectation::returns_fn pub fn predicate(self, pred: T) -> Self where T: TuplePredicate

+ Send + 'static, @@ -370,6 +820,18 @@ impl Expectation Result + Send + 'static) -> Self { self.transport.returns_fn::( self.address, @@ -385,6 +847,19 @@ impl Expectation Result + Send + 'static, @@ -401,6 +876,13 @@ impl Expectation Self { self.transport.returns_error::( self.address, @@ -414,6 +896,14 @@ impl Expectation Self { self.transport.returns_default::( self.address, From e78fb0e8074f58e3c07df75da2e4fe3ce29bdbe7 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:45:03 +0200 Subject: [PATCH 19/21] Add tests --- ethcontract-mock/src/lib.rs | 3 + ethcontract-mock/src/test/doctest/common.rs | 106 ++++++++++ ethcontract-mock/src/test/eth_block_number.rs | 75 +++++++ ethcontract-mock/src/test/eth_chain_id.rs | 11 ++ ethcontract-mock/src/test/eth_estimate_gas.rs | 137 +++++++++++++ ethcontract-mock/src/test/eth_gas_price.rs | 16 ++ .../src/test/eth_get_transaction_receipt.rs | 36 ++++ .../src/test/eth_send_transaction.rs | 17 ++ .../src/test/eth_transaction_count.rs | 147 ++++++++++++++ ethcontract-mock/src/test/mod.rs | 186 ++++++++++++++++++ 10 files changed, 734 insertions(+) create mode 100644 ethcontract-mock/src/test/doctest/common.rs create mode 100644 ethcontract-mock/src/test/eth_block_number.rs create mode 100644 ethcontract-mock/src/test/eth_chain_id.rs create mode 100644 ethcontract-mock/src/test/eth_estimate_gas.rs create mode 100644 ethcontract-mock/src/test/eth_gas_price.rs create mode 100644 ethcontract-mock/src/test/eth_get_transaction_receipt.rs create mode 100644 ethcontract-mock/src/test/eth_send_transaction.rs create mode 100644 ethcontract-mock/src/test/eth_transaction_count.rs create mode 100644 ethcontract-mock/src/test/mod.rs diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 4cd3a7e6..ceffe4de 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -233,6 +233,9 @@ mod details; mod predicate; mod range; +#[cfg(test)] +mod test; + /// Mock ethereum node. /// /// This struct implements a virtual ethereum node with a limited number diff --git a/ethcontract-mock/src/test/doctest/common.rs b/ethcontract-mock/src/test/doctest/common.rs new file mode 100644 index 00000000..a44d2bf7 --- /dev/null +++ b/ethcontract-mock/src/test/doctest/common.rs @@ -0,0 +1,106 @@ +// Common types used in tests and doctests. +// +// This file is `include!`d by doctests, it is not a part of the crate. + +use ethcontract::dyns::DynInstance; +use ethcontract::prelude::*; +use ethcontract_mock::{CallContext, Contract, Expectation, Mock, Signature}; +use predicates::prelude::*; + +fn simple_abi() -> ethcontract::common::Abi { + static ABI: &str = r#" + { + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "a", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "b", + "type": "uint256" + } + ], + "name": "Foo", + "outputs": [ + { + "internalType": "uint256", + "name": "a", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } + "#; + + ethcontract::common::artifact::truffle::TruffleLoader::new() + .load_contract_from_str(ABI) + .unwrap() + .abi +} + +fn voting_abi() -> ethcontract::common::Abi { + static ABI: &str = r#" + { + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposal", + "type": "uint256" + } + ], + "name": "vote", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "winningProposal", + "outputs": [ + { + "internalType": "uint256", + "name": "winningProposal_", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] + } + "#; + + ethcontract::common::artifact::truffle::TruffleLoader::new() + .load_contract_from_str(ABI) + .unwrap() + .abi +} + +fn contract() -> Contract { + Mock::new(10).deploy(simple_abi()) +} + +fn signature() -> Signature<(u64, u64), u64> { + Signature::new([54, 175, 98, 158]) +} + +fn address_for(who: &str) -> Address { + account_for(who).address() +} + +fn account_for(who: &str) -> Account { + use ethcontract::web3::signing::keccak256; + Account::Offline( + PrivateKey::from_raw(keccak256(who.as_bytes())).unwrap(), + None, + ) +} diff --git a/ethcontract-mock/src/test/eth_block_number.rs b/ethcontract-mock/src/test/eth_block_number.rs new file mode 100644 index 00000000..09ae6b32 --- /dev/null +++ b/ethcontract-mock/src/test/eth_block_number.rs @@ -0,0 +1,75 @@ +use super::*; +use crate::Mock; + +#[tokio::test] +async fn block_number_initially_zero() -> Result { + let web3 = Mock::new(1234).web3(); + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + Ok(()) +} + +#[tokio::test] +async fn block_number_advanced_after_tx() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + instance + .transfer(address_for("Alice"), 100.into()) + .send() + .await?; + + assert_eq!(web3.eth().block_number().await?, 1.into()); + + Ok(()) +} + +#[tokio::test] +async fn block_number_advanced_and_confirmed_after_tx() -> Result { + let (_, web3, contract, instance) = setup(); + + contract + .expect(IERC20::signatures().transfer()) + .confirmations(5); + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + instance + .transfer(address_for("Alice"), 100.into()) + .send() + .await?; + + assert_eq!(web3.eth().block_number().await?, 6.into()); + + Ok(()) +} + +#[tokio::test] +async fn block_number_is_not_advanced_after_call_or_gas_estimation() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + instance + .transfer(address_for("Alice"), 100.into()) + .call() + .await?; + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + instance + .transfer(address_for("Alice"), 100.into()) + .into_inner() + .estimate_gas() + .await?; + + assert_eq!(web3.eth().block_number().await?, 0.into()); + + Ok(()) +} diff --git a/ethcontract-mock/src/test/eth_chain_id.rs b/ethcontract-mock/src/test/eth_chain_id.rs new file mode 100644 index 00000000..33d06741 --- /dev/null +++ b/ethcontract-mock/src/test/eth_chain_id.rs @@ -0,0 +1,11 @@ +use super::*; +use crate::Mock; + +#[tokio::test] +async fn chain_id() -> Result { + let web3 = Mock::new(1234).web3(); + + assert_eq!(web3.eth().chain_id().await?, 1234.into()); + + Ok(()) +} diff --git a/ethcontract-mock/src/test/eth_estimate_gas.rs b/ethcontract-mock/src/test/eth_estimate_gas.rs new file mode 100644 index 00000000..e8f97124 --- /dev/null +++ b/ethcontract-mock/src/test/eth_estimate_gas.rs @@ -0,0 +1,137 @@ +use super::*; +use ethcontract::web3::types::CallRequest; + +#[tokio::test] +async fn estimate_gas_returns_one() -> Result { + let (_, _, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + let gas = instance + .transfer(address_for("Alice"), 100.into()) + .into_inner() + .estimate_gas() + .await?; + + assert_eq!(gas, 1.into()); + + Ok(()) +} + +#[tokio::test] +async fn estimate_gas_is_supported_for_edge_block() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + instance + .transfer(address_for("Bob"), 100.into()) + .send() + .await?; + instance + .transfer(address_for("Bob"), 100.into()) + .send() + .await?; + + let request = { + let tx = instance + .transfer(address_for("Alice"), 100.into()) + .into_inner(); + + CallRequest { + from: Some(address_for("Alice")), + to: Some(contract.address), + gas: None, + gas_price: None, + value: None, + data: tx.data, + transaction_type: None, + access_list: None, + } + }; + + assert_eq!( + web3.eth() + .estimate_gas(request.clone(), Some(BlockNumber::Latest)) + .await?, + 1.into() + ); + assert_eq!( + web3.eth() + .estimate_gas(request.clone(), Some(BlockNumber::Pending)) + .await?, + 1.into() + ); + assert_eq!( + web3.eth() + .estimate_gas(request.clone(), Some(BlockNumber::Number(2.into()))) + .await?, + 1.into() + ); + + Ok(()) +} + +#[tokio::test] +#[should_panic( + expected = "mock node does not support executing methods on non-last block" +)] +async fn estimate_gas_is_not_supported_for_custom_block() { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + let request = { + let tx = instance + .transfer(address_for("Alice"), 100.into()) + .into_inner(); + + CallRequest { + from: Some(address_for("Alice")), + to: Some(contract.address), + gas: None, + gas_price: None, + value: None, + data: tx.data, + transaction_type: None, + access_list: None, + } + }; + + web3.eth() + .estimate_gas(request.clone(), Some(BlockNumber::Number(1.into()))) + .await + .unwrap(); +} + +#[tokio::test] +#[should_panic( + expected = "mock node does not support executing methods on earliest block" +)] +async fn estimate_gas_is_not_supported_for_earliest_block() { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + let request = { + let tx = instance + .transfer(address_for("Alice"), 100.into()) + .into_inner(); + + CallRequest { + from: Some(address_for("Alice")), + to: Some(contract.address), + gas: None, + gas_price: None, + value: None, + data: tx.data, + transaction_type: None, + access_list: None, + } + }; + + web3.eth() + .estimate_gas(request.clone(), Some(BlockNumber::Earliest)) + .await + .unwrap(); +} diff --git a/ethcontract-mock/src/test/eth_gas_price.rs b/ethcontract-mock/src/test/eth_gas_price.rs new file mode 100644 index 00000000..be2a6bac --- /dev/null +++ b/ethcontract-mock/src/test/eth_gas_price.rs @@ -0,0 +1,16 @@ +use super::*; +use crate::Mock; + +#[tokio::test] +async fn gas_price() -> Result { + let mock = Mock::new(1234); + let web3 = mock.web3(); + + assert_eq!(web3.eth().gas_price().await?, 1.into()); + + mock.update_gas_price(10); + + assert_eq!(web3.eth().gas_price().await?, 10.into()); + + Ok(()) +} diff --git a/ethcontract-mock/src/test/eth_get_transaction_receipt.rs b/ethcontract-mock/src/test/eth_get_transaction_receipt.rs new file mode 100644 index 00000000..cdec4221 --- /dev/null +++ b/ethcontract-mock/src/test/eth_get_transaction_receipt.rs @@ -0,0 +1,36 @@ +use super::*; +use ethcontract::transaction::ResolveCondition; + +#[tokio::test] +async fn transaction_receipt_is_returned() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + let hash = instance + .transfer(address_for("Bob"), 100.into()) + .into_inner() + .resolve(ResolveCondition::Pending) + .send() + .await? + .hash(); + + let receipt = web3.eth().transaction_receipt(hash).await?.unwrap(); + assert_eq!(receipt.transaction_hash, hash); + assert_eq!(receipt.block_number, Some(1.into())); + assert_eq!(receipt.status, Some(1.into())); + + Ok(()) +} + +#[tokio::test] +#[should_panic(expected = "there is no transaction with hash")] +async fn transaction_receipt_is_panicking_when_hash_not_fount() { + let web3 = Mock::new(1234).web3(); + + web3 + .eth() + .transaction_receipt(Default::default()) + .await + .unwrap(); +} diff --git a/ethcontract-mock/src/test/eth_send_transaction.rs b/ethcontract-mock/src/test/eth_send_transaction.rs new file mode 100644 index 00000000..bea03e6c --- /dev/null +++ b/ethcontract-mock/src/test/eth_send_transaction.rs @@ -0,0 +1,17 @@ +use crate::Mock; +use ethcontract::web3::types::TransactionRequest; + +#[tokio::test] +#[should_panic(expected = "mock node can't sign transactions")] +async fn send_transaction() { + // When we implement `send_transaction`, we should add same tests as for + // send_raw_transaction (expect for raw transaction format/signing) + // and also a test that checks that returned transaction hash is correct. + + let web3 = Mock::new(1234).web3(); + + web3.eth() + .send_transaction(TransactionRequest::default()) + .await + .unwrap(); +} diff --git a/ethcontract-mock/src/test/eth_transaction_count.rs b/ethcontract-mock/src/test/eth_transaction_count.rs new file mode 100644 index 00000000..27616cdf --- /dev/null +++ b/ethcontract-mock/src/test/eth_transaction_count.rs @@ -0,0 +1,147 @@ +use super::*; +use crate::Mock; +use ethcontract::BlockNumber; + +#[tokio::test] +async fn transaction_count_initially_zero() -> Result { + let web3 = Mock::new(1234).web3(); + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 0.into() + ); + + Ok(()) +} + +#[tokio::test] +async fn transaction_count_advanced_after_tx() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 0.into() + ); + + instance + .transfer(address_for("Bob"), 100.into()) + .send() + .await?; + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 1.into() + ); + + Ok(()) +} + +#[tokio::test] +async fn transaction_count_is_not_advanced_after_call_or_gas_estimation() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 0.into() + ); + + instance + .transfer(address_for("Bob"), 100.into()) + .call() + .await?; + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 0.into() + ); + + instance + .transfer(address_for("Bob"), 100.into()) + .into_inner() + .estimate_gas() + .await?; + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), None) + .await?, + 0.into() + ); + + Ok(()) +} + +#[tokio::test] +async fn transaction_count_is_supported_for_edge_block() -> Result { + let (_, web3, contract, instance) = setup(); + + contract.expect(IERC20::signatures().transfer()); + + instance + .transfer(address_for("Bob"), 100.into()) + .send() + .await?; + instance + .transfer(address_for("Bob"), 100.into()) + .send() + .await?; + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Earliest)) + .await?, + 0.into() + ); + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Number(0.into()))) + .await?, + 0.into() + ); + + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Latest)) + .await?, + 2.into() + ); + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Pending)) + .await?, + 2.into() + ); + assert_eq!( + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Number(2.into()))) + .await?, + 2.into() + ); + + Ok(()) +} + +#[tokio::test] +#[should_panic(expected = "mock node does not support returning transaction count for specific block number")] +async fn transaction_count_is_not_supported_for_custom_block() { + let web3 = Mock::new(1234).web3(); + + web3.eth() + .transaction_count(address_for("Alice"), Some(BlockNumber::Number(1.into()))) + .await + .unwrap(); +} diff --git a/ethcontract-mock/src/test/mod.rs b/ethcontract-mock/src/test/mod.rs new file mode 100644 index 00000000..fd678f8d --- /dev/null +++ b/ethcontract-mock/src/test/mod.rs @@ -0,0 +1,186 @@ +/// Tests for mock crate. +/// +/// # TODO +/// +/// Some tests for API are missing: +/// +/// - malformed input in +/// - eth_call +/// - eth_sendTransaction +/// - eth_sendRawTransaction +/// - eth_estimateGas +/// +/// - deployment works +/// - different contracts have different addresses +/// - returned instance has correct address +/// +/// - call to method with no expectations panics +/// - tx to method with no expectations panics +/// - call to method with an expectation succeeds +/// - tx to method with an expectation succeeds +/// +/// - call expectations only match calls +/// - tx expectations only match txs +/// - regular expectations match both calls and txs +/// +/// - predicate filters expectation so test panics +/// - predicate filters multiple expectations so test panics +/// - expectations are evaluated in FIFO order +/// - predicate_fn gets called +/// - predicate_fn_ctx gets called +/// +/// - times can be set for expectation +/// - if expectation called not enough times, test panics +/// - if expectation called enough times, test passes +/// - if expectation called enough times, it is satisfied and test panics +/// - if expectation called enough times, it is satisfied and next expectation is used +/// - expectation is not satisfied if calls are not matched by a predicate +/// +/// - expectations can be added to sequences +/// - expectation can only be in one sequence +/// - adding expectation to sequence requires exact time greater than zero +/// - updating times after expectation was set requires exact time greater than zero +/// - when method called in-order, test passes +/// - when method called in-order multiple times, test passes +/// - when method called out-of-order, test panics +/// - when method called out-of-order first time with times(2), test panics +/// - when method called out-of-order last time with times(2), test panics +/// +/// - default value for solidity type is returned +/// - rust's Default trait is not honored +/// - you can set return value +/// - returns_fn gets called +/// - returns_fn_ctx gets called +/// +/// - expectations become immutable after use in calls and txs +/// - expectations become immutable after use in calls and txs even if they are not matched by a predicate +/// - new expectations are not immutable +/// +/// - checkpoint verifies expectations +/// - checkpoint clears expectations +/// - expectations become invalid +/// +/// - confirmations plays nicely with tx.confirmations + +use crate::{Contract, Mock}; +use ethcontract::dyns::DynWeb3; +use ethcontract::prelude::*; +use predicates::prelude::*; + +mod eth_block_number; +mod eth_chain_id; +mod eth_estimate_gas; +mod eth_gas_price; +mod eth_get_transaction_receipt; +mod eth_send_transaction; +mod eth_transaction_count; + +type Result = std::result::Result<(), Box>; + +ethcontract::contract!("examples/truffle/build/contracts/IERC20.json"); + +fn address_for(who: &str) -> Address { + account_for(who).address() +} + +fn account_for(who: &str) -> Account { + use ethcontract::web3::signing::keccak256; + Account::Offline( + PrivateKey::from_raw(keccak256(who.as_bytes())).unwrap(), + None, + ) +} + +fn setup() -> (Mock, DynWeb3, Contract, IERC20) { + let mock = Mock::new(1234); + let web3 = mock.web3(); + let contract = mock.deploy(IERC20::raw_contract().abi.clone()); + let mut instance = IERC20::at(&web3, contract.address); + instance.defaults_mut().from = Some(account_for("Alice")); + + (mock, web3, contract, instance) +} + +#[tokio::test] +async fn general_test() { + let mock = crate::Mock::new(1234); + let contract = mock.deploy(IERC20::raw_contract().abi.clone()); + + let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + + let mut sequence = mockall::Sequence::new(); + + contract + .expect(IERC20::signatures().balance_of()) + .once() + .predicate((predicate::eq(address_for("Bob")),)) + .returns(U256::from(0)) + .in_sequence(&mut sequence); + + contract + .expect(IERC20::signatures().transfer()) + .once() + .predicate_fn_ctx(|ctx, _| !ctx.is_view_call) + .returns_fn_ctx({ + let called = called.clone(); + move |ctx, (recipient, amount)| { + assert_eq!(ctx.from, address_for("Alice")); + assert_eq!(ctx.nonce.as_u64(), 0); + assert_eq!(ctx.gas.as_u64(), 1); + assert_eq!(ctx.gas_price.as_u64(), 1); + assert_eq!(recipient, address_for("Bob")); + assert_eq!(amount.as_u64(), 100); + + called.store(true, std::sync::atomic::Ordering::Relaxed); + + Ok(true) + } + }) + .confirmations(3) + .in_sequence(&mut sequence); + + contract + .expect(IERC20::signatures().balance_of()) + .once() + .predicate((predicate::eq(address_for("Bob")),)) + .returns(U256::from(100)) + .in_sequence(&mut sequence); + + contract + .expect(IERC20::signatures().balance_of()) + .predicate((predicate::eq(address_for("Alice")),)) + .returns(U256::from(100000)); + + let actual_contract = IERC20::at(&mock.web3(), contract.address); + + let balance = actual_contract + .balance_of(address_for("Bob")) + .call() + .await + .unwrap(); + assert_eq!(balance.as_u64(), 0); + + assert!(!called.load(std::sync::atomic::Ordering::Relaxed)); + actual_contract + .transfer(address_for("Bob"), U256::from(100)) + .from(account_for("Alice")) + .confirmations(3) + .send() + .await + .unwrap(); + assert!(called.load(std::sync::atomic::Ordering::Relaxed)); + + let balance = actual_contract + .balance_of(address_for("Bob")) + .call() + .await + .unwrap(); + assert_eq!(balance.as_u64(), 100); + + let balance = actual_contract + .balance_of(address_for("Alice")) + .call() + .await + .unwrap(); + assert_eq!(balance.as_u64(), 100000); +} From 19a19bf9762245a5bc37a1c81dce08d1654a3f2e Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 2 Aug 2021 11:47:21 +0200 Subject: [PATCH 20/21] Run rustfmt --- ethcontract-mock/src/details/mod.rs | 4 +- ethcontract-mock/src/lib.rs | 6 +- ethcontract-mock/src/test/eth_estimate_gas.rs | 8 +- .../src/test/eth_get_transaction_receipt.rs | 3 +- .../src/test/eth_transaction_count.rs | 4 +- ethcontract-mock/src/test/mod.rs | 126 +++++++++--------- 6 files changed, 74 insertions(+), 77 deletions(-) diff --git a/ethcontract-mock/src/details/mod.rs b/ethcontract-mock/src/details/mod.rs index 87db3baa..61890928 100644 --- a/ethcontract-mock/src/details/mod.rs +++ b/ethcontract-mock/src/details/mod.rs @@ -796,7 +796,7 @@ impl Method { // are only a few expectations for a method, and they are likely // to be filtered out by `is_active`. if let Some(result) = - expectation.process_tx(&tx, &self.description, &self.function, params.clone()) + expectation.process_tx(&tx, &self.description, &self.function, params.clone()) { return result; } @@ -885,7 +885,7 @@ impl Expectation ExpectationApi -for Expectation + for Expectation { fn as_any(&mut self) -> &mut dyn Any { self diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index ceffe4de..54fd7190 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -708,9 +708,9 @@ impl Expectation(self, pred: T) -> Self - where - T: TuplePredicate

+ Send + 'static, - >::P: Send, + where + T: TuplePredicate

+ Send + 'static, + >::P: Send, { self.transport.predicate::( self.address, diff --git a/ethcontract-mock/src/test/eth_estimate_gas.rs b/ethcontract-mock/src/test/eth_estimate_gas.rs index e8f97124..28234a75 100644 --- a/ethcontract-mock/src/test/eth_estimate_gas.rs +++ b/ethcontract-mock/src/test/eth_estimate_gas.rs @@ -73,9 +73,7 @@ async fn estimate_gas_is_supported_for_edge_block() -> Result { } #[tokio::test] -#[should_panic( - expected = "mock node does not support executing methods on non-last block" -)] +#[should_panic(expected = "mock node does not support executing methods on non-last block")] async fn estimate_gas_is_not_supported_for_custom_block() { let (_, web3, contract, instance) = setup(); @@ -105,9 +103,7 @@ async fn estimate_gas_is_not_supported_for_custom_block() { } #[tokio::test] -#[should_panic( - expected = "mock node does not support executing methods on earliest block" -)] +#[should_panic(expected = "mock node does not support executing methods on earliest block")] async fn estimate_gas_is_not_supported_for_earliest_block() { let (_, web3, contract, instance) = setup(); diff --git a/ethcontract-mock/src/test/eth_get_transaction_receipt.rs b/ethcontract-mock/src/test/eth_get_transaction_receipt.rs index cdec4221..ed09c977 100644 --- a/ethcontract-mock/src/test/eth_get_transaction_receipt.rs +++ b/ethcontract-mock/src/test/eth_get_transaction_receipt.rs @@ -28,8 +28,7 @@ async fn transaction_receipt_is_returned() -> Result { async fn transaction_receipt_is_panicking_when_hash_not_fount() { let web3 = Mock::new(1234).web3(); - web3 - .eth() + web3.eth() .transaction_receipt(Default::default()) .await .unwrap(); diff --git a/ethcontract-mock/src/test/eth_transaction_count.rs b/ethcontract-mock/src/test/eth_transaction_count.rs index 27616cdf..403d7659 100644 --- a/ethcontract-mock/src/test/eth_transaction_count.rs +++ b/ethcontract-mock/src/test/eth_transaction_count.rs @@ -136,7 +136,9 @@ async fn transaction_count_is_supported_for_edge_block() -> Result { } #[tokio::test] -#[should_panic(expected = "mock node does not support returning transaction count for specific block number")] +#[should_panic( + expected = "mock node does not support returning transaction count for specific block number" +)] async fn transaction_count_is_not_supported_for_custom_block() { let web3 = Mock::new(1234).web3(); diff --git a/ethcontract-mock/src/test/mod.rs b/ethcontract-mock/src/test/mod.rs index fd678f8d..51763e59 100644 --- a/ethcontract-mock/src/test/mod.rs +++ b/ethcontract-mock/src/test/mod.rs @@ -1,66 +1,66 @@ -/// Tests for mock crate. -/// -/// # TODO -/// -/// Some tests for API are missing: -/// -/// - malformed input in -/// - eth_call -/// - eth_sendTransaction -/// - eth_sendRawTransaction -/// - eth_estimateGas -/// -/// - deployment works -/// - different contracts have different addresses -/// - returned instance has correct address -/// -/// - call to method with no expectations panics -/// - tx to method with no expectations panics -/// - call to method with an expectation succeeds -/// - tx to method with an expectation succeeds -/// -/// - call expectations only match calls -/// - tx expectations only match txs -/// - regular expectations match both calls and txs -/// -/// - predicate filters expectation so test panics -/// - predicate filters multiple expectations so test panics -/// - expectations are evaluated in FIFO order -/// - predicate_fn gets called -/// - predicate_fn_ctx gets called -/// -/// - times can be set for expectation -/// - if expectation called not enough times, test panics -/// - if expectation called enough times, test passes -/// - if expectation called enough times, it is satisfied and test panics -/// - if expectation called enough times, it is satisfied and next expectation is used -/// - expectation is not satisfied if calls are not matched by a predicate -/// -/// - expectations can be added to sequences -/// - expectation can only be in one sequence -/// - adding expectation to sequence requires exact time greater than zero -/// - updating times after expectation was set requires exact time greater than zero -/// - when method called in-order, test passes -/// - when method called in-order multiple times, test passes -/// - when method called out-of-order, test panics -/// - when method called out-of-order first time with times(2), test panics -/// - when method called out-of-order last time with times(2), test panics -/// -/// - default value for solidity type is returned -/// - rust's Default trait is not honored -/// - you can set return value -/// - returns_fn gets called -/// - returns_fn_ctx gets called -/// -/// - expectations become immutable after use in calls and txs -/// - expectations become immutable after use in calls and txs even if they are not matched by a predicate -/// - new expectations are not immutable -/// -/// - checkpoint verifies expectations -/// - checkpoint clears expectations -/// - expectations become invalid -/// -/// - confirmations plays nicely with tx.confirmations +//! Tests for mock crate. +//! +//! # TODO +//! +//! Some tests for API are missing: +//! +//! - malformed input in +//! - eth_call +//! - eth_sendTransaction +//! - eth_sendRawTransaction +//! - eth_estimateGas +//! +//! - deployment works +//! - different contracts have different addresses +//! - returned instance has correct address +//! +//! - call to method with no expectations panics +//! - tx to method with no expectations panics +//! - call to method with an expectation succeeds +//! - tx to method with an expectation succeeds +//! +//! - call expectations only match calls +//! - tx expectations only match txs +//! - regular expectations match both calls and txs +//! +//! - predicate filters expectation so test panics +//! - predicate filters multiple expectations so test panics +//! - expectations are evaluated in FIFO order +//! - predicate_fn gets called +//! - predicate_fn_ctx gets called +//! +//! - times can be set for expectation +//! - if expectation called not enough times, test panics +//! - if expectation called enough times, test passes +//! - if expectation called enough times, it is satisfied and test panics +//! - if expectation called enough times, it is satisfied and next expectation is used +//! - expectation is not satisfied if calls are not matched by a predicate +//! +//! - expectations can be added to sequences +//! - expectation can only be in one sequence +//! - adding expectation to sequence requires exact time greater than zero +//! - updating times after expectation was set requires exact time greater than zero +//! - when method called in-order, test passes +//! - when method called in-order multiple times, test passes +//! - when method called out-of-order, test panics +//! - when method called out-of-order first time with times(2), test panics +//! - when method called out-of-order last time with times(2), test panics +//! +//! - default value for solidity type is returned +//! - rust's Default trait is not honored +//! - you can set return value +//! - returns_fn gets called +//! - returns_fn_ctx gets called +//! +//! - expectations become immutable after use in calls and txs +//! - expectations become immutable after use in calls and txs even if they are not matched by a predicate +//! - new expectations are not immutable +//! +//! - checkpoint verifies expectations +//! - checkpoint clears expectations +//! - expectations become invalid +//! +//! - confirmations plays nicely with tx.confirmations use crate::{Contract, Mock}; use ethcontract::dyns::DynWeb3; From e59df9dad664368267f506bd9482c0d88fd5907d Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Mon, 16 Aug 2021 11:16:18 +0300 Subject: [PATCH 21/21] Update docs --- ethcontract-mock/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ethcontract-mock/src/lib.rs b/ethcontract-mock/src/lib.rs index 54fd7190..81925511 100644 --- a/ethcontract-mock/src/lib.rs +++ b/ethcontract-mock/src/lib.rs @@ -628,10 +628,11 @@ impl Expectation Self { self.transport.in_sequence::(