Rust Web3 connect to Ethereum blockchain: how to

In this article for semi-beginners, we are going to learn how to connect to Ethereum with the web3 crate and Rust. We will connect using a WebSocket and then retrieve the balance of our account. Finally, we will use a token smart contract to retrieve information about the token.

Please note that we will be using the Rinkeby test network for our interactions. So, if an address for a smart contract or token doesn’t seem to work, make sure that you are using the Rinkeby network.

Please look at my GitHub for the full project: rust-web3-basics-tutorial.

Please also see this follow-up article with the same theme: Rust Web3 token transactions from blocks: how to

The follow-up article to this article talks about how to swap tokens on Uniswap V2 using Web3.

If you’re interested in other cryptocurrency-related Rust programming articles:

Prerequisites

To easily follow this article you should have knowledge on:

  • The Ethereum blockchain
  • Basic concept of smart contracts, but it will also be explained
  • The Rust programming language
  • Being able to connect to an Ethereum node (Rinkeby network) and a wallet with fake test Eth. See my article for tips: Easy to set up Ethereum connection and wallet.

If you are not familiar with Ethereum I recommend reading up on the concepts in more detail on the Ethereum website.

Set up the project

Let’s create a new Rust project to connect to Ethereum using Web3 now:

cargo new rust-web3-basics-tutorial

We will add the following dependencies:

  • hex: Encoding and decoding data into/from hexadecimal representation.
  • web3: Ethereum JSON-RPC multi-transport client. Rust implementation of Web3.js library.
  • tokio: An event-driven, non-blocking I/O platform for writing asynchronous I/O backed applications.
  • dotenv: A `dotenv` implementation for Rust. We will use this to load sensitive information (keys, ids, etc) from a .env file.
[package]
name = "uniswap-web3-rust-tutorial"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hex = "0.4"
web3 = "0.17.0"
tokio = { version= "1", features = ["full"] }
dotenv = "0.15.0"

Connecting to an Ethereum node using web3

The first things we are going to do are:

  1. Use web3 to connect to an Ethereum node using a WebSocket connection.
  2. Add an account address.
  3. Retrieve the balance from the account.

Before we start make sure you have an endpoint for an Ethereum node to connect to and a wallet with test Eth to use. See my article for tips: Easy to set up Ethereum connection and wallet.

Create a .env file in the root directory of the Rust project to put the wallet and endpoint information in like so:

INFURA_RINKEBY=wss://rinkeby.infura.io/ws/v3/xxxxxxxxxxx
ACCOUNT_ADDRESS=xxxxxxx

Note that for the account address the 0x prefix should not be included.

Below is the full code listing to achieve this, we will go through the code in sections:

use std::env;
use std::str::FromStr;

use web3::types::{H160, U256};

#[tokio::main]
async fn main() -> web3::Result<()> {
    dotenv::dotenv().ok();

    let websocket = web3::transports::WebSocket::new(&env::var("INFURA_RINKEBY").unwrap()).await?;
    let web3s = web3::Web3::new(websocket);

    let mut accounts = web3s.eth().accounts().await?;
    accounts.push(H160::from_str(&env::var("ACCOUNT_ADDRESS").unwrap()).unwrap());
    println!("Accounts: {:?}", accounts);

    let wei_conv: U256 = U256::exp10(18);
    for account in accounts {
        let balance = web3s.eth().balance(account, None).await?;
        println!(
            "Eth balance of {:?}: {}",
            account,
            balance.checked_div(wei_conv).unwrap()
        );
    }
    
    Ok(())
}

Modules

Let’s bring some things into scope:

use std::env;
use std::str::FromStr;

use web3::types::{Address, H160, U256};

Here we import std::env to read environment variables. The std::str::FromStr trait is imported so that Address::from_str can be used.

The web3::types we are importing are for processing / instancing smart contract addresses and handling return values.

Main function definition and WebSocket connection

The main function is annotated with #[tokio::main] because we need to run in an async runtime. The return value is a web3 result, since we are working with web3.

