Introduction
Welcome to the Rust Ethereum book, where we'll go through examples of how to interact with Ethereum using ethers-rs.
We assume familiarity with Ethereum's mechanics such as:
- Deploying a contract & interacting with it
- Sending funds across accounts
- Signing messages and recovering their sender
Installing Ethers-rs
First create a new Rust project:
$ cargo new ethers-hello-world && cd ethers-hello-world
Edit your Cargo.toml and insert the following dependency:
[dependencies]
ethers = { git = "https://github.com/gakonst/ethers-rs" }
Recommended additional tooling
Tests require the following installed:
solc. We also recommend using solc-select for more flexibility.ganache-cli
In addition, it is recommended that you set the ETHERSCAN_API_KEY environment variable
for the abigen via Etherscan tests.
You can get one here.
Getting Help
First, see if the answer to your question can be easily found here or in the API documentation . If the answer is not there, try opening an issue with the question.
Join the ethers-rs telegram to chat with the community!
Overview
Using ethers-rs
ethers-rs provides extensive tooling to generate type safe bindings for smart contracts. Under the hood, it relies on
the ethabi crate's data model for contracts and all core encoding/decoding
related traits. ethers-rs provides additional types, traits and macros that help with
- ABI encoding/decoding - encode/decode custom types and events
- Contract Bindings with
abigen!- generate type safe contract bindings
ABI Encoding & Decoding
The solidity docs cover the contract ABI specification at
great length. ethabi provides the Token enum which
represents the fixed set of ABI types,
#![allow(unused)] fn main() { pub enum Token { Address(Address), FixedBytes(FixedBytes), Bytes(Bytes), Int(Int), Uint(Uint), Bool(bool), String(String), FixedArray(Vec<Token>), Array(Vec<Token>), Tuple(Vec<Token>), } }
An encoded payload is always represented as Vec<Token>, a series of encoded values:
#![allow(unused)] fn main() { pub fn encode(tokens: &[Token]) -> Bytes { ... } }
To make this generally applicable, ethers-rs provides additional helper traits for tokenizing (encoding) and
detokenizable (decoding), most
importantly Tokenizable. All primitive
types and commonly used data structures, such as tuples are Tokenizable.
Implement ABI encoding/decoding
Contract function input can be very verbose and can consist of quite a few parameters, so it may be convenient to bundle them into user-defined data that also supports ABI compliant encoding/decoding.
A custom ABI type might look like this:
#![allow(unused)] fn main() { #[derive(Debug, Clone)] struct ValueChanged { old_author: Address, new_author: Address, old_value: String, new_value: String, } }
Implement Tokenizable manually
In order to make custom types tokenizable you'd need to implement the Tokenizable trait:
#![allow(unused)] fn main() { impl ethers_core::abi::Tokenizable for ValueChanged { /// Tries to convert a `Token` into an instance of the type fn from_token( token: ethers_core::abi::Token, ) -> Result<Self, ethers_core::abi::InvalidOutputType> where Self: Sized, { if let ethers_core::abi::Token::Tuple(tokens) = token { // -- snip -- } } /// Encodes the type into a `Token` fn into_token(self) -> ethers_core::abi::Token { ethers_core::abi::Token::Tuple(vec![ self.old_author.into_token(), self.new_author.into_token(), self.old_value.into_token(), self.new_value.into_token(), ]) } } }
Implement Tokenizable using derive macro EthAbiType
Implementing Tokenizable can be very verbose itself and error-prone, therefore ethers-rs provides
the EthAbiType derive proc macro that
automatically generates all the necessary from and into token code.
By adding a EthAbiType the code above will be automatically generated
#![allow(unused)] fn main() { use ethers::abi::EthAbiType; use ethers::types::*; #[derive(Debug, Clone, EthAbiType)] struct ValueChanged { old_author: Address, new_author: Address, old_value: String, new_value: String, } }
This will work for every type that is Tokenizable, so it's possible to nest them:
#![allow(unused)] fn main() { #[derive(Debug, Clone, EthAbiType)] struct ValueChangedOuter { inner: ValueChanged, msg: String, } }
Implement Event bindings
Ethereum Events (aka logs) work similarly, but their encoding is dependent on their indexed parameters, see solidity docs for more details.
In ethers-rs Event types are abstracted via
the EthEvent trait, which provides
necessary functions to identify and decode events.
To easily create event bindings, ethers-rs provides the EthEvent as derive proc macro as well.
In order to turn a custom type into an EthEvent you only need to add it as derive:
#![allow(unused)] fn main() { use ethers::abi::EthEvent; #[derive(Debug, Clone, EthEvent)] struct ValueChangedEvent { old_author: Address, new_author: Address, old_value: String, new_value: String, } assert_eq!( "ValueChangedEvent(address,address,string,string)", ValueChangedEvent::abi_signature() ); }
An event can be further customized via the ethevent attribute.
Rename the event
#![allow(unused)] fn main() { #[derive(Debug, Clone, EthEvent)] #[ethevent( name = "MyEvent", )] struct ValueChangedEvent { old_author: Address, new_author: Address, old_value: String, new_value: String, } assert_eq!( "MyEvent(address,address,string,string)", ValueChangedEvent::abi_signature() ); }
Mark fields as indexed
#![allow(unused)] fn main() { #[derive(Debug, Clone, EthEvent)] struct ValueChangedEvent { old_author: Address, #[ethevent(indexed, name = "newAuthor")] new_author: Address, old_value: String, new_value: String, } assert_eq!( "ValueChangedEvent(address,address,string,string)", ValueChangedEvent::abi_signature() ); }
Mark event as anonymous
#![allow(unused)] fn main() { #[derive(Debug, Clone, EthEvent)] #[ethevent(anonymous)] struct ValueChangedEvent { old_author: Address, new_author: Address, old_value: String, new_value: String, } assert_eq!( "ValueChangedEvent(address,address,string,string) anonymous", ValueChangedEvent::abi_signature() ); }
Rename and override the ABI as well
#![allow(unused)] fn main() { #[derive(Debug, Clone, EthAbiType)] struct SomeType { inner: Address, msg: String, } #[ethevent( name = "MyEvent", abi = "MyEvent(address,(address,string),string)" )] struct ValueChangedEvent { old_author: Address, val: SomeType, new_value: String, } assert_eq!( "MyEvent(address,(address,string),string)", ValueChangedEvent::abi_signature() ); }