This tutorial shows how to start a local Ethereum virtual machine (EVM) in Rust and query balance and make simple transactions.
Setup requirements
Before writing any code, we need to make sure we have some required tools installed, specifically Rust and ganache.
The source code is available on Github.
Rust
Install Rust by following the instructions.
ganache-cli
Ganache helps you quickly setup an Ethereum environment for testing. If you don't want the whole bundle, you can just install ganache-cli
using npm,
npm install -g ganache
Run ganache-cli
to check if you've installed it correctly.
Rust project setup
We will now create a Rust project and add the necessary dependencies.
Create our project folder and init with cargo:
mkdir rust-ethereum-tutorial
cd rust-ethereum-tutorial
cargo init
For the convenience of adding Rust dependencies to our project, we'll use cargo-edit
:
cargo install cargo-edit
This will add the latest version of ethers
as a dependency in Cargo.toml
:
cargo add --no-default-features ethers +legacy
and a few more libraries,
cargo add tokio +full clap hex eyre
A short description of these libraries:
- tokio allows us to use asynchronous functions in Rust
- clap is for handling command line arguments
- hex is for handling hex strings
- eyre is a library that helps reduce boilerplate code when doing error handling
Connect to an Ethereum Web3 endpoint
Ethereum node typically provides HTTP, WebSocket or IPC endpoint for us to access the Ethereum network. For developing in the local environment, we can connect to our endpoint provided by locally running ganache. To start ganache in Rust, we can simply use Ganache::new().spawn()
:
use ethers::utils::Ganache;
use eyre::Result;
#[tokio::main]
async fn main() -> Result<()> {
// Spawn a aanache instance
let ganache = Ganache::new().spawn();
println!("HTTP Endpoint: {}", ganache.endpoint());
Ok(())
}
If we run with cargo run
, we'll see the endpoint URL printed in the console:
...
HTTP Endpoint: http://localhost:46795
Access the ganache test wallets
Ganache will use randomly generate mnemonic words each time it starts.
To make our example more deterministic, we can configure it to use provided mnemonic words:
let mnemonic = "gas monster ski craft below illegal discover limit dog bundle bus artefact";
let ganache = Ganache::new().mnemonic(mnemonic).spawn();
To access the wallet created by ganache, we first get the private key and then convert it to the LocalWallet
instance:
// Get the first wallet managed by ganache
let wallet: LocalWallet = ganache.keys()[0].clone().into();
let wallet_address: String = wallet.address().encode_hex();
println!("Default wallet address: {}", wallet_address);
Connect through JSON RPC
To interact with the Ethereum node or network, we'll need to create a client which connects the ganache endpoint:
// A provider is an Ethereum JsonRPC client
let provider = Provider::try_from(ganache.endpoint())?.interval(Duration::from_millis(10));
Query balance by address
Now we have our client connected to an Ethereum network, we can query the balance of any address in our wallet:
// Query the balance of our account
let first_balance = provider.get_balance(first_address, None).await?;
println!("Wallet first address balance: {}", first_balance);
If we run with cargo run
again, we'll find out that we've got 1000ETH in this address. This is added by the Ganache automatically.
To query the balance using an address in hex string format, we need to first convert the string to an Address
type,
// Query the blance of some random account
let other_address_hex = "0xaf206dCE72A0ef76643dfeDa34DB764E2126E646";
let other_address = "0xaf206dCE72A0ef76643dfeDa34DB764E2126E646".parse::<Address>()?;
let other_balance = provider.get_balance(other_address, None).await?;
println!(
"Balance for address {}: {}",
other_address_hex, other_balance
);
Try cargo run
again and you'll see the balance of this address is zero:
Balance for address 0xaf206dCE72A0ef76643dfeDa34DB764E2126E646: 0
- create transaction
Make a simple transaction
Next, we create a simple transaction to transfer some Ethereum tokens to another address.
// Create a transaction to transfer 1000 wei to `other_address`
let tx = TransactionRequest::pay(other_address, U256::from(1000u64)).from(first_address);
// Send the transaction and wait for receipt
let receipt = provider
.send_transaction(tx, None)
.await?
.log_msg("Pending transfer")
.await?
.context("Missing receipt")?;
println!(
"TX mined in block {}",
receipt.block_number.context("Can not get block number")?
);
println!(
"Balance of {} {}",
other_address_hex,
provider.get_balance(other_address, None).await?
);
Run with cargo run
again, you'll get the following results:
Pending transfer: 0xff6153310304732bb28856ca3a90ab9c94c5c9e20cf51e8a2803f3483670cd6f
TX mined in block 1
Balance of 0xaf206dCE72A0ef76643dfeDa34DB764E2126E646 1000
What's next
In the next blog, we'll explore how to compile and deploy Solidity contracts with Rust.