ethcontract-mock 0.25.5

Tools for mocking ethereum contracts.
Documentation

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<dyn std::error::Error>> {
# 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<dyn std::error::Error>> {
# 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<dyn std::error::Error>> {
# 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
# 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<dyn std::error::Error>> {
# 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.