bnb-middleware-toolkit
v0.1.0
Published
Production-grade React hooks and middleware for building DApps on BNB Chain (BSC + opBNB)
Downloads
108
Maintainers
Readme
BNB Chain Middleware Toolkit
React hooks and utilities for building DApps on BNB Chain. Handles BSC's gas quirks, wallet connection, BEP-20/BEP-721 token interactions, opBNB L2 fees, and gasless transactions — so you don't have to build from scratch, just use the SDK and build your custom apps under BSC chain.
Supports BSC Mainnet (56), BSC Testnet (97), opBNB Mainnet (204), opBNB Testnet (5611).
Installation
npm install bnb-middleware-toolkitPeer dependencies — install these alongside the SDK if you don't have them:
npm install wagmi viem @tanstack/react-query reactQuick Start
1. Get a WalletConnect project ID
Go to cloud.walletconnect.com, create a free project, and copy the Project ID.
2. Set up providers
All wagmi hooks run on the client. In Next.js App Router, create a providers wrapper with 'use client' and import it into your root layout:
// app/providers.tsx
'use client'
import { createBnbConfig, Web3Provider, BscMiddlewareProvider } from 'bnb-middleware-toolkit'
import type { ReactNode } from 'react'
const config = createBnbConfig({
walletConnectProjectId: 'your-walletconnect-project-id',
chains: ['bsc', 'bscTestnet'], // also supports 'opBNB', 'opBNBTestnet'
})
export function Providers({ children }: { children: ReactNode }) {
return (
<Web3Provider config={config}>
<BscMiddlewareProvider>
{children}
</BscMiddlewareProvider>
</Web3Provider>
)
}// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}For Vite / Create React App, wrap your app root directly — no 'use client' needed.
3. Send BNB
'use client'
import { useBscTransaction, useEnsureBscChain } from 'bnb-middleware-toolkit'
export function SendBnb() {
const { send, status, txHash, error } = useBscTransaction()
const { isWrongChain, switchToBsc } = useEnsureBscChain()
if (isWrongChain) {
return <button onClick={switchToBsc}>Switch to BSC</button>
}
return (
<div>
<button
disabled={status === 'pending' || status === 'mining'}
onClick={() => send({ to: '0xRecipientAddress', valueEther: '0.01' })}
>
{status === 'idle' ? 'Send 0.01 BNB' : status}
</button>
{txHash && (
<a href={`https://bscscan.com/tx/${txHash}`} target="_blank" rel="noreferrer">
View on BscScan
</a>
)}
{error && <p style={{ color: 'red' }}>{error.userMessage} — {error.suggestion}</p>}
</div>
)
}4. BEP-20 token transfer
'use client'
import { useTokenTransfer, useTokenBalance } from 'bnb-middleware-toolkit'
const TOKEN = '0xTokenContractAddress'
export function TokenSend() {
const { formatted, isLoading } = useTokenBalance(TOKEN)
const { transfer, status, transferHash, error } = useTokenTransfer(TOKEN)
// useTokenTransfer handles approve → transfer as one flow.
// If the wallet hasn't approved the token yet, it requests approval first,
// waits for confirmation, then submits the transfer — all tracked via `status`.
return (
<div>
<p>Balance: {isLoading ? '...' : formatted}</p>
<button
disabled={status !== 'idle'}
onClick={() => transfer({ to: '0xRecipientAddress', amount: '10' })}
>
Send 10 tokens
</button>
<p>Status: {status}</p>
{transferHash && <p>Tx: {transferHash}</p>}
{error && <p style={{ color: 'red' }}>{error.userMessage}</p>}
</div>
)
}SDK vs Demo App
This repository contains both the SDK and a Next.js demo app that uses it.
| | Location | Shipped to consumers |
|---|---|---|
| SDK | src/lib/, src/hooks/, src/providers/, src/index.ts | Yes — compiled to dist/ |
| Demo app | src/app/, src/components/ | No — runs locally only |
Everything exported from src/index.ts is the public API. The demo app (src/app/, src/components/) is not bundled or exported — it exists to prove the SDK works end-to-end against real wallets and testnets.
Hook API Reference
createBnbConfig(options)
Creates a wagmi config pre-wired for BNB Chain. Pass it to <Web3Provider>.
import { createBnbConfig } from 'bnb-middleware-toolkit'
const config = createBnbConfig({
walletConnectProjectId: 'your-id',
chains: ['bsc', 'bscTestnet', 'opBNB', 'opBNBTestnet'], // pick any subset
rpcUrls: {
bsc: 'https://your-private-rpc.nodereal.io/...', // optional — overrides public endpoints
},
})For production use, provide a private RPC via rpcUrls (NodeReal, Ankr, QuickNode). The defaults are public endpoints and rate-limited.
useEnsureBscChain()
Detects whether the connected wallet is on a BNB chain and exposes switch utilities.
const {
isWrongChain, // boolean — true if not on any BNB chain
isSwitching, // boolean — switch in progress
switchToBsc, // () => void — switch to BSC mainnet (56)
switchToTestnet, // () => void — BSC testnet (97)
switchToOpBnb, // () => void — opBNB mainnet (204)
switchToOpBnbTestnet // () => void — opBNB testnet (5611)
} = useEnsureBscChain()If the target chain is not in the user's wallet, wagmi automatically sends wallet_addEthereumChain before switching — no manual "Add Network" required.
useBscTransaction()
Sends BNB with BSC-correct gas handling. Always sets an explicit gasPrice (+10% buffer) and gasLimit (+20% buffer) — never relies on wallet defaults, which can misapply EIP-1559 on BSC.
const { send, status, txHash, error, reset } = useBscTransaction()
await send({ to: '0x...', valueEther: '0.01' })| Field | Type | Description |
|---|---|---|
| send | (params) => Promise<void> | Submit the transaction |
| status | TxStatus | idle \| pending \| mining \| confirmed \| failed |
| txHash | `0x${string}` \| undefined | Hash once submitted |
| error | ParsedBscError \| null | Structured error — see parseBscError |
| reset | () => void | Reset to idle |
useBscGasEstimate({ to, value, enabled? })
Live gas cost preview before the user submits. Re-fetches every 4 seconds.
const { estimate, isLoading } = useBscGasEstimate({
to: '0x...',
value: parseEther('0.01'),
})
console.log(estimate?.estimatedCostBnb) // e.g. "0.000063"
console.log(estimate?.gasLimit) // bigint
console.log(estimate?.gasPrice) // bigintuseTokenBalance(tokenAddress)
BEP-20 balance for the connected wallet.
const { rawBalance, formatted, decimals, isLoading, refetch } = useTokenBalance('0x...')
// formatted → "1234.56"
// rawBalance → 1234560000000000000000nuseTokenMetadata(tokenAddress)
Fetches name, symbol, decimals, totalSupply in one multicall.
const { metadata, isLoading } = useTokenMetadata('0x...')
// metadata.name, metadata.symbol, metadata.decimals
// metadata.formattedTotalSupply → "1,000,000,000"useTokenApproval({ tokenAddress, spenderAddress, amount })
Checks the current allowance and exposes an approval flow. Use this when a contract (e.g. a DEX router) needs to move tokens on the user's behalf.
const { needsApproval, requestApproval, isApproving, isApproved } = useTokenApproval({
tokenAddress: '0x...',
spenderAddress: '0x...',
amount: parseUnits('100', 18),
})
if (needsApproval) requestApproval() // approves MAX_UINT256, tracks confirmationuseTokenTransfer(tokenAddress, spenderAddress?)
Full approve-then-transfer flow in one hook. Pass spenderAddress when a contract executes the transfer (DEX, vault). Omit for wallet-to-wallet — skips the approval step entirely.
const { transfer, status, transferHash, approvalHash, error, reset } = useTokenTransfer(
'0x...', // token contract
'0x...', // spender — omit for direct wallet transfers
)
transfer({ to: '0x...', amount: '100' })| Status | Meaning |
|---|---|
| idle | Ready |
| approving | Approval tx pending in wallet |
| approval-mining | Waiting for approval confirmation |
| transferring | Transfer tx pending in wallet |
| transfer-mining | Waiting for transfer confirmation |
| confirmed | Done |
| failed | Error at any step — check error |
useNFTBalance(contractAddress)
BEP-721 collection balance and metadata for the connected wallet.
const { info, isLoading } = useNFTBalance('0x...')
// info.name, info.symbol
// info.balance → 3n (BigInt — number of NFTs owned)useNFTMetadata(contractAddress, tokenId)
Owner and token URI for a specific token. Automatically parses data:application/json on-chain URIs (fully on-chain NFTs).
const { metadata } = useNFTMetadata('0x...', 42n)
// metadata.owner
// metadata.tokenURI
// If on-chain JSON → metadata.name, metadata.description, metadata.imageuseNFTTransfer(contractAddress, operatorAddress?)
setApprovalForAll → safeTransferFrom as a single flow. Pass operatorAddress for marketplace/contract transfers. Omit for direct wallet transfers.
const { transfer, status, transferHash, isApprovedForAll, error, reset } = useNFTTransfer(
'0x...', // NFT contract
'0x...', // operator — omit for wallet-to-wallet
)
transfer({ to: '0x...', tokenId: 42n })Status values: approving | approval-mining | transferring | transfer-mining | confirmed | failed
useOpBnbFee({ data?, gasLimit?, enabled? })
Full L2 + L1 data fee breakdown for opBNB transactions. Returns breakdown: null on non-opBNB chains — safe to call unconditionally.
const { breakdown, isOpBnb } = useOpBnbFee({
data: '0x', // encoded calldata ('0x' for plain BNB transfers)
gasLimit: 21000n,
})
if (isOpBnb && breakdown) {
console.log(breakdown.l2FeeBnb) // execution cost
console.log(breakdown.l1FeeBnb) // data posting cost to BSC L1
console.log(breakdown.totalFeeBnb) // what the user actually pays
}Wallets (including MetaMask) typically show only the L2 portion. This hook reads the L1 Gas Price Oracle (0x420000...000F) deployed on opBNB and exposes both components.
useGaslessTransaction()
Sends via the BSC Paymaster (Megafuel) when a Paymaster URL is configured — the user pays zero gas. Automatically falls back to a normal gas transaction if the Paymaster rejects the tx or is unreachable. Safe to use unconditionally.
const { send, status, mode, txHash, error, reset } = useGaslessTransaction()
// BNB transfer
await send({ to: '0x...', valueEther: '0.01' })
// Contract call
await send({
to: '0x...',
abi: MY_ABI,
functionName: 'stake',
args: [parseEther('10')],
gasLimit: 150000n,
})
// mode === 'sponsored' → Paymaster covered gas
// mode === 'fallback-normal' → user paid, Paymaster skipped| Status | Meaning |
|---|---|
| checking | Calling pm_isSponsorable |
| pending | Waiting for wallet signature |
| mining | Submitted, waiting for confirmation |
| confirmed / failed | Final states |
To enable, set NEXT_PUBLIC_PAYMASTER_RPC_URL in your environment before building:
# .env.local (Next.js)
NEXT_PUBLIC_PAYMASTER_RPC_URL=https://bsc-mainnet.nodereal.io/v1/YOUR_KEYuseGaslessTransaction calls getPaymasterUrl() at runtime, which reads this variable. If the variable is unset, the hook skips the sponsored path and always sends a normal gas transaction — no errors, no config needed to disable it.
parseBscError(error)
Classifies any chain/wallet error into a structured object with a user-friendly message and a fix suggestion. Used internally by all hooks — also available directly.
import { parseBscError } from 'bnb-middleware-toolkit'
try {
await sendTransaction(...)
} catch (err) {
const { code, userMessage, suggestion } = parseBscError(err)
// Show userMessage in your UI, log code for debugging
}| Code | Trigger |
|---|---|
| INSUFFICIENT_FUNDS | Not enough BNB for gas + value |
| GAS_ESTIMATION_FAILED | Contract would revert (bad args, missing approval, paused) |
| TX_UNDERPRICED | gasPrice below node minimum |
| NONCE_ERROR | Nonce conflict — pending tx in the mempool |
| EXECUTION_REVERTED | Contract revert — reason string extracted automatically |
| USER_REJECTED | User cancelled in wallet |
| UNKNOWN_ERROR | Unclassified — raw message included |
BscMiddleware class
A viem-based, framework-agnostic client. Use this in Node.js scripts, server-side code, or anywhere outside React.
import { BscMiddleware } from 'bnb-middleware-toolkit'
const middleware = new BscMiddleware({ testnet: true })
// Gas estimate with safety buffers applied
const { gasLimit, gasPrice, estimatedCostBnb } = await middleware.estimateGas({
from: '0x...',
to: '0x...',
value: parseEther('0.01'),
})
// Token info — parallel multicall
const { name, symbol, decimals, formattedTotalSupply } = await middleware.getTokenInfo('0x...')
// Balances
const { formatted: bnbBalance } = await middleware.getBnbBalance('0x...')
const { formatted: tokenBalance } = await middleware.getTokenBalance('0xTOKEN', '0xOWNER')Chain utilities
import { CHAIN_META, explorerTxUrl, explorerAddressUrl } from 'bnb-middleware-toolkit'
explorerTxUrl(56, '0xabc...') // "https://bscscan.com/tx/0xabc..."
explorerTxUrl(5611, '0xabc...') // "https://testnet.opbnbscan.com/tx/0xabc..."
explorerAddressUrl(97, '0xdef...') // "https://testnet.bscscan.com/address/0xdef..."
CHAIN_META[56].label // "BSC Mainnet"
CHAIN_META[204].isL2 // true| Chain | ID | Explorer | |---|---|---| | BSC Mainnet | 56 | bscscan.com | | BSC Testnet | 97 | testnet.bscscan.com | | opBNB Mainnet | 204 | opbnbscan.com | | opBNB Testnet | 5611 | testnet.opbnbscan.com |
BSC Design Decisions
No EIP-1559 — explicit gasPrice required
BSC uses legacy Type-0 transactions. There is no base fee or priority fee — only a flat gasPrice. MetaMask sometimes applies EIP-1559 logic to BSC, sending maxFeePerGas instead, which nodes reject as "transaction underpriced."
Every transaction in this SDK calls eth_gasPrice and sets it explicitly — wallet auto-detection is never trusted.
Gas buffers
- +10% on gas price — protects against short-lived gas spikes on BSC
- +20% on gas limit — contract gas estimation can be tight; over-estimating is always safer
QueryClient defaults
refetchInterval: 4000ms, staleTime: 3000ms — tuned to BSC's ~3-second block time so balances and allowances stay near-real-time.
Approve → Execute
ERC-20/BEP-20 tokens require approve(spender, amount) before a contract can transfer tokens on the user's behalf. useTokenApproval reads the current allowance first and skips approval if it's already sufficient. useTokenTransfer bundles both steps into one state machine so callers never need to orchestrate them manually.
Running the Demo App
The demo at src/app/ shows every hook running against a real wallet and real testnet.
git clone https://github.com/your-username/bnb-middleware-toolkit
cd bnb-middleware-toolkit
npm install
cp .env.local.example .env.local # add NEXT_PUBLIC_WC_PROJECT_ID
npm run devOpen http://localhost:3000.
Get testnet BNB: bnbchain.org/en/testnet-faucet
Find testnet tokens: testnet.bscscan.com
Resources
| Resource | URL | |---|---| | BNB Chain Docs | https://docs.bnbchain.org/ | | BSC RPC Endpoints | https://docs.bnbchain.org/bnb-smart-chain/developers/rpc/ | | BSC Paymaster | https://docs.bnbchain.org/bnb-smart-chain/developers/paymaster/overview/ | | BEP-20 Standard | https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP20.md | | wagmi Docs | https://wagmi.sh | | viem Docs | https://viem.sh |
