Guide: Subscribing to the Latest Ethereum Blocks for Automated Trading in Rust
Block Subscriptions Allow for the Latest Finalized Ethereum Blocks
In the realm of cryptocurrency, the term "Sniper Bot" frequently surfaces. But what exactly does it entail? A Sniper Bot functions to execute buy or sell orders for tokens automatically, based on preset prices or specific conditions. These automated systems possess a swiftness that surpasses human capabilities, enabling them to exploit market shifts or seize opportunities with newly introduced tokens ahead of others.
Frequently, you’ll come across mention of a sniper bot swiftly purchasing tokens upon their launch, often acquiring substantial amounts for a fraction of their actual value. Take, for instance, the individual who invested $27 in PEPE tokens, which skyrocketed to $1 million in value just days later. However, it's important to note that if a Sniper Bot consistently snatches up all tokens immediately upon launch, it can lead to issues. Some tokens may interact with malicious contracts or face potential rug pulls before the bot can execute its selling conditions.
In the publication, we’ll evaluate and implement the start of an Ethereum Sniper Bot.
Design Considerations
The foundation of any good trading bot must consider the following:
The programming language of the program
How the program obtains the latest block information
Programming Language
The Ethereum mainnet averages a block time of 10 to 15 seconds at the time of writing. Python is favored for its simplicity and ease of use in developing quick, functional scripts. However, the speed benefits of languages like Rust surpass the ease of learning Python. In the fast-paced trading environment, this speed is crucial, as a few seconds can decide whether a transaction is included in the next block or a later one.
Getting started with Rust is easy and information is plentiful on the internet for those interested in learning.
Obtaining the Latest Block Transactions
Time is critical. For an Ethereum Sniper bot, accessing the latest block information and transactions quickly is essential. For example, if the bot employs a non-optimized method that retrieves block data 30 seconds after finalization, it will base its decisions on information that even a person monitoring actively could obtain.
The two main techniques for acquiring the latest block information are subscription and polling methods. In subscription methods, the web socket RPC delivers the latest block information as soon as a block is finalized, while in polling methods, the program periodically requests the latest block information. Subscription methods, which reduce the number of RPC calls compared to polling, will be used initially by the bot to evaluate finalized blocks.
Getting Started with Rust
Rust is a programming language known for its memory safety and performance. Information may be found
Setting up a development environment for Rust is very straightforward. You can follow the guide provided here. All examples will use Unix-based environments, such as Ubuntu or macOS.
A new Rust environment is easy to create:
cargo init eth-sniper-rs
cd eth-sniper-rs
By default, the program or crate includes a “Hello World” example with the main function located at ./src/main.rs
. When compiling and running the crate with cargo run
, the crate will execute with “Hello World!” printing to the terminal.
>> 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
Manifest File for Rust's Package Manager: Cargo.toml
Rust dependencies are specified in the Cargo.toml
file. These dependencies are packages that the Rust program will install, import, and utilize. To update your project's dependencies, update the dependencies in the Cargo.toml
file as follows:
[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 but will be utilized in future tutorials.
Environmental File: .env
The .env
file is known as the environment file. The dotenv
package reads this file and sets the variables defined within it for the environment when the program is run. Create a .env file and populate it with the following where <websocket rpc endpoint> is the Websocket RPC Endpoint such as Quicknode, Alchemy, or self-hosted.
Ideally, hosting an Ethereum node locally would be beneficial to minimize the delay between RPC calls and their responses.
RUST_LOG=info
ETH_RPC_WSS="<websocket rpc endpoint>"
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 utility functions useful to obtaining the Unix time and calculating the times.
// 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("ETH_RPC_WSS")?).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("ETH_RPC_WSS")?).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
In our publication, we've developed a basic framework for an Ethereum Sniper Bot that subscribes to blocks as they are finalized. However, creating an efficient and effective Sniper Bot involves significant enhancements. These improvements include but are not limited to processing pending transactions, analyzing the finalized block to pinpoint and identify tokens of interest, strategies to select targets while avoiding potentially malicious contracts, identifying conditions that trigger buying and selling of tokens, and executing the token swaps.
In future publications, we will delve deeper into the decision-making and implementation of these features. The crate will be designed to support various trading strategies, including Reinforcement Learning and Long Short-Term Memory (LSTM) methods.
Happy programming!