calendar_today
January 30, 2025
|
schedule
min read
Sui Events Indexer
No items found. Add items to cms.
Check out the open-source repo here: https://github.com/buidly/sui-events-indexer
NPM: https://www.npmjs.com/package/sui-events-indexer

What is sui-events-indexer?

sui-events-indexer is a command-line tool that helps developers create a complete system for tracking and storing events from Sui Move smart contracts.

Think of it as your personal event-watching assistant that:

  • Reads your Sui Move smart contract
  • Creates TypeScript types for each of your Move events
  • Sets up a database to store these events
  • Generates an Express.js API to query the stored data

Instead of manually coding boilerplate for event indexing, you can run a single CLI command to generate a fully functional Express.js module tailored to your contract’s needs.

Why a SUI Events Indexing Service?

We built this service to simplify how developers handle events emitted by Sui Move smart contracts. Rather than juggling indexing scripts, database schemas, or TypeScript mappings, everything is generated for you in one go.

General Flow

  1. Take a Sui package ID
  2. Fetch event definitions from the source code
  3. Maps events into TypeScript DTOs
  4. Generate an Express.js module (complete with indexing logic, database storage, and an API)
  5. Deliver a ready-to-use setup that requires minimal configuration

How it all comes together

When you provide a Sui contract address, the tool grabs every relevant event from the chain, creates matching TypeScript DTOs, and sets up an Express.js module to manage event indexing and database storage. It also generates the event processing logic and Prisma schema, so you won’t have to maintain repetitive boilerplate by hand.

By automating these steps, you can skip the hassle of writing custom services, updating database models, or manually converting Move structs into TypeScript. Instead, you just run the CLI and start building your application on top of a reliable, preconfigured foundation.

Case study: Real-Time Analytics for Cetus on Sui

Like most DeFi projects, Cetus needs to process on-chain events in real time, think swap executions, liquidity updates, and reward distributions. These events power dashboards, user notifications, and overall protocol transparency.

Tracking these events manually can be a big lift:

  • Parsing each new version of smart contracts for event definitions
  • Maintaining a custom database schema and migration process for every contract update
  • Converting Move-based data types (like u64) into something JavaScript understands
  • Restoring state consistently if the indexer or database goes offline

These obstacles eat up development time that could otherwise go toward refining AMM mechanics, user experience, or cross-chain integrations.

By using sui-events-indexer, Cetus could spin up a production-ready event monitoring pipeline in minutes:

Package: 0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb

The system scans your Move package for any event::emit calls, collecting each event type for further processing.

161: Call event::emit<AddLiquidityEvent>(AddLiquidityEvent)
151: Call event::emit<SwapEvent>(SwapEvent)
149: Call event::emit<RemoveLiquidityEvent>(RemoveLiquidityEvent)

Auto-Discovery of Events

The tool identified SwapExecuted, LiquidityAdded, LiquidityRemoved, and any other contract emissions, ensuring complete coverage without extra config. Next, it identifies every struct linked to these events, including nested or referenced types from other modules.

struct AddLiquidityEvent has copy, drop, store {
    pool: ID,
    position: ID,
    tick_lower: I32,
    tick_upper: I32,
    liquidity: u128,
    after_liquidity: u128,
    amount_a: u64,
    amount_b: u64,
}
struct RemoveLiquidityEvent has copy, drop, store {
    pool: ID,
    position: ID,
    tick_lower: I32,
    tick_upper: I32,
    liquidity: u128,
    after_liquidity: u128,
    amount_a: u64,
    amount_b: u64,
}
struct SwapEvent has copy, drop, store {
    atob: bool,
    pool: ID,
    partner: ID,
    amount_in: u64,
    amount_out: u64,
    ref_amount: u64,
    fee_amount: u64,
    vault_a_amount: u64,
    vault_b_amount: u64,
    before_sqrt_price: u128,
    after_sqrt_price: u128,
    steps: u64,
}

It recursively checks for external modules referenced by these structs, so you get a complete mapping of all related fields.

All Move structs became precise TypeScript interfaces.

