npm package: @moonwell-fi/lunar-indexer
Description: TypeScript SDK for the Moonwell Lunar Indexer.
Latest version: 0.1.0

# @moonwell-fi/lunar-data-sdk

Reorg-aware TypeScript SDK for building Lunar Indexers. Uses the latest **viem** client for chain access, **zod** for validation, and a pluggable storage adapter to persist raw events, projections, block history, and checkpoints. Ships as pure ESM with strict typing.

## Installation

```sh
bun add @moonwell-fi/lunar-indexer viem zod
```

## Core Concepts

- **Engine**: `LunarIndexer` manages live indexing, reorg detection/recovery, and replay.
- **Event definitions**: `defineEvent` pairs a viem ABI with an optional zod schema for validated args.
- **Storage adapter**: `LunarIndexerStorageAdapter` lets you plug in any persistence (Postgres, KV, Durable Objects, etc.).
- **Reorg safety**: Seam validation each loop + binary search rollback within a configurable window.
- **Replay**: Rebuild projections from the raw archive without hitting RPC.

## Quick Start

```ts
import { http, createPublicClient, mainnet } from "viem";
import { z } from "zod";
import {
	LunarIndexer,
	defineEvent,
	type LunarIndexerStorageAdapter,
	type DecodedEvent,
} from "@moonwell-fi/lunar-indexer";

// 1) Define events with ABIs + zod schemas
const transferEvent = defineEvent({
	name: "erc20-transfer",
	abi: [
		{
			type: "event",
			name: "Transfer",
			inputs: [
				{ name: "from", type: "address", indexed: true },
				{ name: "to", type: "address", indexed: true },
				{ name: "value", type: "uint256", indexed: false },
			],
		},
	],
	eventName: "Transfer",
	schema: z.object({
		from: z.string(),
		to: z.string(),
		value: z.bigint(),
	}),
});

// 2) Implement the storage adapter contract
const storage: LunarIndexerStorageAdapter = {
	async getCheckpoint() {
		return null; // TODO: load from DB
	},
	async saveIngestionBatch({ rawEvents, decodedEvents, blockHistory, newCheckpoint }) {
		// TODO: persist atomically
	},
	async getBlockHash() {
		return null; // TODO: return stored block hash
	},
	async deleteDataAfter() {
		// TODO: rollback state at/after the divergence block
	},
	async saveProjectionBatch() {
		/* TODO */
	},
	readRawEvents: async function* () {
		/* TODO: yield stored RawEvent batches for replay */
	},
};

// 3) Create a viem client
const client = createPublicClient({
	chain: mainnet,
	transport: http(process.env.RPC_URL!),
});

// 4) Run the indexer
const indexer = new LunarIndexer({
	indexerId: "my-indexer-v1",
	client,
	chainId: mainnet.id,
	storage,
	events: [transferEvent],
	defaultStartBlock: 18_000_000n,
	minConfirmations: 2,
	reorgWindowSize: 128,
	onBatchProcessed(info) {
		console.log("batch", info);
	},
});

await indexer.run();
```

### Replay projections

```ts
await indexer.replay({
	projectionId: "balances-v2",
	fromBlock: 15_000_000n,
	toBlock: 16_000_000n,
	batchSize: 500,
});
```

## Storage Adapter Contract (summary)

- `getCheckpoint` → return last `{ blockNumber, blockHash, chainId }` or `null`.
- `saveIngestionBatch` → atomic write of `rawEvents`, `decodedEvents`, `blockHistory`, and `newCheckpoint`; **idempotent** by event IDs.
- `getBlockHash` → used in reorg binary search.
- `deleteDataAfter` → remove all raw/derived/block-history/checkpoint data at or after a block when a reorg is detected.
- `readRawEvents` → ordered `(blockNumber ASC, logIndex ASC)` async iterable for replay.
- `saveProjectionBatch` → idempotent derived writes keyed by `(indexerId, projectionId, blockNumber, logIndex)`.

## Configuration Notes

- `defaultStartBlock`: starting block if no checkpoint.
- `batchSize`: number of blocks per `getLogs` request (default 1000).
- `minConfirmations`: index up to `head - minConfirmations` (default 0).
- `reorgWindowSize`: history retained and searched during reorg recovery (default 64).
- `liveProjectionId`: projection key for live ingestion (default `default`).
- `pollIntervalMs`: sleep when caught up to tip (default 1000).
- `blockConcurrency`: parallelism for per-block hash/timestamp lookups (default 5; higher = fewer RPC round trips, more load).
- `logLevel`: logging verbosity for the built-in logger (`silent`, `error`, `warn`, `info`, `debug`). Default: `info`.

## Reorg Handling

- Seam validation each loop compares stored checkpoint hash with RPC hash.
- If mismatch: shallow scan, then binary search within `reorgWindowSize` using stored block hashes.
- On divergence: `deleteDataAfter` rollback, then resume from the safe block.
- If history is insufficient to resolve, a `ReorgResolutionError` is thrown (operator should widen retention or rebuild).

## Import Alias

Local development uses `@/` → `src/` (configured in `tsconfig.json` and `tsup.config.ts`). The linter forbids relative imports inside `src` to keep the alias consistent.

## Scripts

- `bun run build` – bundle with tsup and emit d.ts
- `bun run dev` – watch & rebuild
- `bun run lint` / `bun run format` – Biome linting/formatting (fixes on write)
- `bun run typecheck` – strict type checking
- `bun run changeset` – create a version entry
- `bun run release` – publish (workflow is manual-triggered)

## Releasing

Changesets drives versioning and publishing. The GitHub Actions workflow is manual-only (`workflow_dispatch`); trigger from the Actions tab when you're ready, with `NPM_TOKEN` configured in repository secrets.