npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@moonwell-fi/lunar-indexer

v0.1.3

Published

TypeScript SDK for the Moonwell Lunar Indexer.

Downloads

485

Readme

@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

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

Core Concepts

  • Engine: LunarIndexer manages live indexing, reorg detection/recovery, and replay.
  • Global trigger scanning: GlobalIndexer manages permissive topic-driven trigger scans with checkpointing, optional selective raw-event archival, and no reorg handling.
  • Event definitions: defineEvent pairs a viem ABI with an optional zod schema for validated args.
  • Trigger definitions: defineGlobalTriggerEvent adds per-trigger decode behavior for noisy global scans.
  • 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

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

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

Global Trigger Scanning

GlobalIndexer is for MAMO-style trigger discovery where you scan broad topic sets, tolerate unrelated topic collisions, and inspect receipts in application code after ingestion.

  • It persists checkpoint progress so restarts resume from the last processed block.
  • It does not do seam validation, block-history writes, or reorg rollback.
  • It only persists raw trigger logs if the consumer explicitly marks emitted events during onEventsIndexed.
  • onEventsIndexed receives controls for persistEventIds(...) / persistEvents(...), so the consumer can keep only the accepted subset from a noisy batch.
  • It does not expose replay().
  • Mixed wildcard and address-filtered trigger configs are queried separately, so one wildcard trigger does not erase address filtering for the others.
import { http, createPublicClient, mainnet } from "viem";
import {
	GlobalIndexer,
	defineGlobalTriggerEvent,
	type LunarIndexerStorageAdapter,
} from "@moonwell-fi/lunar-indexer";

const erc20Transfer = defineGlobalTriggerEvent({
	name: "usdc-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",
	contractAddresses: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
	decodeMode: "permissive",
});

const wildcardDeposit = defineGlobalTriggerEvent({
	name: "mamo-deposit-trigger",
	abi: [
		{
			type: "event",
			name: "Deposited",
			inputs: [
				{ name: "owner", type: "address", indexed: true },
				{ name: "amount", type: "uint256", indexed: false },
			],
		},
	],
	eventName: "Deposited",
	decodeMode: "metadata-only",
});

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

const storage: LunarIndexerStorageAdapter = {
	async getCheckpoint() {
		return null;
	},
	async getBlockHash() {
		return null;
	},
	async deleteDataAfter() {},
	async saveProjectionBatch() {},
	async saveIngestionBatch() {},
	readRawEvents: async function* () {},
	async saveProgressBatch({ newCheckpoint }) {
		// Persist checkpoint progress only.
	},
};

const indexer = new GlobalIndexer({
	indexerId: "mamo-global-triggers",
	client,
	chainId: mainnet.id,
	storage,
	events: [erc20Transfer, wildcardDeposit],
	defaultStartBlock: 18_000_000n,
	minConfirmations: 2,
	includeBlockMetadata: false,
	onEventsIndexed: async (events, controls) => {
		const accepted = events.filter((event) => {
			// After receipt inspection, keep only triggers your app actually owns.
			return event.address.toLowerCase() ===
				"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
		});
		for (const event of events) {
			// Fetch the tx receipt here and do strict strategy-specific filtering.
			console.log(event.transactionHash, event.eventName, event.address);
		}
		controls.persistEvents(accepted);
	},
});

await indexer.run();

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.
  • saveProgressBatch → optional checkpoint-only write path for scanners such as GlobalIndexer when no raw events from the batch were selected for persistence.
  • 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).
  • liveProjectionId: projection key for live ingestion (default default).
  • pollIntervalMs: sleep when caught up to tip (default 1000).
  • includeBlockMetadata: enrich emitted GlobalIndexer events with block metadata such as timestamp. Default: false. Keep this disabled for high-volume trigger scanners that only need transactionHash, blockNumber, logIndex, address, and optional decoded args.
  • blockConcurrency: parallelism for per-block hash/timestamp lookups (default 5). For GlobalIndexer, this only matters when includeBlockMetadata is enabled.
  • logLevel: logging verbosity for the built-in logger (silent, error, warn, info, debug). Default: info.

GlobalIndexer is optimized for a thin getLogs -> permissive match -> callback -> selective raw write -> checkpoint loop. Implement saveProgressBatch in storage if you want the cheapest checkpoint-only persistence path for batches where nothing was selected.

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.