BNB Smart Chain

BNB Smart Chain

Rust

For Rust developers, the rust-web3 crate is the established way to interact with BNB Smart Chain, exposing JSON-RPC calls as ordinary async functions over a typed client. Its whole API is asynchronous, so it leans on the Tokio runtime to drive futures to completion. To get started, declare two dependencies in Cargo.toml: tokio = { version = "1", features = ["full"] } for the async runtime and web3 = "0.19" for the client itself. That pairing is enough to read balances and send transactions on chain 56.

The Tokio dependency is not optional: rust-web3's futures need an executor, and tokio with the full feature set provides the timers, I/O, and multi-threaded scheduler they expect. Pin both crates explicitly in Cargo.toml, tokio at version "1" with features = ["full"] plus web3 at "0.19", so your BSC client builds reproducibly.

Web3-rs

Client setup is a two-stage move. First build the HTTP transport, web3::transports::Http::new("https://bsc.therpc.io/YOUR_API_KEY"), then wrap it with web3::Web3::new(transport) to get the high-level client bound to BNB Smart Chain. Every network method returns a Result, which suits Rust idioms nicely: inside an async function you propagate failures upward with the ? operator, so a balance read on chain 56 either yields a value or short-circuits with a typed error.

use web3::Web3;
use web3::types::{Address, H256, TransactionParameters, U256};
use std::str::FromStr;
struct EthereumClient {
web3: Web3<web3::transports::Http>,
}
impl EthereumClient {
pub async fn new(url: &str) -> Result<Self, web3::Error> {
let transport = web3::transports::Http::new(url)?;
let web3 = Web3::new(transport);
Ok(Self { web3 })
}
pub async fn get_balance(&self, address: &str) -> Result<U256, web3::Error> {
let address = Address::from_str(address)
.map_err(|_| web3::Error::InvalidAddress)?;
self.web3.eth().balance(address, None).await
}
pub async fn send_transaction(
&self,
from: Address,
to: Address,
value: U256,
) -> Result<H256, web3::Error> {
let tx = TransactionParameters {
to: Some(to),
value,
from,
..Default::default()
};
self.web3.eth().send_transaction(tx).await
}
}
  • GitHub: https://github.com/tomusdrw/rust-web3
  • Docs: https://docs.rs/web3
  • Fully async API driven by the Tokio runtime
  • Type-safe interfaces backed by Rust's strong typing
  • Contract interaction and transaction management on chain 56
  • ENS resolution plus WebSocket transports for live BSC subscriptions

Contract Integration

To work with a deployed contract, load its ABI through Contract::from_json, supplying the client, the on-chain address, and the ABI bytes. That binds a typed handle to, say, a BEP-20 token on chain 56. The crate then separates the two access patterns by method: contract.query() runs a read-only eth_call and decodes the return value, while contract.call() submits a state-changing transaction to BNB Smart Chain that costs gas and must be mined.

use web3::contract::{Contract, Options};
use web3::types::{Address, U256};
struct SmartContract {
contract: Contract<web3::transports::Http>,
}
impl SmartContract {
pub async fn new(
web3: Web3<web3::transports::Http>,
address: Address,
abi: &[u8],
) -> Result<Self, web3::Error> {
let contract = Contract::from_json(
web3.eth(),
address,
abi,
)?;
Ok(Self { contract })
}
pub async fn call_method<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: &[web3::contract::tokens::Tokenize],
) -> Result<T, web3::Error> {
let result = self.contract.query(
method,
params,
None,
Options::default(),
None,
).await?;
Ok(result)
}
}

Error Handling

Idiomatic Rust error handling for BSC starts with thiserror: derive a custom error enum whose variants wrap web3::Error alongside your own cases, so callers match on meaningful kinds instead of stringly-typed failures. On top of that, expose convenience wrappers like safe_get_balance and safe_send that handle the unit conversion for you, turning a raw U256 wei amount into an f64 BNB figure at the boundary. Application code then deals in human-scale numbers while the precise integer math stays internal to chain-56 calls.

use thiserror::Error;
#[derive(Error, Debug)]
pub enum EthereumError {
#[error("Web3 error: {0}")]
Web3Error(#[from] web3::Error),
#[error("Invalid address: {0}")]
InvalidAddress(String),
#[error("Contract error: {0}")]
ContractError(String),
#[error("Transaction failed: {0}")]
TransactionError(String),
}
impl EthereumClient {
pub async fn safe_get_balance(&self, address: &str) -> Result<f64, EthereumError> {
let wei = self.get_balance(address).await?;
let eth = wei.as_u128() as f64 / 1e18;
Ok(eth)
}
}

Async Event Handling

Live block tracking via subscribe_new_heads returns a Stream of headers, and streaming pushes data the HTTP transport cannot deliver, so this path requires a WebSocket transport pointed at the wss:// form of your BSC endpoint. Pull futures::StreamExt into scope to get the combinators you need, then drain the stream in a while let Some(header) = stream.next().await loop, reacting to each new chain-56 head as it arrives.

use futures::StreamExt;
impl EthereumClient {
pub async fn monitor_blocks(&self) -> Result<(), web3::Error> {
let mut stream = self.web3.eth_subscribe().subscribe_new_heads().await?;
while let Some(block) = stream.next().await {
match block {
Ok(header) => {
println!("New block: {}", header.number.unwrap_or_default());
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
}
Ok(())
}
}

Testing

Because the client is async, your tests must run on the runtime too — annotate them with #[tokio::test] so each async test function gets its own executor. Just as important, aim tests at a local node such as http://localhost:8545 or a forked instance of BNB Smart Chain rather than the production endpoint; a test suite that pounds the live chain-56 URL wastes your request budget and can produce flaky results when network conditions shift.

#[cfg(test)]
mod tests {
use super::*;
use tokio;
#[tokio::test]
async fn test_balance_query() {
let client = EthereumClient::new("http://localhost:8545")
.await
.expect("Failed to create client");
let balance = client
.get_balance("0x742d35Cc6634C0532925a3b844Bc454e4438f44e")
.await
.expect("Failed to get balance");
assert!(balance > U256::zero());
}
}

Ready to call this in production?

Free tier covers personal projects. Pay-as-you-go scales without a card.