Ethereum with Rust Tutorial Part 1: Create simple transcactions with Rust

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.

Comment