Skip to content

feat: add IKA coordinator event listening to SUI indexer. #322

Merged
Rcc999 merged 19 commits intomasterfrom
rayane/event-ika-polling
Feb 11, 2026
Merged

feat: add IKA coordinator event listening to SUI indexer. #322
Rcc999 merged 19 commits intomasterfrom
rayane/event-ika-polling

Conversation

@Rcc999
Copy link
Contributor

@Rcc999 Rcc999 commented Jan 23, 2026

Events are only logged for now, processing will be added in a follow up PR

Closes: #297

Summary by Sourcery

Add support for polling and handling IKA coordinator events in the Sui indexer alongside existing nBTC event processing.

New Features:

  • Introduce polling of IKA coordinator events via the Sui indexer processor and GraphQL client.
  • Persist IKA coordinator cursor state per network and package in a new indexer_ika_state table.
  • Log completed and rejected IKA signing events for future processing in the redeem service.

Enhancements:

  • Extend the Sui event handler to support optional setup IDs and additional event types without affecting existing nBTC processing.
  • Allow the GraphQL event fetcher to filter events by configurable module, enabling reuse for both nBTC and IKA coordinator events.
  • Update the Sui indexer main loop to independently process nBTC and IKA coordinator packages per network.

… only logged for now, processing will be added in a follow up PR

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
@Rcc999 Rcc999 requested a review from a team as a code owner January 23, 2026 18:11
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 23, 2026

Reviewer's Guide

Adds IKA coordinator event polling to the Sui indexer, including new DB state for coordinator cursors, storage helpers, event handling for IKA sign completion/rejection, and wiring into the existing polling loop with module-aware GraphQL event fetching.

Sequence diagram for IKA coordinator polling in Sui indexer

sequenceDiagram
  participant IndexerLoop
  participant D1Storage
  participant Processor
  participant SuiGraphQLClient
  participant SuiEventHandler

  IndexerLoop->>D1Storage: getActiveCoordinatorPkgs(netCfg.name)
  D1Storage-->>IndexerLoop: ikaPkgs
  alt ikaPkgs not empty
    IndexerLoop->>Processor: pollIkaEvents(ikaPkgs)
    loop while hasAnyNextPage
      Processor->>D1Storage: getIkaCursors(coordinatorPkgIds)
      D1Storage-->>Processor: cursors
      Processor->>SuiGraphQLClient: fetchEvents(packages with module coordinator_inner)
      SuiGraphQLClient-->>Processor: results
      loop for each coordinatorPkgId
        alt events for pkgId exist
          Processor->>SuiEventHandler: new SuiEventHandler(storage)
          Processor->>SuiEventHandler: handleEvents(result.events)
          SuiEventHandler-->>Processor: done
          alt endCursor advanced
            Processor->>D1Storage: saveIkaCursors(cursorsToSave)
            D1Storage-->>Processor: done
          end
        end
      end
    end
  else ikaPkgs empty
    IndexerLoop-->>IndexerLoop: skip IKA polling
  end
Loading

ER diagram for new indexer_ika_state table

erDiagram
  indexer_ika_state {
    TEXT sui_network
    TEXT coordinator_pkg_id PK
    TEXT ika_cursor
    INTEGER updated_at
  }
Loading

Updated class diagram for Sui indexer IKA support

classDiagram

  class Processor {
    +NetworkConfig netCfg
    +D1Storage storage
    +EventFetcher eventFetcher
    +pollAllNbtcEvents(packageIds string[]) void
    +pollIkaEvents(coordinatorPkgIds string[]) void
  }

  class D1Storage {
    +getActiveNbtcPkgs(suiNetwork SuiNet) string[]
    +getActiveCoordinatorPkgs(suiNetwork SuiNet) string[]
    +getIkaCursors(coordinatorPkgIds string[]) Record_string_string_or_null_
    +saveIkaCursors(cursors CursorRecord[]) void
  }

  class CursorRecord {
    +coordinatorPkgId string
    +suiNetwork SuiNet
    +cursor string
  }

  class EventFetcher {
    <<interface>>
    +fetchEvents(packages EventFetchPackage[]) Record_string_EventsBatch_
  }

  class EventFetchPackage {
    +id string
    +cursor string_or_null
    +module string_or_undefined
  }

  class SuiGraphQLClient {
    +url string
    +fetchEvents(packages EventFetchPackage[]) Record_string_EventsBatch_
  }

  class SuiEventHandler {
    -D1Storage storage
    -number_or_undefined setupId
    +constructor(storage D1Storage, setupId number_or_undefined)
    +handleEvents(events SuiEventNode[]) Promise_void
    -getSetupId() number
    -handleMint(txDigest string, e MintEventRaw) Promise_void
    -handleRedeemRequest(txDigest string, e RedeemRequestEventRaw) Promise_void
    -handleSolved(e SolvedEventRaw) Promise_void
    -handleIkaSignatureRecorded(e SignatureRecordedEventRaw) Promise_void
    -handleCompletedSign(e CompletedSignEventRaw) Promise_void
    -handleRejectedSign(e RejectedSignEventRaw) Promise_void
  }

  class CompletedSignEventRaw {
    +sign_id string
    +signature number[]
    +is_future_sign boolean
  }

  class RejectedSignEventRaw {
    +sign_id string
    +is_future_sign boolean
  }

  Processor --> D1Storage
  Processor --> EventFetcher
  SuiGraphQLClient ..|> EventFetcher
  Processor --> SuiEventHandler
  SuiEventHandler --> D1Storage
  D1Storage --> CursorRecord
  EventFetcher --> EventFetchPackage
  SuiEventHandler --> CompletedSignEventRaw
  SuiEventHandler --> RejectedSignEventRaw
