Recipe · Operations

Indexing events

The SDK doesn't ship an indexer. It ships ABIs. Indexing is a viem flow with backfill + watch hooked into your persistence layer; the SDK keeps the contract surface honest.

Backfill#

Start with the manager / clone / singleton's deployment block (read via useManagerDeploymentBlockNumberfor vesting; analogous helpers exist on the other products). Page the getLogs range, RPC providers cap at 2k blocks for free tiers. Use the ABI export for clean decoding.

indexer/backfill.ts
ts
import { parseEventLogs } from "viem";
import { confidentialVestingManagerAbi } from "@tokenops/sdk/fhe-vesting";

// One-time backfill: query the historical range.
const logs = await publicClient.getLogs({
  address: managerAddress,
  fromBlock,
  toBlock: "latest",
});

// Decode every log against the ABI; non-matching are dropped.
const events = parseEventLogs({
  abi: confidentialVestingManagerAbi,
  logs,
});

for (const e of events) {
  switch (e.eventName) {
    case "VestingCreated":
      // e.args = { vestingId, recipient, encryptedAmountHandle, ... }
      await persistVesting(e.args);
      break;
    case "VestingClaimed":
      await persistClaim(e.args);
      break;
    // ...
  }
}

Live watch#

For new blocks, watchEvent handles re-orgs and gaps. Pair it with the backfill range so the indexer never has a missed-block window.

indexer/watch.ts
ts
// Live subscription: watch new blocks for the same set of events.
publicClient.watchEvent({
  address: managerAddress,
  events: confidentialVestingManagerAbi.filter((x) => x.type === "event"),
  onLogs(logs) {
    const events = parseEventLogs({
      abi: confidentialVestingManagerAbi,
      logs,
    });
    for (const e of events) handle(e);
  },
});

Schema sketch#

A minimal persistence schema:

vestings(id, recipient, manager, encryptedAllocationHandle, start, end, cliff, interval, status)
claims(vestingId, txHash, encryptedAmountHandle, claimedAt)
disclosures(vestingId, party, disclosureType, txHash)
roles(manager, role, account, grantedAt, revokedAt)

Encrypted handles stay as bytes32 strings in the DB; consumers query for handle ownership, then decrypt at render time via useDecryptedHandle.

See also