@hazbase/kit
v0.8.1
Published
An SDK that wraps pre‑designed smart contracts for safe Web (TypeScript) access.
Maintainers
Readme
@hazbase/kit
Overview
@hazbase/kit is an SDK that wraps pre‑designed smart contracts for safe Web (TypeScript) access.
For each domain (issuance, KPI, whitelist, emergency pause, etc.) it provides typed Helpers that unify reads/writes, snapshots, and event handling over ethers v6. Use the same code in browsers or Node.js.
- Typical helpers:
FlexibleTokenHelper,BondTokenHelper,KpiRegistryHelper,EmergencyPauseManagerHelper,WhitelistHelper, … - Wallet API client:
createHazbaseWalletClientfor token lists, balances, activity, transfers, and x402 wallet payments - x402 utilities: request parsing, payment requirement selection, URL handoff, and extension content bridge helpers
- Design: ESM‑first, ethers v6, BigInt‑friendly types, minimal runtime assumptions
- Goal: Let frontends and backends safely connect and operate contracts using a consistent TypeScript API
Requirements
- Node.js >= 18.18 (ESM, fetch, BigInt)
- TypeScript >= 5.2
- Ethers v6
- Module format: ESM (CommonJS‑only builds are discouraged)
package.json (example)
{
"type": "module",
"engines": { "node": ">=18.18" }
}tsconfig.json (example)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2023", "DOM"],
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"types": ["node"]
}
}Installation
pnpm add @hazbase/kit ethers dotenv
# or
npm i @hazbase/kit ethers dotenvQuick start: hazBase wallet API client
Use createHazbaseWalletClient from @hazbase/kit/wallet when an app needs
hazBase-hosted wallet APIs without hand-writing fetch wrappers. The wallet
subpath is browser-friendly and does not pull in contract helper dependencies.
import { createHazbaseWalletClient } from '@hazbase/kit/wallet';
const wallet = createHazbaseWalletClient();
const tokens = await wallet.listTokens({ chainId: 11155111 });
const balance = await wallet.getBalance({
chainId: 11155111,
token: 'example-token',
account: '0x1234...',
});
const prepared = await wallet.prepareTransfer({
chainId: 11155111,
token: 'example-token',
account: '0x1234...',
recipient: '0xabcd...',
amount: '10.0',
});
const submitted = await wallet.submitTransfer({
emailSession: '<app-session-access-token>',
chainId: 11155111,
token: 'example-token',
account: prepared.account,
recipient: prepared.recipient,
amount: prepared.amount.input,
deviceBindingId: 'devb_...',
highTrustToken: '<fresh-passkey-token>',
});The client is token-agnostic. Pass token IDs, chain IDs, account addresses, and metadata from your application config.
Applications that need a contract call use the policy-gated owner-operation methods. The API executes only targets, selectors, value limits, and ERC-20 approval bindings registered by the operator; arbitrary calldata is rejected.
const preparedOperation = await wallet.prepareOwnerOperation({
chainId: 11155111,
account: '0x1234...',
policyKey: 'example.registered-action',
calls: [{ to: '0xabcd...', value: '0', data: '0x12345678...' }],
});
const submittedOperation = await wallet.submitOwnerOperation({
emailSession: '<app-session-access-token>',
chainId: preparedOperation.chainId,
account: preparedOperation.account,
policyKey: preparedOperation.operation.policyKey,
calls: preparedOperation.operation.calls,
deviceBindingId: 'devb_...',
highTrustToken: '<fresh-owner-reauth-token>',
});By default, the client uses https://api.hazbase.com. Pass apiEndpoint only
when you need a local, staging, or self-hosted API:
const localWallet = createHazbaseWalletClient({
apiEndpoint: 'http://127.0.0.1:3110',
});Quick start: x402 parsing and wallet extension bridge
Use @hazbase/kit/x402 to parse payment requirements without hard-coding a
specific token or chain. The caller supplies the accepted networks and assets.
import { summarizeX402Request } from '@hazbase/kit/x402';
const request = summarizeX402Request(x402Payload, {
sourceUrl: location.href,
pageTitle: document.title,
}, {
networks: ['sepolia'],
assets: [{ asset: '0xTokenAddress...', assetKey: 'example-token', decimals: 18 }],
});Wallet extensions can use @hazbase/kit/extension to expose the standard
hazbase:x402:* and signed hazbase:wallet:link-* page bridges while keeping
wallet-specific runtime message names in the wallet implementation.
import { installHazbaseWalletContentBridge } from '@hazbase/kit/extension';
installHazbaseWalletContentBridge({
walletName: 'Example Wallet',
openX402MessageType: 'example:x402Detected',
receiveWalletLinkMessageType: 'example:approveWalletLink',
runtimePaymentMessageType: 'example:x402BridgePayment',
runtimeCancelledMessageType: 'example:x402BridgeCancelled',
});The wallet runtime must approve the forwarded challenge with
createHazbaseWalletClient().approveWalletLink(...) after checking that the
requested address belongs to its authenticated app session. This produces a
short-lived proof bound to the requesting origin, purpose, wallet, chain, and
one-time nonce.
Static merchant or game pages can also load the browser bundle and use the same page bridge without a framework:
Copy node_modules/@hazbase/kit/dist/browser.global.js to a public asset path
such as /vendor/hazbase-kit.js.
<script src="/vendor/hazbase-kit.js"></script>
<script>
(async () => {
const result = await HazbaseKit.requestWalletLink({
purpose: 'account_link',
timeoutMs: 3500,
});
if (result.ok) {
console.log('verified wallet', result.walletAddress);
localStorage.setItem('wallet-link-session', result.linkSessionToken);
return;
}
if (result.challenge) {
location.href = HazbaseKit.createWalletLinkPwaUrl(walletBaseUrl, {
challenge: result.challenge,
returnUrl: location.href,
});
}
})();
</script>After a PWA handoff returns, consume and verify the proof before persisting the address:
const verified = await HazbaseKit.consumeAndVerifyWalletLinkFromFragment();
if (verified) {
console.log('verified wallet', verified.walletAddress);
localStorage.setItem('wallet-link-session', verified.linkSessionToken);
}On later visits, restore only after the signed session is verified. Never trust a separately cached address or a local boolean marker:
const token = localStorage.getItem('wallet-link-session');
const restored = token ? await HazbaseKit.verifyWalletLinkSession(token) : null;
if (restored) {
console.log('restored wallet', restored.walletAddress);
}Link sessions are bound to the requesting origin and purpose and expire after a server-configured lifetime (seven days by default).
requestWalletAddress() remains available for non-security-sensitive display
or migration code. Do not use a raw returned address as authentication,
authorization, ownership, or eligibility evidence.
Owner-confirmed wallet operations
Applications can request a wallet-owned smart-account operation without
embedding wallet UI or signing logic. The backend must validate every target,
selector, native value, calldata limit, and ERC-20 approval binding against a
named policy before the wallet asks the owner to approve it. A trusted
application backend must also mint a short-lived, one-time grantToken bound to
the account, policy, exact calls, and metadata. Never expose the grant issuer
secret to browser code.
const result = await HazbaseKit.requestWalletOperation({
id: operationRequestId,
request: {
chainId: 11155111,
account: linkedWalletAddress,
policyKey: 'example.deposit',
calls: preparedCalls,
grantToken: preparedGrantToken,
metadata: { action: 'deposit' },
},
});If no extension acknowledges the request, continue through a PWA while keeping the server-issued operation ID in application session storage:
if (!result.ok && result.reason === 'wallet_operation_unavailable') {
location.href = HazbaseKit.createWalletOperationPwaUrl(walletBaseUrl, {
id: operationRequestId,
request: preparedOperation,
origin: location.origin,
returnUrl: location.href,
});
}The wallet validates that the handoff return URL has the same origin as the requesting application, expires the request after a bounded interval, checks the selected wallet account, and revalidates both the operation policy and its exact one-time grant with the backend. The PWA handoff is placed in the URL fragment so it is not sent in the HTTP request or a normal referrer header. On return, consume the result once and bind it to the server-issued operation ID kept by the application:
const walletResult = HazbaseKit.consumeWalletOperationResultFromFragment({
expectedId: operationRequestId,
});Treat this result as a submitted operation, not as final settlement. Persist the returned UserOperation hash against the server-issued request and independently confirm the expected finalized chain event before crediting assets or releasing goods.
For x402 handoff pages, use the browser helpers to keep URL generation and extension messages consistent across services:
const walletUrl = HazbaseKit.createX402WalletUrl(walletBaseUrl, x402Payload, {
sourceUrl: location.href,
title: document.title,
completionMode: 'fragment',
completionParam: 'xPayment',
});
HazbaseKit.postX402BridgeRequest({
x402: x402Payload,
sourceUrl: location.href,
title: document.title,
completionMode: 'fragment',
completionParam: 'xPayment',
});Migration Notes
This minor release includes a breaking change in Splitter route definitions.
Breaking change
Splitter.Route now requires reserveBucket. Existing route objects must be updated before upgrading.
// before
{ dest: "0xRecipient...", bps: 5000 }
// after
{ dest: "0xRecipient...", bps: 5000, reserveBucket: "direct" }Use the following values:
direct: standard recipient routingcompensation: send to aReservePoolcompensation bucketliquidity: send to aReservePoolliquidity bucket
If your existing integration sends funds to a ReservePool, review every route explicitly instead of relying on the old implicit compensation path.
Proof bootstrap checklist
Treat deployed and proof-ready as separate states when using MultiTrustCredential with KpiRegistry.
const mtc = MultiTrustCredentialHelper.attach(process.env.MTC_ADDRESS!, signer);
await mtc.assertIntegratedProofReadiness({
kpiRegistry: process.env.KPI_REGISTRY_ADDRESS!,
requiredKpiWriterRoles: ['KPI_WRITER'],
});Use this check after deploy and before enabling proof-dependent flows in staging or production. It will fail if:
- the verifier is not configured on
MultiTrustCredential - the
KpiRegistrypoints at a different MTC instance - the registry is missing
MTC.ADMIN_ROLE - the registry is missing required writer-role grants on MTC
For Splitter route validation, use SplitterHelper.lintRoutes(routes, 'erc20' | 'native') during config review. Native routes that try to fund the ReservePool liquidity bucket are rejected by the helper before submission.
Environment (.env example)
RPC_URL=https://<your-rpc>
PRIVATE_KEY=0x<private-key> # server-side only
FLEXIBLE_TOKEN_ADDRESS=0x... # attach to an existing deployment (optional)Quick start: FlexibleToken deploy → mint/issue → transfer
scripts/flexible-token.ts
// FlexibleToken end-to-end: deploy -> mint -> transfer
import 'dotenv/config';
import { ethers } from 'ethers';
import { FlexibleTokenHelper } from '@hazbase/kit'; // Main exports
async function main() {
// 1) Provider / Signer
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL!);
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
// 2) Deploy via Factory (clone deployment)
// NOTE: args order/name can differ depending on your implementation
const chainId = Number((await provider.getNetwork()).chainId);
const name = 'My Flexible Token';
const symbol = 'MFT';
const decimals = 18;
const admin = await signer.getAddress();
const {address: tokenAddress} = await FlexibleTokenHelper.deploy(
{
name,
symbol,
treasury: signer.address,
initialSupply: 0n,
cap: ethers.parseUnits("1000000000", decimals), // raw, scaled by token decimals — 1 B max
decimals,
transferable: true,
admin: signer.address,
forwarders: []
},
deployer // deploy signer (owner)
);
console.log('Deployed FlexibleToken at:', tokenAddress);
// 3) Attach helper
const token = await FlexibleTokenHelper.attach(tokenAddress, signer);
// Sanity reads
console.log('symbol =', await token.symbol());
console.log('decimals =', await token.decimals());
// 4) Mint/Issue to self (requires proper role)
const recipient = admin;
const amount = 1_000n * 10n ** 18n;
const txMint = await token.mint(recipient, amount); // or token.issue(...)
const rcMint = await txMint.wait();
console.log('Minted:', amount.toString(), 'tx:', rcMint?.hash);
console.log('balance(recipient) =', (await token.balanceOf(recipient)).toString());
// 5) Transfer to another address
const to = '0x0123456789abcdef0123456789abcdef01234567';
const sendAmt = 100n * 10n ** 18n;
const tx = await token.transfer(to, sendAmt);
const rc = await tx.wait();
console.log('Transferred:', sendAmt.toString(), 'to:', to, 'tx:', rc?.hash);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});Run
tsx scripts/flexible-token.ts
# or: node --env-file=.env --loader tsx scripts/flexible-token.tsIf you already have a deployment, set
FLEXIBLE_TOKEN_ADDRESSand doFlexibleTokenHelper.attach(FLEXIBLE_TOKEN_ADDRESS, signer)instead of deploying.
Common operations (snippets)
1) Attach → read → write
// Attach, read, write (FlexibleToken)
const token = await FlexibleTokenHelper.attach(process.env.FLEXIBLE_TOKEN_ADDRESS!, signer);
// Reads
console.log('name =', await token.name());
console.log('totalSupply =', (await token.totalSupply()).toString());
// Writes (roles/pauses may apply)
await (await token.transfer('0xRecipient...', 1_000n)).wait();2) Subscribe to events & fetch historical logs
// Live subscription (ERC-20 style Transfer)
token.contract.on('Transfer', (from, to, value, ev) => {
console.log('Transfer:', { from, to, value: value.toString(), tx: ev.log.transactionHash });
});
// Historical logs
const event = token.contract.interface.getEvent('Transfer');
const topic0 = token.contract.interface.getEventTopic(event);
const logs = await token.contract.runner!.provider!.getLogs({
address: token.address,
topics: [topic0], // add indexed filters as needed
fromBlock: 0x0,
toBlock: 'latest',
});
for (const l of logs) {
const parsed = token.contract.interface.parseLog(l);
console.log('past Transfer:', parsed.args);
}Helper names
- FlexibleTokenHelper (used above)
- BondTokenHelper
- ReservePoolHelper
- AgreementManagerHelper
- PooledTokenEscrowHelper
- MarketManagerHelper
- WhitelistHelper
- KpiRegistryHelper
- PrivilegeNFTHelper / PrivilegeEditionHelper
- DebtManagerHelper
- EmergencyPauseManagerHelper
- TimelockControllerHelper
- GenericGovernorHelper / MetaGovernorHelper
- MultiTrustCredentialHelper
- SplitterHelper
- StakingHelper
Pooled ERC-20 escrow
PooledTokenEscrowHelper wraps many-to-one pooled payments without assuming a
specific token, wallet, or application. For a smart account, batch the exact
approval and deposit in one operation:
import { PooledTokenEscrowHelper } from '@hazbase/kit/escrow';
const escrow = PooledTokenEscrowHelper.attach(ESCROW_ADDRESS, provider);
const calls = escrow.buildApproveAndDepositCalls(
TOKEN_ADDRESS,
escrowId,
contributionId,
1_000n,
);
// Submit `calls` with an EOA wallet, smart account, or wallet SDK.Before release, buildWithdrawOpenContributionCall lets a smart account
withdraw only its own net contribution without cancelling the pool. The helper
also exposes createEscrow, assignBeneficiary, claim, enableRefunds,
refund, read methods, and EIP-712 beneficiary assignment utilities. Product
metadata and identity checks remain application concerns.
Operations (roles & pause)
- Least privilege: hand off
DEFAULT_ADMIN_ROLEto a Timelock/Multisig. SplitMINTER_ROLE,PAUSER_ROLE, etc. - Pause/resume: define a clear runbook for
pause/unpause(monitoring signals, approval steps) and call through helpers.
Troubleshooting (FAQ)
INSUFFICIENT_ROLE/AccessControl:— missing role. Check minter/transfer permissions.paused/whenNotPaused— contract paused. Follow your governance recovery flow.insufficient funds— not enough gas. Fund the EOA or ensure relayer quota.- ESM/CJS mismatch — kit is ESM‑first. If you’re on webpack4/CJS‑only, upgrade to Vite/webpack5 or enable ESM builds.
Next steps
- See each helper’s detailed page (
FlexibleTokenHelper,BondTokenHelper,KpiRegistryHelper, …) for full signatures, revert reasons, and recipes. - Implement event aggregation / snapshots in your dashboard/backend for robust disclosure & audit.
Security: recommended overrides
ethers currently pins a ws version with a known advisory, and npm ignores
overrides declared inside a dependency. To protect your own dependency tree,
add this to your application's package.json and reinstall:
{
"overrides": {
"ws": "^8.21.0"
}
}(yarn: use resolutions; pnpm: use pnpm.overrides.) Workaround until ethers
ships a fixed ws range upstream.
License
Apache-2.0