export interface SwapEvent {
  atob: boolean;
  pool: string;
  partner: string;
  amount_in: string;
  amount_out: string;
  ref_amount: string;
  fee_amount: string;
  vault_a_amount: string;
  vault_b_amount: string;
  before_sqrt_price: string;
  after_sqrt_price: string;
  steps: string;
}
import { I32 } from './I32';
export interface RemoveLiquidityEvent {
  pool: string;
  position: string;
  tick_lower: I32;
  tick_upper: I32;
  liquidity: string;
  after_liquidity: string;
  amount_a: string;
  amount_b: string;
}
import { I32 } from './I32';
export interface AddLiquidityEvent {
  pool: string;
  position: string;
  tick_lower: I32;
  tick_upper: I32;
  liquidity: string;
  after_liquidity: string;
  amount_a: string;
  amount_b: string;
}

By auto-generating these interfaces, the tool ensures your project stays in sync with on-chain data structures.

Generates Prisma Schema

A Prisma schema and PostgreSQL tables were created automatically, with built-in mechanisms to track processed events.

model AddLiquidityEvent {
  dbId String @id @unique @default(uuid())
  pool String
  position String
  tick_lower Json
  tick_upper Json
  liquidity String
  after_liquidity String
  amount_a String
  amount_b String
}
model RemoveLiquidityEvent {
  dbId String @id @unique @default(uuid())
  pool String
  position String
  tick_lower Json
  tick_upper Json
  liquidity String
  after_liquidity String
  amount_a String
  amount_b String
}
model SwapEvent {
  dbId String @id @unique @default(uuid())
  atob Boolean
  pool String
  partner String
  amount_in String
  amount_out String
  ref_amount String
  fee_amount String
  vault_a_amount String
  vault_b_amount String
  before_sqrt_price String
  after_sqrt_price String
  steps String
}

Each model corresponds directly to an on-chain event, making data storage straightforward and consistent.

Creates the Event Indexer

If the indexer paused for updates or scaling, it resumed at the exact block it left off, preventing data holes in analytics.

