Efficient Real‑Time Ethereum Block Retrieval in Rust

Efficient Real‑Time Ethereum Block Retrieval in Rust

Efficient Real‑Time Ethereum Block Retrieval in Rust
Sniper bots require efficient real-time on-chain transaction data

In the crypto world, you’ll often hear the phrase “Sniper Bot.” But what’s it actually doing? Think of it as a computer program that automatically places buy or sell orders for tokens, all with the goal of making a profit. Because it’s automated, it can react in milliseconds and much faster than a human trader can. That speed lets the bot catch tiny market moves or hop on brand‑new token launches before anyone else has a chance.

You’ll often hear about a “sniper bot” swooping in to buy tokens right after they launch, scooping up huge amounts for a tiny price. A classic example is someone who spent just $27 on PEPE tokens and watched their value explode to about $1 million within days. But when a sniper bot grabs all the tokens the moment they hit the market, problems can arise. If the token is backed by a malicious contract or is a potential rug‑pull, the bot might still buy in before it can sell and put it all at risk.

Let's take a look at the implementation of an Ethereum Sniper Bot.

Design Considerations

The foundation of any good trading bot must consider the following (but not limited to, of course):

  • The programming language of the program
  • How the program obtains the latest block information

Programming Language

On the Ethereum mainnet, blocks are added roughly every 12 seconds. While many developers are drawn towards Python because it’s straightforward and great for quick prototypes, the raw speed of compiled languages such as Rust can make a real difference. In high‑frequency trading, even a handful of seconds can be the difference between a trade getting confirmed in the next block or slipping into a later one.

Getting started with Rust is surprisingly simple, and there’s a wealth of tutorials, guides, and community resources online for anyone ready to dive in.

Obtaining the Latest Block Transactions

Time is everything. An Ethereum sniper bot can’t afford to be slow, especially if it only pulls block data 30 seconds after finalization. It would be making decisions on stale information that even a human watcher would have seen minutes earlier. 

There are two ways to stay up‑to‑date with the chain:

  1. Subscription – A WebSocket RPC streams the newest block as soon as it’s finalized. 
  2. Polling – The program repeatedly queries the node for the latest block.

Because polling generates a lot of extra RPC traffic, a bot usually starts with a subscription to keep the call count low while still catching every finalized block in real time (roughly).

Getting Started with Rust

Rust is celebrated for its blend of memory safety and high performance. If you want to see the concrete speed gains, take a look at this deep dive.
Getting a Rust development environment up and running is straightforward:

Creating a new Rust project is as simple as:

cargo init eth-sniper-rs
cd eth-sniper-rs

By default, a new Rust crate comes pre‑loaded with a simple “Hello World” example. The main entry point lives in ./src/main.rs. Running cargo run will result in:

