Rust - Ethereum Project [Basic]: Transactions, Accounts, and Balance
1. Why Do We Need This? (Problem Statement & Status Quo)
Most blockchain development tutorials rely heavily on high-level JavaScript frameworks like Hardhat, Python frameworks like Brownie, or Rust frameworks like Foundry. While these tools abstract away the complex underlying mechanics of Ethereum through scaffolding and CLI utilities, they often leave developers with a "black box" understanding of how wallets, JSON-RPC providers, accounts, transactions, and smart contract deployments actually work under the hood.
This project explores how to interact with the Ethereum ecosystem directly from Rust from scratch—without relying on high-level orchestration frameworks like Foundry or Hardhat. By building raw transaction dispatchers, account balancers, and contract interactions using low-level Rust crates (ethers-rs, tokio, clap), developers gain deep insight into the foundational mechanisms that power Ethereum nodes and client libraries.
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: The Automated Postal Dispatcher and Local Post Office
Think of interacting with an Ethereum blockchain programmatically as managing an automated mail dispatch and auditing system:
- Ganache (Local Ethereum Node) acts as your private, local postal facility (Post Office) running on your desk (
localhost). It doesn't touch the real-world global postal network; it's isolated, fast, and free to experiment with. - Mnemonic Phrase (Seed Phrase) acts as your master vault password or cryptographic key ring. From this 12-word master phrase, an infinite number of safe-deposit boxes (wallet accounts) can be deterministically generated.
- Provider acts as the postal courier window. You hand your envelope (transaction request) to the provider, who carries it across the network to the local post office (node) for processing.
- Transaction Receipt is the official certified return receipt stamped by the postal clerk, proving your letter arrived in a specific delivery batch (Block Number).
Rust Application (Client Code)
|
| 1. Generates wallet from Mnemonic phrase
| 2. Builds Transaction Request (from, to, value)
v
Provider (JSON-RPC Client)
|
| 3. Sends HTTP POST request (JSON-RPC call)
v
Ganache Instance (Local Ethereum Node on Localhost)
|
| 4. Executes transaction in EVM, mines block #1
v
Ganache returns Transaction Receipt to Rust App via Provider
3. Ecosystem Tooling Comparison
When building for Ethereum across different programming languages and stacks, developers choose from several established frameworks and low-level clients.
4. Step-by-Step Project Setup & Dependencies
To build our raw Rust Ethereum client, we configure our project environment, set up Cargo management extensions, and install essential cryptographic, asynchronous, and error-handling libraries.
Initializing the Project
Run the following commands in your terminal to create a new binary crate and install cargo-edit for seamless dependency management:
cargo new rust_eth_yt --bin
cd rust_eth_yt
cargo install cargo-edit
Configuring Cargo.toml
Open Cargo.toml and populate your dependencies. We use ethers for Ethereum interactions, tokio for async runtime execution, clap for command-line argument parsing, hex for encoding/decoding cryptographic byte arrays, and eyre for ergonomic error handling.
[package]
name = "rust_eth_yt"
version = "0.1.0"
edition = "2021"
authors = ["Akhil Sharma <akhil@example.com>"]
[dependencies]
ethers = "2.0"
tokio = { version = "1.0", features = ["full"] }
clap = { version = "4.0", features = ["derive"] }
hex = "0.4"
eyre = "0.6"
tracing = "0.1"
tracing-subscriber = "0.3"
5. Building the Core Transaction Module (simple_transactions.rs)
We implement a complete asynchronous routine that bootstraps a local Ganache instance, derives accounts from a deterministic mnemonic, queries account balances, and dispatches a native transaction.
Complete Source Code Implementation
Create src/simple_transactions.rs (or replace main.rs content for execution):
use std::time::Duration;
use ethers::prelude::*;
use ethers::utils::Ganache;
use eyre::Context;
use hex;
#[tokio::main]
async fn main() -> eyre::Result<()> {
// 1. Define standard deterministic mnemonic for local testing
let mnemonic = "test test test test test test test test test test test junk";
// 2. Spawn a local Ganache instance
let ganache = Ganache::new().mnemonic(mnemonic).spawn();
println!("HTTP Endpoint for Ganache: {}", ganache.endpoint());
// 3. Extract the first managed wallet from Ganache keys
let wallet: LocalWallet = ganache.keys()[0].clone().into();
let first_address = wallet.address();
println!("Wallet First Address: 0x{}", hex::encode(first_address));
// 4. Instantiate the Provider connected to the Ganache endpoint
let provider = Provider::<Http>::try_from(ganache.endpoint())?
.interval(Duration::from_millis(10));
// 5. Query the balance of the first address
let first_balance = provider.get_balance(first_address, None).await?;
println!("First Address Balance: {} Wei", first_balance);
// 6. Define and query another random Ethereum address
let other_address_str = "0x2325514f777E579F39b0391E5a2e5192318cfb7b";
let other_address: Address = other_address_str.parse()?;
let other_balance = provider.get_balance(other_address, None).await?;
println!("Balance for Address {}: {} Wei", other_address_str, other_balance);
// 7. Create and dispatch a transaction (transfer 1000 Wei from first address to other address)
let tx = TransactionRequest::new()
.to(other_address)
.value(1000)
.from(first_address);
// Note: In a production setting, sign the transaction with a signer middleware.
// Here we dispatch directly using the provider connected to the local node.
let pending_tx = provider
.send_transaction(tx, None)
.await
.context("Failed to send transaction")?;
println!("Pending transfer submitted... waiting for confirmation.");
// 8. Await transaction receipt and inspect mined block number
let receipt = pending_tx
.await
.context("Missing transaction receipt")?
.ok_or_else(|| eyre::eyre!("Transaction dropped or not found"))?;
let block_number = receipt
.block_number
.ok_or_else(|| eyre::eyre!("Cannot get block number from receipt"))?;
println!("Transaction mined successfully in block number: {}", block_number);
// 9. Verify updated balance of the recipient address
let updated_other_balance = provider.get_balance(other_address, None).await?;
println!("Updated Balance for Other Address: {} Wei", updated_other_balance);
Ok(())
}
6. Real-World Practical Pitfalls & Debugging Mindset
When executing low-level blockchain code in Rust, developers frequently encounter specific error classes:
- Typo and Identifier Mismatches in Macro Calls:
- Symptom: Compiler errors on macros like
tokio::mainor error-handling context helpers (.context("...")). - Fix: Ensure correct trait imports from prelude modules (
use ethers::prelude::*;) and verify crate features inCargo.toml(e.g., ensuringfullis enabled for Tokio).
- Symptom: Compiler errors on macros like
- Asynchronous Await Deadlocks (
.awaitomissions):- Symptom: Functions returning futures instead of executing network calls.
- Fix: Always append
.await?when querying providers or dispatching transactions, ensuring proper propagation witheyre::Result.
- Hex Encoding Discrepancies:
- Symptom: Passing raw byte arrays where string formatting or
0xprefixes are expected. - Fix: Use the
hex::encode()utility combined with explicit string formatting (0x{}) for human-readable terminal output.
- Symptom: Passing raw byte arrays where string formatting or
7. Complete Flow Walkthrough
When executing cargo run, the program executes the following precise sequential pipeline:
8. Summary and Key Takeaways
- Low-Level Control: Building Ethereum applications directly in Rust using
ethers-rsprovides complete visibility into transaction construction, signing, and JSON-RPC communication without framework abstraction. - Deterministic Key Derivation: Standard mnemonic seed phrases allow predictable and repeatable wallet and account generation across test networks and production environments.
- Asynchronous Execution with Tokio: Blockchain interactions are inherently I/O bound network requests; pairing Rust with
tokioasync runtimes enables non-blocking JSON-RPC calls and receipt polling. - Provider and Middleware Pattern: The
Providerabstraction acts as the primary communication gateway to Ethereum nodes, handling balance lookups, gas estimations, and transaction submissions. - Transaction Lifecycles: Transactions transition from raw request payloads to pending hashes, and finally to mined inclusion receipts containing verified block numbers.
- Error Ergonomics: Integrating error-handling crates like
eyreensures clean context propagation when network requests fail or transaction receipts cannot be resolved.