@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 ethersThis 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