cargo run
   Compiling eth-sniper-rs v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/eth-sniper-rs`
Hello, world!

Configuring the Environment

The Cargo.toml – Rust’s Dependency Manifest

Cargo.toml is the central configuration file for Cargo, Rust’s build and package manager.
Inside this file you list every crate your project needs. Cargo will fetch, compile, and link them for you.
When you want to add or update a dependency, simply edit the relevant section of Cargo.toml and run cargo build again.

[dependencies]
ethers = { version = "2.0", features = ["ws"] }
# Ethers' async features rely upon the Tokio async runtime.
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
eyre = "0.6"
ethers-core = "2.0.14"
log = "0.4.21"
dotenv = "0.15.0"
env_logger = "0.11.3"
chrono = "0.4.38"

Note: The tokio package will be installing the multi-threaded features and will not be used in the current example.

Environmental File: .env

The .env file holds environment variables that the dotenv package loads at runtime.
Create a file named .env in your project root and add your RPC endpoint (e.g. QuicknodeAlchemy, or self-hosted):

RUST_LOG=info
WS_RPC_ENDPOINT="<websocket rpc endpoint>"

Running the app will automatically read this file and expose WS_RPC_ENDPOINT to the process.

Tip: Running an Ethereum node locally cuts the latency of RPC calls, giving your bot a competitive edge.

Ethereum Sniper Main Source File

In the tutorial, all of the source code will be hosted in the ./src/main.rs.

Crate Imports

The first section of the main.rs contains the crate’s imports and usages of other crates.

use chrono::{DateTime, Utc};
use dotenv::dotenv;
use ethers::providers::{Middleware, Provider, StreamExt, Ws};
use ethers::types::U64;
use eyre::Result;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;

Utility Functions

We will define useful utility functions such as a function that obtains the current Unix time and calculating the relative time to an input timestamp.

// obtain the current unix time
pub fn get_unix_time() -> u64 {
    match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
        Ok(n) => n.as_secs(),
        Err(_) => panic!("SystemTime before UNIX EPOCH!"),
    }
}

// obtain the time between the current timestamp and the timestamp used in the call
pub fn get_delta_time(timestamp: u64) -> i64 {
    let curr_time = get_unix_time() as i64;
    return curr_time - timestamp as i64;
}

Main Function

The main function is the entry point that is invoked when compiling and running the crate. We’ll begin with the basic main function shown below and expand on it progressively.

Base Main Function

This main function will execute asynchronously and will require the use of Tokio for handling ethers with multi-threading in upcoming implementations. The use of Result<()> facilitates the unpacking of results and options.

dotenv().ok() - Read the .env file and set the environmental variable

env_logger::init(); - Initialize the environmental logger

Ok(()) - Upon completion of the main function, Ok(()) is returned and satisfies the criteria of Result<()>.

#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
async fn main() -> Result<()> {
    dotenv().ok();
    env_logger::init();
    
    Ok(())
}

Websocket RPC Endpoint

A connection to the websocket RPC endpoint is created utilizing std::Arc also known as “ARC” or “Atomically Reference Counted”. Arc allows for a thread-safe reference-counting pointer for the endpoint as below:

let client: Arc<Provider<Ws>> = Arc::new(Provider::<Ws>::connect(std::env::var("WS_RPC_ENDPOINT")?).await?);
Note: The usage of the ? operator to unpack the environmental variable and the creation of client for the endpoint.

Block Subscription

The websocket RPC client allows for several methods including the subscribe_blocks method equivalent to the block subscription, or SubscribeNewHead method.

let mut block_stream = client.subscribe_blocks().await?;

Receiving New Blocks from the Subscription

New blocks are received from the block subscription with the following while loop.

while let Some(block) = block_stream.next().await {
        // process the new block
    }

Updated Main Function

Incorporating all the components previously described, the updated main function is as follows:

#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
async fn main() -> Result<()> {
    dotenv().ok();
    env_logger::init();
    // initialize defaults
    let mut start_time: DateTime<Utc>;
    let mut process_time: Duration;
    let mut block_age: i64;
    let mut block_number: U64;

    log::info!("Starting Ethereum Sniper Bot!");
    log::info!("Connecting to the Websocket RPC!");
    let client: Arc<Provider<Ws>> =
        Arc::new(Provider::<Ws>::connect(std::env::var("WS_RPC_ENDPOINT")?).await?);

    // create subscription stream
    log::info!("Subscribing to New Blocks!");
    let mut block_stream = client.subscribe_blocks().await?;
    log::info!("Evaluating Blocks from the Subscription!");
    while let Some(block) = block_stream.next().await {
        start_time = Utc::now();
        block_number = block.number.unwrap();
        block_age = get_delta_time(block.timestamp.low_u64() as u64);
        // insert block processing here
        // calculate the time of the processing
        process_time = Utc::now()
            .signed_duration_since(start_time)
            .to_std()
            .unwrap();
        log::info!(
            "[{}] Age: {}s - Process: {} ms",
            block_number,
            block_age,
            process_time.as_millis(),
        );
    }
    Ok(())
}

Compiling and Executing the Crate

Compiling and executing the crate using cargo run --release generates a client that connects to the websocket RPC endpoint with a block subscription, receiving blocks within 1 to 2 seconds after they are finalized, as demonstrated below:

 cargo run --release
   Compiling eth-sniper-rs v0.1.0 
    Finished `release` profile [optimized] target(s) in 1.41s
     Running `target/release/eth-sniper-rs`
[INFO  eth_sniper_rs] Starting Ethereum Sniper Bot!
[INFO  eth_sniper_rs] Connecting to the Websocket RPC!
[INFO  eth_sniper_rs] Subscribing to New Blocks!
[INFO  eth_sniper_rs] Evaluating Blocks from the Subscription!
[INFO  eth_sniper_rs] [19800914] Age: 2s - Process: 0 ms
[INFO  eth_sniper_rs] [19800915] Age: 1s - Process: 0 ms
[INFO  eth_sniper_rs] [19800916] Age: 2s - Process: 0 ms
[INFO  eth_sniper_rs] [19800917] Age: 2s - Process: 0 ms
...

Conclusion

We’ve just put together a bare‑bones skeleton for an Ethereum Sniper Bot that watches for new blocks as they get finalized.
Making that skeleton into a real‑world trading tool is a whole other story. To be truly competitive, the bot must:

  • Scan the mempool for pending transactions
  • Inspect each finalized block to spot the tokens that fit its criteria
  • Apply filters to avoid contracts that look suspicious or malicious
  • Decide when a token is a good buy or sell based on price moves, liquidity, or other signals
  • Execute the token swap as fast as possible

In the next posts we’ll dive into those decision layers and show how to wire them into the bot. The framework will be built to accommodate a variety of strategies, from classic rule‑based logic to machine‑learning approaches like Reinforcement Learning and Long‑Short‑Term‑Memory (LSTM) networks.

Subscribe to Wumpus World

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe