# @hazbase/kit
[![npm version](https://badge.fury.io/js/@hazbase%2Fkit.svg)](https://badge.fury.io/js/@hazbase%2Fkit)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

## 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: `createHazbaseWalletClient` for 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)**
```jsonc
{
  "type": "module",
  "engines": { "node": ">=18.18" }
}
```

**`tsconfig.json` (example)**
```jsonc
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2023", "DOM"],
    "strict": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "types": ["node"]
  }
}
```

---

## Installation
```bash
pnpm add @hazbase/kit ethers dotenv
# or
npm i @hazbase/kit ethers dotenv
```

---

## Quick 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.

```ts
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.

```ts
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:

```ts
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.

```ts
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.

```ts
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`.

```html
<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:

```js
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:

```js
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.

```js
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:

```js
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:

```js
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:

```js
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.

```ts
// before
{ dest: "0xRecipient...", bps: 5000 }

// after
{ dest: "0xRecipient...", bps: 5000, reserveBucket: "direct" }
```

Use the following values:
- `direct`: standard recipient routing
- `compensation`: send to a `ReservePool` compensation bucket
- `liquidity`: send to a `ReservePool` liquidity 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`.

```ts
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 `KpiRegistry` points 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`**
```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**
```bash
tsx scripts/flexible-token.ts
# or: node --env-file=.env --loader tsx scripts/flexible-token.ts
```

> If you already have a deployment, set `FLEXIBLE_TOKEN_ADDRESS` and do `FlexibleTokenHelper.attach(FLEXIBLE_TOKEN_ADDRESS, signer)` instead of deploying.

---

## Common operations (snippets)

### 1) Attach → read → write
```ts
// 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
```ts
// 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:

```ts
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_ROLE` to a Timelock/Multisig. Split `MINTER_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:

```jsonc
{
  "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