const EVENTS_TO_TRACK: EventTracker[] = [
  {
    type: `${CONFIG.CONTRACT.packageId}::pool`,
    filter: {
      MoveEventModule: {
        module: 'pool',
        package: CONFIG.CONTRACT.packageId,
      },
    },
    callback: handlePoolEvents,
  },
...

It uses the specified package and module to fetch events continuously, dispatching them to the right callback functions for processing.

Stores Events in the Database

When events arrive, they’re passed to the relevant handlers for insertion into the database. For example:

switch (eventName) {
  case 'RemoveLiquidityEvent':
    await prisma.removeLiquidityEvent.createMany({
      data: events as Prisma.RemoveLiquidityEventCreateManyInput[],
    });
    break;
  case 'AddLiquidityEvent':
    await prisma.addLiquidityEvent.createMany({
      data: events as Prisma.AddLiquidityEventCreateManyInput[],
    });
    break;
  case 'SwapEvent':
    await prisma.swapEvent.createMany({
      data: events as Prisma.SwapEventCreateManyInput[],
    });
    break;
  default:
    console.log('Unknown event type:', eventName);
}

These operations ensure that each event record is safely committed to the database, ready for analysis or display.

Creates GET Endpoints for Each Event Type

Finally, the tool generates Express endpoints for quick queries

app.get('/events/pool/add-liquidity-event', async (req, res) => {
  try {
    const events = await prisma.addLiquidityEvent.findMany();
    res.json(events);
  } catch (error) {
    console.error('Failed to fetch pool-AddLiquidityEvent:', error);
    res.status(500).json({ error: 'Failed to fetch events' });
  }
});

Similar endpoints exist for removing liquidity and swap events. This gives you a convenient REST API to retrieve event data in JSON, making it simple to build dashboards or integrate with external analytics.

Example Response

A GET /events/pool/swap-event call might return data like:

[
  {
    "dbId": "e0f98883-a5d6-4190-943c-05a811364728",
    "atob": true,
    "pool": "0xcf994611fd4c48e277ce3ffd4d4364c914af2c3cbb05f7bf6facd371de688630",
    "partner": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "amount_in": "30000000",
    "amount_out": "22965831196",
    "ref_amount": "0",
    "fee_amount": "75000",
    "vault_a_amount": "12869999917",
    "vault_b_amount": "9900000000001",
    "before_sqrt_price": "511620627870163534986",
    "after_sqrt_price": "510433780094775207155",
    "steps": "1"
  },
  ...
]

This JSON response can power a UI dashboard, monitoring tool, or any other feature that depends on real-time event data.

Impact

  • Real-Time Updates
  • No custom migrations or one-off scripts.
  • Automatic Move-to-TypeScript conversions
  • The indexed data stayed consistent.

The adoption of sui-events-indexer demonstrates how quickly and effectively a DeFi project on Sui can integrate real-time event tracking. Instead of building a custom system to parse, store, and query on-chain data, they leveraged a turnkey solution that scales with their protocol

The result? Faster feature rollouts, fewer headaches.

How it works

CLI Tool Input

Developers run the CLI tool, providing the smart contract address (package ID) they want to index:

Example command:

sui-events-indexer generate \
 -p 0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809 \
 --name my-project \
 --network mainnet \
 -i 1000

The tool accepts the following flags:

- `-p, — package <id>` (required) — Package ID to index events from

- ` — name <name>` (required) — Project name

- `-o, — output <dir>` — Output directory (default: current directory)

- `-n, — network <network>` — Network to use (default: mainnet)

- `-i, — interval <ms>` — Polling interval in milliseconds (default: 1000)

Fetch Smart Contract Events

The CLI tool first fetches the compiled Move bytecode for all modules within the specified package ID using Sui’s sui_getObject JSON-RPC endpoint. It then scans through the bytecode to find any event::emit calls, identifying all possible events your contract can emit. These fully qualified event types (e.g., 0x123::module_EventName) form the foundation for generating TypeScript types and the Prisma schema.

Map Events to TypeScript DTOs

Once events are fetched, the tool gets all the normalized Move structs for these events by calling Sui’s JSON-RPC API sui_getNormalizedMoveModulesByPackage.

  1. For each event struct found:
  • Examines all fields
  • Maps primitive types directly
  • Notes custom types (structs/enums) for processing recursively

2. Recursive Type Search

  • When a custom type is found:
  • Locates its definition in the module, and if not found there, expands its search to external modules referenced through ‘use’ statements, ensuring complete type resolution across both local and imported package dependencies.
  • Adds its fields to processing queue
  • Repeats until reaching primitive types
  • Handles both structs and enums
  • Maintains a visited-types cache to avoid cycles

This recursive exploration ensures we capture the complete type hierarchy, from top-level events down to their most basic components.

The tool will then map the Move types to Typescript using these rules:

// Primitive Types
u8, u16, u32     → number
u64, u128, u256  → string    // Preserves large numbers
bool             → boolean
address          → string
// Sui Framework Types (0x2)
object::ID       → string
object::UID      → string
// Container Types
vector<T>        → T[]
option<T>        → T | null
String           → string
// Collection Types
Table<K,V>       → Map<K,V>
VecMap<K,V>      → Map<K,V>
VecSet<T>        → Set<T>

Example DTO (TypeScript):

export interface SwapEvent {
 atob: boolean;
 pool: string;
 partner: string;
 amount_in: string;
 amount_out: string;
 ref_amount: string;
 fee_amount: string;
 vault_a_amount: string;
 vault_b_amount: string;
 before_sqrt_price: string;
 after_sqrt_price: string;
 steps: string;
}

Strong typing reduces bugs by making sure your contract events match exactly what your application expects.

Generate Prisma Schema

Based on these DTOs, the tool automatically generates a Prisma schema for each raw event type.

Each event has its own table, with fields corresponding to the DTO. This way, you can store and query your event data reliably.

Although the Prisma scheme is fully functional, we recommend adjusting the table definitions to only store the relevant fields for your use-case.

model SwapEvent {
  dbId String @id @unique @default(uuid())
  atob Boolean
  pool String
  partner String
  amount_in String
  amount_out String
  ref_amount String
  fee_amount String
  vault_a_amount String
  vault_b_amount String
  before_sqrt_price String
  after_sqrt_price String
  steps String
}

Each event has its own table, with fields corresponding to the DTO. This way, you can store and query your event data reliably.

Generate Express.js Module

The tool creates an Express.js module with:

  • Event Indexing Service: Continuously polls the SUI RPC endpoint for new events
  • Database Service: Stores events in PostgreSQL using Prisma

Controller: Provides endpoints for querying stored events

Generated Project Structure

my-custom-project/
├── prisma/
│   └── schema.prisma     # Generated Prisma schema
├── handlers/             # Event-specific handlers
├── indexer/             # Event indexing logic
├── types/               # Generated TypeScript types
├── config.ts            # Project configuration
├── db.ts                # Database client
├── indexer.ts           # Indexer entry point
├── server.ts            # Express API server
└── docker-compose.yml   # PostgreSQL setup

Event Indexing Logic

A polling service fetches new events from the SUI network at intervals defined by -i, — interval or an environment variable.

Polling Logic:

const executeEventJob = async (
 client: SuiClient,
 tracker: EventTracker,
 cursor: SuiEventsCursor,
): Promise<EventExecutionResult> => {
 try {
   const { data, hasNextPage, nextCursor } = await client.queryEvents({
     query: tracker.filter,
     cursor,
     order: 'ascending',
   });

   await tracker.callback(data, tracker.type);

   if (nextCursor && data.length > 0) {
     await saveLatestCursor(tracker, nextCursor);
     return { cursor: nextCursor, hasNextPage };
   }
 } catch (e) {
   console.error(e);
 }
 return { cursor, hasNextPage: false };
};

The polling interval can be configured through the .env file or CLI flag:

POLLING_INTERVAL_MS=1000

If interrupted, the indexer resumes from the last known cursor.

Event handlers

Each module has its own handler for different event types:

import { SuiEvent } from '@mysten/sui/client';
import { prisma, Prisma } from '../db';

export const handlePoolEvents = async (events: SuiEvent[], type: string) => {
 const eventsByType = new Map<string, any[]>();
  for (const event of events) {
   if (!event.type.startsWith(type)) throw new Error('Invalid event module origin');
   const eventData = eventsByType.get(event.type) || [];
   eventData.push(event.parsedJson);
   eventsByType.set(event.type, eventData);
 }

 await Promise.all(
   Array.from(eventsByType.entries()).map(async ([eventType, events]) => {
     const eventName = eventType.split('::').pop() || eventType;
     switch (eventName) {
       case 'AddLiquidityEvent':
         await prisma.addLiquidityEvent.createMany({
           data: events as Prisma.AddLiquidityEventCreateManyInput[],
         });
         console.log('Created AddLiquidityEvent events');
         break;
       case 'RemoveLiquidityEvent':
         await prisma.removeLiquidityEvent.createMany({
           data: events as Prisma.RemoveLiquidityEventCreateManyInput[],
         });
         console.log('Created RemoveLiquidityEvent events');
         break;
       case 'SwapEvent':
         await prisma.swapEvent.createMany({
           data: events as Prisma.SwapEventCreateManyInput[],
         });
         console.log('Created SwapEvent events');
         break;
       case 'CollectFeeEvent':
         await prisma.collectFeeEvent.createMany({
           data: events as Prisma.CollectFeeEventCreateManyInput[],
         });
         console.log('Created CollectFeeEvent events');
         break;
       case 'CollectRewardEvent':
         await prisma.collectRewardEvent.createMany({
           data: events as Prisma.CollectRewardEventCreateManyInput[],
         });
         console.log('Created CollectRewardEvent events');
         break;
       default:
         console.log('Unknown event type:', eventName);
     }
   }),
 );
};

This structure allows you to process different event types in isolation, making your indexing logic clean and organized.

Output

Once the CLI finishes, you have a fully operational Express.js module with:

  • DTOs for each event type
  • Prisma Schema for seamless database integration
  • Event Indexing Logic for polling and storing events
  • REST API endpoints to query stored events

Developers can integrate this module into larger applications with minimal effort.

Error Handling and Logging

Basic error handling and logging are included in the generated code:

  • Logs I/O errors by default
  • Offers hooks for developers to customize error responses
Check out the open-source repo here: https://github.com/buidly/sui-events-indexer
NPM: https://www.npmjs.com/package/sui-events-indexer

About Buidly

We’re not a typical Web3 dev shop. We’re a team of engineers ready to solve the toughest technical challenges.
We work with established blockchain protocols to build the core tech they need and help grow their ecosystems. Our team jumps in where it matters most, protocol-level upgrades, cross-chain solutions, smart contracts, core infrastructure. We handle the critical pieces that keep your protocol secure, scalable, and ready for what’s next.
But we’re not just about back-end work.
We take on projects we believe in and strive to become partners in your success. Our track record spans 20+ successful projects, delivering critical infrastructure and ecosystem tools that help protocols scale and grow.

You can also reach out on X, telegram or email us at contact@buidly.com. Be sure to follow our X for the latest updates and insights.

Placeholder

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.