Ethereum
Rust
rust-web3 is the long-standing crate for talking to Ethereum from Rust, and it's what the examples here use. (The newer ethers-rs and alloy crates have taken over much new development, so check them if you're starting fresh — but rust-web3 still works and the patterns carry over.) It's built on Tokio, so the async runtime isn't optional: add tokio = { version = "1", features = ["full"] } alongside web3 = "0.19" in your Cargo.toml, and every call to chain 1 becomes an .await inside a Tokio task.
rust-web3 leans on Tokio for its async machinery, so the runtime is a hard dependency. Add tokio = { version = "1", features = ["full"] } and web3 = "0.19" to your Cargo.toml before connecting to Ethereum.
Web3-rs
Connecting is two steps: build an HTTP transport with web3::transports::Http::new(url) pointing at the Ethereum endpoint, then wrap it in Web3::new(transport). The transport constructor itself returns a Result, and so does every network call — eth().balance(...) gives you Result<U256, web3::Error>. Lean on the ? operator inside your async fn to propagate those errors up rather than unwrapping; a dropped connection or a bad address becomes a typed error your caller handles, not a panic mid-request.
- GitHub: https://github.com/tomusdrw/rust-web3
- Docs: https://docs.rs/web3
- Fully async, built on Tokio
- Type-safe interfaces with
U256,Address, andH256primitives - Contract interaction loaded from a Solidity ABI
- Transaction construction, signing, and submission
- ENS resolution for
.ethnames - WebSocket transport for streaming Ethereum subscriptions
Contract Integration
Contract::from_json takes the eth() handle, an address, and the ABI bytes, and returns a Contract bound to that deployment on Ethereum. The two verbs to keep straight: contract.query(...) runs a read as an eth_call and decodes the return into the Rust type you ask for — perfect for an ERC-20 balanceOf — while contract.call(...) submits a state-changing transaction and returns its hash. Mixing them up is the most common beginner slip; query never touches the chain's state, call always does.
Error Handling
Rather than pass web3::Error around everywhere, derive a domain error enum with thiserror and fold the library error in through #[from], as the EthereumError below does. That gives you one error type to match on, with variants for invalid addresses and failed transactions that read clearly in a Result. Pair it with safe_ wrappers that do the wei-to-ether conversion at the boundary: a balance comes back as U256 wei, and dividing by 1e18 into an f64 is fine for display — just remember that f64 loses precision, so never feed that float back into anything that signs or settles on Ethereum.
Async Event Handling
subscribe_new_heads hands back a Stream of block headers, which means you need the WebSocket transport — eth_subscribe doesn't ride HTTP, so connect over wss://ethereum.therpc.io/YOUR_API_KEY rather than the HTTPS URL. Bring futures::StreamExt into scope and drain the stream with while let Some(block) = stream.next().await, matching each item as Ok(header) or an error. Since Ethereum produces a header roughly every 12 seconds, the loop spends most of its time parked on .await rather than spinning.
Testing
Mark async tests with #[tokio::test] so each one gets its own runtime — a plain #[test] can't .await. Point those tests at a local node on http://localhost:8545 (anvil or a geth dev chain) or a mainnet fork, not the production endpoint. Hammering the live Ethereum connection from a test suite burns CUs on every CI run and makes assertions flaky, since real chain 1 state moves with each block; a forked or local chain stays deterministic.