Rust ethers-rs: How to send transactions
In this guide we'll look at how to send both a legacy and an EIP-1559 transaction in Rust 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 the MetaMask Developer faucet.
Before jumping into it, whoever is new to Rust make sure to:
-
Create a new project with
cargo new infura_rsreference) -
Now let's edit the
Cargo.tomlfile 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 transaction ("type":"0x0").
To send an EIP-1559 transaction ("type":"0x2"), the TransactionRequest would become Eip1559TransactionRequest. Here's how the code would change:
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
// it's 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 show 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
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(())
}