@hazbase/react is a lightweight React toolkit for wallet connection and network control built on ethers v6.
It unifies MetaMask (injected), WalletConnect (via EIP-1193 provider), and Guest login (ephemeral private key in the browser), and provides a simple API surface:
<WalletProvider />— global state & actionsuseSigner()— signer instance & connect/disconnectuseAddress()— current addressuseNetwork()— read current chain info and switch/add network
Designed to work smoothly with @hazbase/kit helpers (connect with an ethers Signer), and to fit compliance-first architectures later (suitability, reputation, circuit-breaker, etc.).
- Node.js: 18+ (ESM recommended)
- React: 18+
- Deps:
ethersv6
npm i @hazbase/react ethers react react-dom
# or
yarn add @hazbase/react ethers react react-domWrap your app with WalletProvider, then use hooks from anywhere.
// main.tsx import React from "react"; import { createRoot } from "react-dom/client"; import App from "./App"; import { WalletProvider } from "@hazbase/react"; const knownChains = [ { id: 1, name: "Ethereum", rpcUrls: ["https://..."] }, { id: 137, name: "Polygon", rpcUrls: ["https://..."] } ]; createRoot(document.getElementById("root")!).render( <React.StrictMode> <WalletProvider knownChains={knownChains} autoConnect> <App /> </WalletProvider> </React.StrictMode> );
// App.tsx import React from "react"; import { useSigner, useAddress, useNetwork } from "@hazbase/react"; export default function App() { const { status, signer, connectMetaMask, connectGuest, disconnect } = useSigner(); const { address, isConnected } = useAddress(); const { chain, chainId, switchNetwork } = useNetwork(); const polygon = { id: 137, name: "Polygon", rpcUrls: ["https://..."] }; return ( <div style={{ padding: 24 }}> <h2>@hazbase/react demo</h2> <p>Status: <b>{status}</b></p> <p>Address: <b>{address ?? "-"}</b></p> <p>Network: <b>{chain?.name ?? chainId ?? "-"}</b></p> <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginTop: 12 }}> <button onClick={connectMetaMask}>Connect MetaMask</button> <button onClick={() => connectGuest({ privKey: "0x...", rpcUrl: "https://..." }) } > Connect Guest (demo key) </button> <button onClick={() => switchNetwork(polygon)} disabled={!isConnected}> Switch to Polygon </button> <button onClick={disconnect}>Disconnect</button> </div> <p style={{ marginTop: 12 }}>Signer ready? <b>{signer ? "yes" : "no"}</b></p> </div> ); }
Global provider that manages connector state (MetaMask or Guest), current account, chain, and exposes actions.
Props
knownChains?: ChainConfig[]— known networks for name/metadata and EIP-3085wallet_addEthereumChain.autoConnect?: boolean— re-connect last connector (MetaMask only) on mount.
export interface ChainConfig { id: number; // EIP-155 chain id (decimal) name: string; rpcUrls: string[]; icon?: string; nativeCurrency?: { name: string; symbol: string; decimals: number }; blockExplorers?: { name: string; url: string }[]; }
Returns signer info & connect/disconnect actions.
type Status = "disconnected" | "connecting" | "connected" | "locked"; function useSigner(): { status: Status; signer: import("ethers").JsonRpcSigner | import("ethers").Wallet | null; connectMetaMask(): Promise<void>; connectGuest(params: { privKey: `0x${string}`; rpcUrl: string }): Promise<void>; disconnect(): void; }
- MetaMask: wraps
window.ethereumwithBrowserProvider("any"), follows chain changes automatically. - Guest: constructs
Wallet(privKey, new JsonRpcProvider(rpcUrl)).
Production note: encrypt at rest (IndexedDB + WebCrypto) and decrypt only right before signing.
Read current address and connection status.
function useAddress(): { address: `0x${string}` | null; isConnected: boolean; }
Read chain info and switch/add networks.
function useNetwork(): { chainId: number | null; chain: ChainConfig | null | undefined; connector: "metamask" | "guest" | null; refresh(): Promise<void>; switchNetwork(target: ChainConfig): Promise<void>; // MetaMask: EIP-3326; Guest: rebind RPC addNetwork(params: ChainConfig): Promise<void>; // EIP-3085 for injected wallets }
- MetaMask: tries
wallet_switchEthereumChain; if unknown (code=4902) useswallet_addEthereumChain. - Guest: re-creates
JsonRpcProviderandWalletwith the same private key, validates actualchainId, then commits state.
Most helpers accept an ethers Signer. Just connect(signer) when available.
import { useEffect, useMemo } from "react"; import { JsonRpcProvider } from "ethers"; import { useSigner } from "@hazbase/react"; import { FlexibleTokenHelper } from "@hazbase/kit"; export function BondWidget() { const { status, signer } = useSigner(); const helper = useMemo(() => { const provider = new JsonRpcProvider("https://..."); const token = FlexibleTokenHelper.attach("0xToken..." as `0x${string}`, provider); }, []); useEffect(() => { if (status === "connected" && signer) helper.connect(signer); }, [status, signer, helper]); // ... return null; }
import { useSigner, useAddress } from "@hazbase/react"; export function ConnectButton() { const { status, connectMetaMask, connectGuest, disconnect } = useSigner(); const { address, isConnected } = useAddress(); return isConnected ? ( <div style={{ display: "flex", gap: 8 }}> <span>{address?.slice(0, 6)}...{address?.slice(-4)}</span> <button onClick={disconnect}>Disconnect</button> </div> ) : ( <div style={{ display: "flex", gap: 8 }}> <button onClick={connectMetaMask}>Connect MetaMask</button> <button onClick={() => connectGuest({ privKey: "0x...", rpcUrl: "https://..." })}> Guest </button> <span style={{ color: "#888" }}>{status === "connecting" ? "connecting..." : ""}</span> </div> ); }
- MetaMask chain change: With
BrowserProvider(..., "any"), the provider follows chain changes. This library also listens tochainChangedand re-reads account & signer. - Account change:
accountsChangedtriggers re-fetchingaccountandsigner. - Guest mode: app-managed RPC & key; always switch networks via
useNetwork().switchNetwork()to keep state/signers in sync.
METAMASK_NOT_FOUND— No injected EIP-1193 provider. Show install link or fallback to Guest.RPC_URL_REQUIRED(Guest) — TargetChainConfig.rpcUrls[0]missing.CHAIN_ID_MISMATCH(Guest) — RPC chain ID differs fromChainConfig.id. Fix target config or RPC.UNSUPPORTED_CONNECTOR— Action not available for current connector.
- WalletConnect connector (EIP-1193 provider →
BrowserProvider) - Guest key encryption helpers (IndexedDB + WebCrypto)
- Optional compliance hooks (suitability, reputation/MTC, circuit-breaker)
Apache-2.0