Skip to main content

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://docs.metamask.io/developer-tools/faucet/sepolia

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

  • Install Rust from Installation - The Cargo Book

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

  • 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"
  • 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(())
}
  • 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(())
}