#[tokio::main]
async fn main() -> web3::Result<()> {
    dotenv::dotenv().ok();

    let websocket = web3::transports::WebSocket::new(&env::var("INFURA_RINKEBY").unwrap()).await?;
    let web3s = web3::Web3::new(websocket);

With line 9 we load variables from the .env file so that we can later retrieve the values using std::env::var(s). On line 11 we create a transports::WebSocket instance that will be used to establish the connection to the Ethereum network. Here the value of the INFURA_RINKEBY environment variable is retrieved, which should contain the full WebSocket endpoint URL. Finally, line 12 creates the Web3 instance.

Retrieve and add accounts

In this section, we retrieve a list of accounts, which will be an empty list initially, and add one (for example our metamask wallet).

    let mut accounts = web3s.eth().accounts().await?;
    accounts.push(H160::from_str(&env::var("ACCOUNT_ADDRESS").unwrap()).unwrap());
    println!("Accounts: {:?}", accounts);

We simply call the accounts() on .eth() to retrieve the list of accounts, and then on line 15 add an account address from a value we stored in the ACCOUNT_ADDRESS environment variable. The list is a vec of H160 so we instance this type from a string.

Printing the accounts should look something like this in the terminal:

Accounts: [0x4a3dxxxxxxxxxxxxxxxxxxxx]

Retrieve account balance

In this final part, we will retrieve the balance from our account(s), before moving on to interacting with smart contracts. Our balance will be represented in “Wei”. In fact, most if not all asset values on the Ethereum blockchain are represented in terms of Wei. It is the smallest denomination of ether. One ether = 1,000,000,000,000,000,000 Wei (1018). Because we are normally used to thinking in terms of Eth value, let’s first write a function that converts the Wei value to Eth:

fn wei_to_eth(wei_val: U256) -> f64 {
    let res = wei_val.as_u128() as f64;
    res / 1_000_000_000_000_000_000.0
}

On line 8 we convert the U256 to a f64 by first turning it into a u128. Because there is no function to covert to f64 directly on the U256 struct. Then we divide that number by 1_000_000_000_000_000_000.0, because 1 Eth is 1^18 Wei.

    for account in accounts {
        let balance = web3s.eth().balance(account, None).await?;
        println!("Eth balance of {:?}: {}", account, wei_to_eth(balance));
    }

We then loop through the list of accounts and call balance(), passing the account as a parameter to retrieve the account balance. To get the balance value in terms of Eth we use the conversion function we write just now. Then we print the balance to the terminal.

If all goes well, we should see our account’s address printed to the terminal together with the balance. We now know we have a working connection to the Rinkeby Ethereum test network.

We have completed the first section of our “connect to Ethereum with Rust Web3” project.

Let’s move on to using a smart contract.

Interacting with smart contracts

With smart contracts, the important concept to know is that smart contracts are code programs that run on the Ethereum network. All of these programs have an address and an application binary interface (ABI). We need to know these two pieces of information to be able to interact with a smart contract in our Rust code.

One of the tools to find the address and the interface of a smart contract or token is rinkeby.etherscan.io.

Finding the address and contract ABI

Let’s look up the address for the AAVE token as the first piece of information. Make sure to go to the Rinkeby URL for etherscan, so that we are searching on the test net.

search for aave

Clicking on the Aave Token (AAVE) result will navigate to the following page:

Here we see the contract address on the left: 0x42447d5f59d5bf78a82c34663474922bdf278162. If we click on the address we can go to the contract page:

AAVE contract information

The second piece of information we need, the ABI. We will find this in the contract details on the “Contract” tab. If we scroll down on that tab we will find the “Contract ABI” section:

Contract ABI section

Here is the entire ABI in JSON format:

[{"inputs":[{"internalType":"uint256","name":"_initialAmount","type":"uint256"},{"internalType":"string","name":"_tokenName","type":"string"},{"internalType":"uint8","name":"_decimalUnits","type":"uint8"},{"internalType":"string","name":"_tokenSymbol","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"constant":false,"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"allocateTo","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"src","type":"address"},{"internalType":"address","name":"dst","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]

We will put this JSON in a file called erc20_abi.json in the src directory of our project. We now have all the pieces of information to get information from the AAVE token smart contract.

Calling methods on the AAVE smart contract

We are now going to write the code that will retrieve the name of the token and get the total supply available.

First, we are going to update the imports section to include certain structs and types we will be needing. The updated lines are highlighted:

use std::env;
use std::str::FromStr;

use web3::contract::{Contract, Options};
use web3::types::{Address, H160, U256};

Add the following lines below the code for printing the account balance of our wallet we wrote earlier:

    let aave_addr = Address::from_str("0x42447d5f59d5bf78a82c34663474922bdf278162").unwrap();
    let token_contract =
        Contract::from_json(web3s.eth(), aave_addr, include_bytes!("erc20_abi.json")).unwrap();

    let token_name: String = token_contract
        .query("name", (), None, Options::default(), None)
        .await
        .unwrap();

    let total_supply: U256 = token_contract
        .query("totalSupply", (), None, Options::default(), None)
        .await
        .unwrap();

    println!("Token name: {}, total supply: {}", token_name, total_supply);

First, on line 28 we create an Address object from the AAVE token contract address we found earlier. Then we use that Address object and our Web3 instance (web3s) to instance a Contract object from the JSON ABI. The Contract object will allow us to call functions on the smart contract.

To get the token name, we call the “name” function from the contract using the query() function on the contract instance. Let’s look at the JSON interface definition for this “name” function:

{
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [{
        "internalType": "string",
        "name": "",
        "type": "string"
    }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
}

There are no inputs, and there is one output that is of type "string". The output type is important as we have to explicitly set the type of the variable we are assigning the output to. So the entire call looks like:

let token_name: String = token_contract
        .query("name", (), None, Options::default(), None)
        .await
        .unwrap();

The first parameter is the function name, then the parameters of which we have none, the from address (not needed for this call), for options we can set to default, and then finally the block id, which also isn’t needed for this call.

In a similar way we can get the total supply of the token in terms of Wei, first the JSON definition of the function:

{
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
    }],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
}

Note that the type is different since we are dealing with a number. Calling this function with our code is almost the same as with the “name” function:

let total_supply: U256 = token_contract
        .query("totalSupply", (), None, Options::default(), None)
        .await
        .unwrap();

The big difference here is obviously the function name "totalSupply" and the output type for the value: U256, which can hold a very large number.

Printing out the retrieved information to the terminal looks like this:

Token name: Aave Token, total supply: 22942961516902797000

The full main.rs code listing looks like this:

use std::env;
use std::str::FromStr;

use web3::contract::{Contract, Options};
use web3::types::{Address, H160, U256};

fn wei_to_eth(wei_val: U256) -> f64 {
    let res = wei_val.as_u128() as f64;
    res / 1_000_000_000_000_000_000.0
}

#[tokio::main]
async fn main() -> web3::Result<()> {
    dotenv::dotenv().ok();

    let websocket = web3::transports::WebSocket::new(&env::var("INFURA_RINKEBY").unwrap()).await?;
    let web3s = web3::Web3::new(websocket);

    let mut accounts = web3s.eth().accounts().await?;
    accounts.push(H160::from_str(&env::var("ACCOUNT_ADDRESS").unwrap()).unwrap());
    println!("Accounts: {:?}", accounts);

    for account in accounts {
        let balance = web3s.eth().balance(account, None).await?;
        println!("Eth balance of {:?}: {}", account, wei_to_eth(balance));
    }

    let aave_addr = Address::from_str("0x42447d5f59d5bf78a82c34663474922bdf278162").unwrap();
    let token_contract =
        Contract::from_json(web3s.eth(), aave_addr, include_bytes!("erc20_abi.json")).unwrap();

    let token_name: String = token_contract
        .query("name", (), None, Options::default(), None)
        .await
        .unwrap();

    let total_supply: U256 = token_contract
        .query("totalSupply", (), None, Options::default(), None)
        .await
        .unwrap();

    println!("Token name: {}, total supply: {}", token_name, total_supply);

    Ok(())
}

Conclusion

What have we learned in our connect to Ethereum network using Rust Web3 project? We learned how to connect to the Ethereum (test) network, and retrieve the balance of an account. Last but not least, we looked at the basics of interacting with a smart contract, and how to find the required information using etherscan.io.

For the full code for this project go to my GitHub: rust-web3-basics-tutorial.

Please also check out the follow-up article where we learn how to swap tokens on Uniswap V2 and this follow-up article with the same theme as the current one: Rust Web3 token transactions from blocks: how to

Please follow me on Twitter to get updates on more Web3 and cryptocurrency programming related articles:

Leave a Reply

Your email address will not be published. Required fields are marked *