@hazbase/smartwallet is a lightweight helper to work with ERC‐4337 Smart Accounts using minimal code.
Highlights
- Chain‐aware
SmartWallet.connect(...)that auto‐fills defaults (Factory / EntryPoint / Provider) - Fluent UserOperation builder:
execute(...).withGasLimits(...).buildAndSend() - Bundler integration with
X-Api-Keyheader and path‐based routing (https://<host>/<chainId>) - Optional Paymaster middleware to inject
paymasterAndData - Signer adapter:
asSigner()exposes an ethers‐compatibleAbstractSigner
- Node.js: 18+
- ethers: v6 (uses
JsonRpcProviderandFetchRequest) - Bundler must implement
eth_sendUserOperationandeth_getUserOperationReceipt(and optionally route by/chainId). - (Optional) Paymaster API that accepts
POST { userOp, chainId }and returns{ data: { paymasterAndData } }.
npm i @hazbase/smartwallet ethers
This package expects explicit configuration in code (it does not read .env).
bundlerUrl: e.g.,https://bundler.example.com(the library appends/{chainId}internally)apiKey: value for theX-Api-Keyheader sent to your bundlerowner: an ethersSignerwith a connectedProviderentryPoint/factory/provider: resolved byconnect()per chain; throws if unsupported
import { ethers } from "ethers"; import { SmartWallet } from "@hazbase/smartwallet"; async function main() { // 1) Prepare owner signer with a provider const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!); const owner = new ethers.Wallet(process.env.OWNER_KEY!, provider); // 2) Connect SmartWallet (factory/entryPoint/provider are auto-filled by chainId) const sw = await SmartWallet.connect({ owner, factory : "0x0", // placeholder (auto-resolved) entryPoint : "0x0", // placeholder (auto-resolved) bundlerUrl : "https://bundler.example.com", apiKey : process.env.BUNDLER_API_KEY || "", usePaymaster: false }); // 3) Send a 0 ETH call (or set a real value) const to = "0x000000000000000000000000000000000000dEaD"; const value = 0n; const data = "0x"; // empty calldata // Build & send → returns tx hash after inclusion const txHash = await sw.execute(to, value, data) .withGasLimits({ call: 200_000n }) .buildAndSend(); console.log("txHash:", txHash); } main().catch(console.error);
import { SmartWallet } from "@hazbase/smartwallet"; async function transferErc20(sw: SmartWallet, token: string, to: string, amount: bigint) { const txHash = await sw.transferERC20(token, to, amount).buildAndSend(); console.log("erc20 transfer txHash:", txHash); }
import { SmartWallet, paymasterMiddleware } from "@hazbase/smartwallet"; const chainId = 11155111; // sepolia const sw = await SmartWallet.connect({ owner, factory : "0x0", entryPoint : "0x0", bundlerUrl : "https://bundler.example.com", apiKey : process.env.BUNDLER_API_KEY || "", usePaymaster: true, // enable default middlewares: [ paymasterMiddleware("https://paymaster.example.com", chainId) ] });
import { ethers } from "ethers"; const signer = sw.asSigner(); // AbstractSigner-compatible const resp = await signer.sendTransaction({ to: "0x000000000000000000000000000000000000dEaD", value: 0n, data: "0x" }); console.log("userOp hash:", resp.hash); const receipt = await resp.wait(1); // internally polls eth_getUserOperationReceipt console.log("included tx:", receipt.transactionHash);
- The library requests
bundlerUrl + "/" + chainId:- e.g.,
https://bundler.example.com/11155111 - e.g.,
https://bundler.example.com/80002
- e.g.,
- If
apiKeyis provided, requests includeX-Api-Key: <your-key>automatically.
- Role: Construct a wallet with chain defaults (Factory / EntryPoint / Provider), route bundler by
/{chainId}, and attach middlewares. - Args
owner: Signer(must have Provider)bundlerUrl: string(e.g.,https://bundler.example.com)apiKey?: string(addsX-Api-Keyheader)usePaymaster?: boolean/middlewares?: Middleware[]factory? / entryPoint? / provider?(optional; will be auto‐resolved per chain)
- Returns:
SmartWallet
- Role: Predict (or return) the SmartAccount address via
factory.predictAddress(owner, salt). - Returns:
Address
- Role: Check if the account is deployed using
factory.usedKey(keccak(owner,salt)). - Returns:
boolean
- Role: Deploy the account via
factory.createAccount(owner,salt). - Returns: Deployment tx receipt
- Throws: If already deployed
- Role: Deposit native token (ETH) to EntryPoint for this account (
depositTo(address)). - Args:
bigint | string | number(string/numberparsed viaparseEther) - Returns: Tx receipt
- Role: Read EntryPoint deposit (uses
balanceOf, falls back todepositsfor older EP). - Returns:
weibalance
- Role: Execute
EntryPoint.withdrawTo(to, amount)via a userOp. - Returns: L1 tx hash after inclusion
- Role: Read
EntryPoint.getNonce(account, key)(defaults to key=0). - Returns:
nonce
- Role: Compose
SmartAccount.execute(to, value, data)userOp. - Returns:
UserOpBuilder(withGasLimits().build().buildAndSend())
- Role: Shortcut to call ERC‐20
transfer(to, amount)via userOp.
- Role: Run
beforeSignmiddlewares →EntryPoint.getUserOpHash(uo)→owner.signMessage. - Returns: Signed
UserOperation(signature populated)
- Role:
sign()→ Bundlereth_sendUserOperation→wait()→afterSendmiddlewares. - Returns: L1 tx hash once included
- Role: Poll
eth_getUserOperationReceiptuntil present or timeout. - Returns: Inclusion log (has
transactionHash) - Throws: If not found within the limit
- Role: Return an ethers
AbstractSignerto integrate with existing tooling.
- Role: Expose the underlying EOA signer.
AA23 reverted: deposit fail
The account or Paymaster deposit is insufficient. Top up withdepositTo(...).invalid paymaster signature
ThepaymasterAndDatasignature scheme or signer does not match the Paymaster contract.- UserOp receipt not found
Increasewait()attempts or ensure the Bundler retains receipts long enough. - CORS (browser usage)
AllowAccess-Control-Allow-*headers on your HTTPS reverse proxy.
- The library automatically sets
X-Api-KeywhenapiKeyis provided. - L1 gas is ultimately paid by the Account deposit or the Paymaster deposit.
Apache-2.0