HESTIAdocs

The route service

The trust-minimized convenience layer — indexer, store, relayer, and API handlers — and exactly what it can and cannot do.


@hestia/route is the off-chain convenience layer: an indexer that rebuilds pool state from chain events, a store that caches it, a relayer that submits proofs and pays gas, and framework-agnostic handlers that expose it all over a small API. It is designed to be trust-minimized — and to be something you can self-host or skip.

ts
import { ROUTE_API_VERSION } from "@hestia/route"; // "v1"

The store

The store is an in-memory index of pool state, reconstructed from events. The chain is the source of truth; the store is a cache that can be rebuilt by re-scanning.

ts
class HestiaStore {
  static create(depth?: number): Promise<HestiaStore>;
  applyCommitment(commitment, leafIndex, encryptedNote, blockNumber): void;
  applyNullifier(nullifier): void;
  applyAspRoot(root, uri): void;
  revokeAspRoot(root): void;
  get root(): bigint;
  get leafCount(): number;
  proof(leafIndex): MerkleProof;
  isNullified(nullifier): boolean;
  isKnownAspRoot(root): boolean;
  notesSince(fromBlock): IndexedNote[];
}

The in-memory store is the v0.1 implementation. Production persistence is a Prisma/Postgres schema with the same shape, so the interface doesn't change when you move from a single process to a durable database.

The indexer

The indexer reads HestiaPool and AssociationSetRegistry events with viem and applies them to a store — reconstructing the commitment tree, the nullifier set, and the approved ASP roots.

ts
import { Indexer, HestiaStore } from "@hestia/route";

const store = await HestiaStore.create();
const indexer = new Indexer(publicClient, poolAddress, registryAddress, store);
await indexer.sync(); // fetch + apply events from the last indexed block to head

Calling sync() repeatedly keeps the store current. Because the chain is authoritative, a fresh process can rebuild the entire state from block zero — nothing is lost if the cache is discarded.

The relayer

The relayer submits a client-built proof so a withdrawal's destination address has no gas-funding history tying it to the depositor. It is reimbursed in-token via the proof-bound feeAmount.

ts
import { relayTransact1x2, relayTransact2x2 } from "@hestia/route";

const txHash = await relayTransact1x2(walletClient, poolAddress, {
  proof: { a, b, c },
  nullifiers: [nf],
  outCommitments: [c0, c1],
  data, // { root, associationRoot, withdrawAmount, token, recipient, feeAmount, relayer }
  encryptedNotes: [blob0, blob1],
});

The crucial property: the relayer cannot alter recipient, withdrawAmount, or feeAmount — they are bound into the proof's public signals, so a dishonest relayer can only submit your transaction faithfully or fail to submit it. It can never redirect funds or skim more than the fee you authorized.

The handlers

Pure functions over a store, framework-agnostic — a thin Next.js app wraps them as the API routes:

ts
poolState(store);                 // { root, leafCount, treeDepth, lastBlock }
treeProof(store, leafIndex);      // { leaf, pathElements, pathIndices, root }
notesSince → notesScan(store, fromBlock); // encrypted blobs to trial-decrypt
associationStatus(store, root);   // { root, valid, uri }
nullifierStatus(store, nullifier);// { nullifier, spent }
health(store);                    // { status, lastBlock, leafCount }

There is also a standalone startServer (a node:http server) for running the route outside any framework, and loadChainConfig / makePublicClient for resolving chain config from the environment.

The trust model

The route is trust-minimized: it indexes public chain state and relays transactions. It cannot:

  • steal — it never holds your keys or your notes' plaintext;
  • forge — it cannot produce a valid proof; only you can; or
  • deanonymize beyond what is already public — it sees commitments, nullifiers, and ciphertexts it cannot read.

The worst a malicious route can do is withhold service (refuse to relay, or serve stale data). And because every component is open source and the chain is authoritative, agents may self-host the indexer and relayer, or run the SDK against an RPC with no route at all. See self-hosting.