Loading

File-Level Changes

Change Details Files
Introduce polling pipeline for IKA coordinator events using existing Sui event fetcher and handler infrastructure.
  • Add Processor.pollIkaEvents to fetch coordinator_inner events for configured coordinator package IDs, loop through pages, and update cursors
  • Reuse SuiEventHandler for IKA events, instantiating it without a setupId for coordinator flows
  • Log detailed debug information per coordinator package and persist updated cursors via storage
packages/sui-indexer/src/processor.ts
Extend storage layer and schema to track IKA coordinator cursor state and active coordinator packages per Sui network.
  • Create indexer_ika_state table with sui_network, coordinator_pkg_id primary key, ika_cursor, and updated_at columns
  • Add D1Storage.getIkaCursors and saveIkaCursors for reading/writing coordinator cursors
  • Add D1Storage.getActiveCoordinatorPkgs to list coordinator packages for a network
packages/btcindexer/db/migrations/0001_initial_schema.sql
packages/sui-indexer/src/storage.ts
Update event handler to support optional setup IDs and handle new IKA sign lifecycle events.
  • Make SuiEventHandler.setupId optional and guard usages via new getSetupId helper that throws when setupId is missing
  • Wire new CompletedSignEventRaw and RejectedSignEventRaw types into the handler event dispatch switch
  • Log completed/rejected sign events for IKA (no persistence yet; to be added in a follow-up)
packages/sui-indexer/src/handler.ts
packages/sui-indexer/src/models.ts
Wire IKA coordinator polling into the indexer runtime and make GraphQL client module-aware.
  • Split poolAndProcessEvents into separate nBTC and IKA flows using getActiveNbtcPkgs and getActiveCoordinatorPkgs, with distinct logging
  • Extend EventFetcher and SuiGraphQLClient.fetchEvents to accept an optional module name and use it in the GraphQL filter (defaulting to nbtc)
  • Clean up a stray eslint-disable comment in D1Storage.utxo-related batch code
packages/sui-indexer/src/index.ts
packages/sui-indexer/src/graphql-client.ts
packages/sui-indexer/src/storage.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#297 Add IKA coordinator event polling/listening to the SUI indexer, including persistence of per-coordinator cursors.
#297 Handle IKA signature completion and failure events (CompletedSignEvent and RejectedSignEvent) within the SUI indexer event handling pipeline.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The new createInterface import in models.ts is never used and can be safely removed to avoid confusion.
  • In storage.ts, the eslint-disable-next-line @typescript-eslint/no-non-null-assertion comment was removed but the non-null assertion dwalletIds[i]! remains; either restore the disable or refactor that code path to satisfy the linter without a non-null assertion.
  • In Processor.pollIkaEvents error handling, you log network: this.netCfg whereas other logs use this.netCfg.name; consider logging the network name for consistency and to avoid dumping the full config object.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `createInterface` import in `models.ts` is never used and can be safely removed to avoid confusion.
- In `storage.ts`, the `eslint-disable-next-line @typescript-eslint/no-non-null-assertion` comment was removed but the non-null assertion `dwalletIds[i]!` remains; either restore the disable or refactor that code path to satisfy the linter without a non-null assertion.
- In `Processor.pollIkaEvents` error handling, you log `network: this.netCfg` whereas other logs use `this.netCfg.name`; consider logging the network name for consistency and to avoid dumping the full config object.

## Individual Comments

