Rust ethers-rs: How to send transactions

Hello rustafarians !

In this support tip we’ll look at how to send both a Legacy and a EIP-1559 transaction in Rust and using the ethers-rs library.
We’ll use Sepolia so make sure to have some test ETH - if you don’t, no worries, head over to https://www.infura.io/faucet/sepolia

Before jumping into it, whoever is new to Rust make sure to:

  1. Install Rust from Installation - The Cargo Book

  2. Create a new project with cargo new infura_rs (reference First Steps with Cargo - The Cargo Book)

  3. Now let’s edit the Cargo.toml file and add these dependencies:

[dependencies]
ethers = "2.0"
eyre = "0.6.8"
hex = "0.4.3"
tokio = { version = "1.28.2", features = ["full"] }
serde_json = "1.0.96"
  1. Edit the src/main.rs file and add the following code:
use ethers::{
    core::{types::TransactionRequest},
    middleware::SignerMiddleware,
    providers::{Http, Middleware, Provider},
    signers::{LocalWallet, Signer},
    utils,
    prelude::*
};
use eyre::Result;
use std::convert::TryFrom;

#[tokio::main]
async fn main() -> Result<()> {

    // connect to the network
    let provider = Provider::<Http>::try_from("https://sepolia.infura.io/v3/INFURA_API_KEY")?;

    let chain_id = provider.get_chainid().await?;

    // define the signer
    // for simplicity replace the private key (without 0x), ofc it always recommended to load it from an .env file or external vault
    let wallet: LocalWallet = "SIGNER_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(chain_id.as_u64());

    let to_address = "<to_address_goes_here>";

    // connect the wallet to the provider
    let client = SignerMiddleware::new(provider, wallet);

   // craft the transaction
   // it knows to figure out the default gas value and determine the next nonce so no need to explicitly add them unless you want to 
   let tx = TransactionRequest::new()
        .to(to_address)
        .value(U256::from(utils::parse_ether(0.01)?));

   // send it!
   let pending_tx = client.send_transaction(tx, None).await?;

   // get the mined tx
   let receipt = pending_tx.await?.ok_or_else(|| eyre::format_err!("tx dropped from mempool"))?;
   let tx = client.get_transaction(receipt.transaction_hash).await?;

   println!("Sent tx: {}\n", serde_json::to_string(&tx)?);
   println!("Tx receipt: {}", serde_json::to_string(&receipt)?);

   Ok(())
}
  1. Compile and run it with cargo run - you should see a similar output:
Sent tx: {"hash":"0xb4...","nonce":"0xa",...,"type":"0x0","chainId":"0xaa36a7"}
Tx receipt: {"transactionHash":"0xb4...",...,"type":"0x0","effectiveGasPrice":"0xcbe0"}

Notice that you’ve just sent a legacy tx ("type":"0x0") - so how to send an EIP-1559 tx ("type":"0x2") ?
That’s a fairly easy change, the TransactionRequest would become Eip1559TransactionRequest, updated code below:

use ethers::{
    core::{types::TransactionRequest},
    middleware::SignerMiddleware,
    providers::{Http, Middleware, Provider},
    signers::{LocalWallet, Signer},
    utils,
    prelude::*
};
use eyre::Result;
use std::convert::TryFrom;
use types::Eip1559TransactionRequest;

#[tokio::main]
async fn main() -> Result<()> {

    // connect to the network
    let provider = Provider::<Http>::try_from("https://sepolia.infura.io/v3/INFURA_API_KEY")?;

    let chain_id = provider.get_chainid().await?;

    // define the signer
    // for simplicity replace the private key (without 0x), ofc it always recommended to load it from an .env file or external vault
    let wallet: LocalWallet = "SIGNER_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(chain_id.as_u64());

    let to_address = "<to_address_goes_here>";

    // connect the wallet to the provider
    let client = SignerMiddleware::new(provider, wallet);

   // craft the transaction
  // this also knows to estimate the `max_priority_fee_per_gas` but added it manually just to see how it would look
   let tx = Eip1559TransactionRequest::new()
        .to(to_address)
        .value(U256::from(utils::parse_ether(0.01)?))
        .max_priority_fee_per_gas(U256::from(2000000000_u128)); // 2 Gwei

   // send it!
   let pending_tx = client.send_transaction(tx, None).await?;

   // get the mined tx
   let receipt = pending_tx.await?.ok_or_else(|| eyre::format_err!("tx dropped from mempool"))?;
   let tx = client.get_transaction(receipt.transaction_hash).await?;

   println!("Sent tx: {}\n", serde_json::to_string(&tx)?);
   println!("Tx receipt: {}", serde_json::to_string(&receipt)?);

   Ok(())
}
5 Likes