### Comment 1
<location> `packages/sui-indexer/src/storage.ts:139-143` </location>
<code_context>
+	): Promise<void> {
+		if (cursors.length === 0) return;
+
+		const stmt = this.db.prepare(
+			`INSERT INTO indexer_ika_state (coordinator_pkg_id, sui_network, ika_cursor, updated_at)
+			 VALUES (?, ?, ?, ?)
+			 ON CONFLICT(coordinator_pkg_id) DO UPDATE SET ika_cursor = excluded.ika_cursor, updated_at = excluded.updated_at`,
+		);
+
+		const now = Date.now();
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Conflict handling for `indexer_ika_state` only keys on `coordinator_pkg_id`, ignoring `sui_network`.

Because `coordinator_pkg_id` is the primary key and conflict target:
- You can't persist distinct cursors per `sui_network` for the same `coordinator_pkg_id`.
- If a `coordinator_pkg_id` is reused on another network, the row’s `sui_network` will never change and becomes misleading.

If you need per-network cursors, use a composite key `(sui_network, coordinator_pkg_id)` (and the same pair in `ON CONFLICT`). If you truly want a single global row per coordinator, enforce that assumption in code and consider dropping `sui_network` from the key path to avoid confusion.

Suggested implementation:

```typescript
		const stmt = this.db.prepare(
			`INSERT INTO indexer_ika_state (coordinator_pkg_id, sui_network, ika_cursor, updated_at)
			 VALUES (?, ?, ?, ?)
			 ON CONFLICT(sui_network, coordinator_pkg_id) DO UPDATE SET ika_cursor = excluded.ika_cursor, updated_at = excluded.updated_at`,
		);

```

To fully support per-network cursors as implied by this change, the database schema for `indexer_ika_state` must be updated to:
1. Define a composite primary key or unique constraint on `(sui_network, coordinator_pkg_id)`.
2. Remove any conflicting single-column primary key on `coordinator_pkg_id` if it exists.
These changes should be implemented in the relevant migration or schema definition files for the indexer database.
</issue_to_address>

### Comment 2
<location> `packages/sui-indexer/src/handler.ts:39-42` </location>
<code_context>
 				await this.handleSolved(json as SolvedEventRaw);
 			} else if (e.type.includes("::nbtc::redeem_request::SignatureRecordedEvent")) {
 				await this.handleIkaSignatureRecorded(json as SignatureRecordedEventRaw);
+			} else if (e.type.includes("CompletedSignEvent")) {
+				await this.handleCompletedSign(e.json as CompletedSignEventRaw);
+			} else if (e.type.includes("RejectedSignEvent")) {
+				await this.handleRejectedSign(e.json as RejectedSignEventRaw);
 			}
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The `includes("CompletedSignEvent")` / `includes("RejectedSignEvent")` checks are quite broad and may match unintended event types.

These branches rely on substring matches:

```ts
} else if (e.type.includes("CompletedSignEvent")) {
  await this.handleCompletedSign(e.json as CompletedSignEventRaw);
} else if (e.type.includes("RejectedSignEvent")) {
  await this.handleRejectedSign(e.json as RejectedSignEventRaw);
}
```

If Sui adds other events whose names contain these substrings, they could be routed here unintentionally. To avoid that, prefer an exact or fully-qualified match, e.g.:

```ts
if (e.type === "<pkg>::coordinator_inner::CompletedSignEvent")
// or
if (e.type.endsWith("::coordinator_inner::CompletedSignEvent"))
```

```suggestion
			} else if (e.type.endsWith("::CompletedSignEvent")) {
				await this.handleCompletedSign(e.json as CompletedSignEventRaw);
			} else if (e.type.endsWith("::RejectedSignEvent")) {
				await this.handleRejectedSign(e.json as RejectedSignEventRaw);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Copy link
Contributor

@sczembor sczembor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left review

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
* Removed optimistic polling from recordIkaSig into event handler so that when we listen to IKA events, we process a redeem request based on the 2 events we listen

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>

* Resolved comments

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>

* Resolved comments

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>

---------

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds IKA (decentralized MPC coordinator) event listening functionality to the Sui indexer to track signature completion and rejection events for nBTC redeem operations.

Changes:

  • Adds IkaEventHandler to process CompletedSignEvent and RejectedSignEvent from IKA coordinator
  • Refactors signature verification from polling-based to event-driven architecture
  • Introduces ika_state table and supporting storage methods for tracking IKA event cursors per network and coordinator package
  • Extends GraphQL client and processor to support querying events from multiple modules

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/sui-indexer/src/handler.ts Renames SuiEventHandler to NbtcEventHandler and adds new IkaEventHandler class for processing IKA coordinator events
packages/sui-indexer/src/processor.ts Adds pollIkaEvents method and suiClient parameter to support IKA event polling
packages/sui-indexer/src/storage.ts Adds methods for IKA cursor management, redeem sign info retrieval, and updates getSigningRedeems query
packages/sui-indexer/src/models.ts Adds IkaCursorUpdate and RedeemSignInfo interfaces and IKA event type definitions
packages/sui-indexer/src/index.ts Integrates IKA event polling into main loop and refactors client initialization
packages/sui-indexer/src/redeem-service.ts Removes polling-based signature verification logic, now handled by IKA event handlers
packages/sui-indexer/src/graphql-client.ts Adds optional module parameter to support querying events from different Move modules
packages/lib/src/secrets.ts Adds reusable getMnemonic helper function for secret retrieval with error handling
packages/btcindexer/src/btcindexer.ts Updates to use new getMnemonic helper function
packages/btcindexer/db/migrations/0001_initial_schema.sql Adds ika_state table and ika_coordinator_pkg column to setups table
packages/lib/src/test-helpers/init_db.ts Updates test table list to include ika_state

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
…lanning

Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
Signed-off-by: Rayane Charif <rayane.charif@gonative.cc>
@Rcc999 Rcc999 merged commit 5e319dd into master Feb 11, 2026
12 checks passed
@Rcc999 Rcc999 deleted the rayane/event-ika-polling branch February 11, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nBTC Redeem: Add Ika event listening to sui-indexer

5 participants