Skip to content

feat(indexer): chain-indexer app (indexer v2) - #3579

Draft
baktun14 wants to merge 6 commits into
mainfrom
feat/indexer-scaffold-chain-indexer-app
Draft

feat(indexer): chain-indexer app (indexer v2)#3579
baktun14 wants to merge 6 commits into
mainfrom
feat/indexer-scaffold-chain-indexer-app

Conversation

@baktun14

@baktun14 baktun14 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Collaboration branch

This PR doubles as the integration branch for the indexer v2 rewrite. Per-issue PRs (CON-804 and onward) target feat/indexer-scaffold-chain-indexer-app and merge here; this PR accumulates them and goes to main once a releasable slice is ready. Note for reviewers: the CI test/lint/tsc matrix only runs on PRs based on main, so we verify child PRs locally before merging.

Why

Related to CON-803

First code slice of the Indexer v2 rewrite: the new apps/chain-indexer app plus a working sandbox tracer bullet, built in parallel with the design review so the discussion has running code to poke at. Draft until the design review lands.

What

New workspace app @akashnetwork/chain-indexer following the tx-signer/api conventions (tsyringe DI, LoggerService, zod-validated env, Hono + zod-openapi, tsup, vitest). Nothing existing is touched; the only change outside the new folder is the lockfile.

  • Runtime roles via INDEXER_ROLE (sync | backfill | api | jobs) and NETWORK (mainnet | sandbox | testnet). sync and a minimal api (healthz + status) are implemented; backfill and jobs exit with ROLE_NOT_IMPLEMENTED.
  • Own database, Drizzle-owned: cosmos.blocks, cosmos.transactions, cosmos.messages (interned message_types + size-capped decoded body jsonb), and an indexer_state checkpoint row per stream. No UUIDs, natural keys, hashes as bytea. Migrations run on boot.
  • RpcClientPool: least-loaded healthy node selection, failover, cooldown on failure, timeout via AbortSignal.
  • Pipeline: decode once into an immutable DecodedBlock (chain-sdk + cosmos registry), one Postgres transaction per block including the checkpoint advance, idempotent natural-key upserts, no isProcessed flags.
  • Sync runner: pg advisory-lock leader election, parent-hash continuity verification (halt on mismatch), resume from checkpoint after restart, graceful SIGTERM shutdown (exit 0).

Verified against live sandbox: tails blocks at the tip, /v1/status checkpoint advances, kill + restart resumes at checkpoint + 1 with no continuity break, and rows land in all three tables. 17 unit tests over the decoder, canonical JSON serializer, and RPC pool; lint and tsc clean.

Message bodies for types outside the registry (e.g. cosmwasm on sandbox) are stored as null with the typeUrl preserved; the version-adapter and dead-letter layer is a later issue.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a42859a5-dda2-4bb1-9b47-f43fc1e46e4b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The Chain Indexer app now includes validated configuration, PostgreSQL persistence, RPC failover, block decoding, synchronization with advisory locking, HTTP health and status routes, server lifecycle handling, tests, and development tooling.

Changes

Chain Indexer application

Layer / File(s) Summary
Application foundation
apps/chain-indexer/package.json, apps/chain-indexer/src/config/*, apps/chain-indexer/src/providers/*, apps/chain-indexer/src/services/config/*, apps/chain-indexer/src/types/*, apps/chain-indexer/tsconfig*, apps/chain-indexer/vitest.config.ts, apps/chain-indexer/tsup.config.ts
Added package scripts, TypeScript and test tooling, Zod configuration validation, dependency-injection providers, logging, protocol type registration, and shared Hono types.
Database schema and migrations
apps/chain-indexer/src/db/*, apps/chain-indexer/src/providers/db.provider.ts, apps/chain-indexer/drizzle*
Added Cosmos block-indexing tables, binary column mapping, PostgreSQL client management, Drizzle migration execution, and initial migration metadata.
RPC access and block decoding
apps/chain-indexer/src/rpc/*, apps/chain-indexer/src/pipeline/block-decoder.service.ts, apps/chain-indexer/src/pipeline/decoded-block.ts, apps/chain-indexer/src/pipeline/canonical-json.*
Added typed RPC responses, node failover and cooldown handling, canonical JSON conversion, block decoding, transaction metadata extraction, message decoding, and unit tests.
Synchronization and block commits
apps/chain-indexer/src/pipeline/block-committer.service.ts, apps/chain-indexer/src/pipeline/sync-runner.service.ts
Added transactional block persistence, message-type interning, checkpoint updates, advisory-lock leadership, restart-height recovery, parent-hash validation, and sequential synchronization.
HTTP routes and server lifecycle
apps/chain-indexer/src/app.ts, apps/chain-indexer/src/routes/*, apps/chain-indexer/src/http-schemas/*, apps/chain-indexer/src/services/hono-error-handler/*, apps/chain-indexer/src/services/start-server/*, apps/chain-indexer/src/services/shutdown-server/*, apps/chain-indexer/src/index.ts, apps/chain-indexer/src/server.ts
Added health and status endpoints, structured error handling, application assembly, role-based bootstrap, server startup, shutdown handling, and test teardown.
Runtime documentation and environment template
apps/chain-indexer/README.md, apps/chain-indexer/env/.env.sample
Documented runtime roles, local setup, commands, migration generation, and environment settings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: ygrishajev

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/indexer-scaffold-chain-indexer-app

Comment @coderabbitai help to get the list of available commands.

Comment thread package-lock.json
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.38576% with 197 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.37%. Comparing base (de09757) to head (35cfc61).
⚠️ Report is 11 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...s/hono-error-handler/hono-error-handler.service.ts 0.00% 35 Missing and 6 partials ⚠️
...-indexer/src/services/start-server/start-server.ts 0.00% 35 Missing and 3 partials ⚠️
...er/src/services/shutdown-server/shutdown-server.ts 0.00% 21 Missing and 3 partials ⚠️
apps/chain-indexer/src/reconcile/reconcile.ts 0.00% 11 Missing and 4 partials ⚠️
apps/chain-indexer/src/app.ts 0.00% 8 Missing ⚠️
apps/chain-indexer/src/providers/db.provider.ts 33.33% 8 Missing ⚠️
...hain-indexer/src/services/status/status.service.ts 0.00% 8 Missing ⚠️
.../chain-indexer/src/pipeline/sync-runner.service.ts 90.27% 7 Missing ⚠️
...ces/open-api-hono-handler/open-api-hono-handler.ts 0.00% 4 Missing and 1 partial ⚠️
apps/chain-indexer/src/db/pg-client.service.ts 20.00% 4 Missing ⚠️
... and 17 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3579      +/-   ##
==========================================
+ Coverage   76.43%   77.37%   +0.94%     
==========================================
  Files        1137     1209      +72     
  Lines       29623    31374    +1751     
  Branches     7382     7738     +356     
==========================================
+ Hits        22641    24276    +1635     
- Misses       6153     6254     +101     
- Partials      829      844      +15     
Flag Coverage Δ
api 89.09% <ø> (-0.05%) ⬇️
chain-indexer 85.38% <85.38%> (?)
deploy-web 67.37% <ø> (+1.10%) ⬆️
log-collector 85.85% <ø> (ø)
notifications 93.84% <ø> (ø)
provider-console 81.38% <ø> (ø)
provider-inventory 84.98% <ø> (ø)
provider-proxy 88.17% <ø> (ø)
tx-signer 90.19% <ø> (+3.46%) ⬆️
Files with missing lines Coverage Δ
.../chain-indexer/src/archive/archive-block-source.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/archive/archive-codec.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/archive/archive-layout.ts 100.00% <100.00%> (ø)
...chain-indexer/src/archive/block-archive.service.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/config/env.config.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/db/bytea.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/db/insert-chunk-size.ts 100.00% <100.00%> (ø)
apps/chain-indexer/src/db/insert-chunked.ts 100.00% <100.00%> (ø)
...hain-indexer/src/genesis/account-seeder.service.ts 100.00% <100.00%> (ø)
...s/chain-indexer/src/genesis/bank-seeder.service.ts 100.00% <100.00%> (ø)
... and 49 more

... and 96 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@baktun14
baktun14 marked this pull request as ready for review August 11, 2026 07:55
@baktun14
baktun14 requested a review from a team as a code owner August 11, 2026 07:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (7)
apps/chain-indexer/tsconfig.json (1)

2-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a broad IDE include pattern.

Change include to ["**/*.ts"]. Keep tsconfig.build.json narrow for build tooling. The current configuration excludes test and root TypeScript files from IDE diagnostics.

Proposed change
-  "exclude": ["node_modules", "dist", "test", "src/**/*.spec.ts"],
+  "exclude": ["node_modules", "dist"],
   "extends": "./tsconfig.build.json",
-  "include": ["src/**/*"],
+  "include": ["**/*.ts"],

Based on learnings, app tsconfig.json files should use a broad "**/*.ts" include while separate build configurations remain narrow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/tsconfig.json` around lines 2 - 4, Update the include
setting in the app tsconfig to ["**/*.ts"] so IDE diagnostics cover all
TypeScript files, including tests and root-level files. Leave the existing
exclude entries and the narrow tsconfig.build.json configuration unchanged.

Source: Learnings

apps/chain-indexer/src/db/schema.ts (1)

58-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

indexer_state lands in public, not cosmos.

Every other table uses cosmosSchema.table. Line 58 uses pgTable, so the migration creates public.indexer_state. Move it into cosmosSchema now. After release, relocating a table needs a migration plus a deployment window where readers and writers disagree on the search path.

♻️ Proposed change
-export const IndexerState = pgTable("indexer_state", {
+export const IndexerState = cosmosSchema.table("indexer_state", {
   stream: text("stream").primaryKey(),
   lastHeight: bigint("last_height", { mode: "number" }).notNull(),
   updatedAt: timestamp("updated_at", { withTimezone: true }).notNull()
 });

Regenerate the migration and snapshot after this change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/db/schema.ts` around lines 58 - 62, Update
IndexerState to use cosmosSchema.table instead of pgTable so indexer_state is
created in the cosmos schema consistently with the other tables. Regenerate the
associated migration and schema snapshot to reflect the corrected table
location.
apps/chain-indexer/src/pipeline/sync-runner.service.ts (1)

140-154: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Confirm the restart path when the checkpoint block row is absent.

Line 145 sets #lastHash to null when no row matches state.lastHeight. #verifyContinuity then skips the parent-hash check for the first block after the restart. That is the correct fallback, but it also silently masks a checkpoint that points at a height with no persisted block. Log a warning in that branch so the condition is visible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/pipeline/sync-runner.service.ts` around lines 140 -
154, Update `#resolveStartHeight` so that when state exists but checkpointBlock is
absent, it logs a warning describing the missing persisted block and checkpoint
height before setting `#lastHash` to null; preserve the existing restart behavior
and parent-hash fallback.
apps/chain-indexer/src/pipeline/block-committer.service.ts (1)

33-41: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

as number hides a reachable undefined typeId.

Line 38 asserts the lookup succeeds. If #internMessageTypes does not return an id for a type URL, typeId is undefined at runtime and the insert fails inside the transaction with a driver-level error that names no type URL. Throw an explicit error instead so the failure identifies the missing type URL.

♻️ Proposed change
     const messageRows = block.transactions.flatMap(tx =>
-      tx.messages.map(message => ({
-        height: block.height,
-        txIndex: tx.index,
-        index: message.index,
-        typeId: typeIds.get(message.typeUrl) as number,
-        body: message.body
-      }))
+      tx.messages.map(message => {
+        const typeId = typeIds.get(message.typeUrl);
+
+        if (typeId === undefined) {
+          throw new Error(`Missing interned message type id for ${message.typeUrl} at height ${block.height}`);
+        }
+
+        return {
+          height: block.height,
+          txIndex: tx.index,
+          index: message.index,
+          typeId,
+          body: message.body
+        };
+      })
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/pipeline/block-committer.service.ts` around lines 33 -
41, Update the message-row construction in the block commit flow to validate the
result of typeIds.get(message.typeUrl) before assigning typeId. Throw an
explicit error that includes the missing message.typeUrl when no ID exists, and
remove the unsafe “as number” assertion while preserving numeric IDs for
successful lookups.
apps/chain-indexer/src/providers/logging.provider.ts (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use @singleton() for LoggerService.

LoggerService is stateless. @injectable() uses tsyringe's default transient scope, while @singleton() reuses one instance. (raw.githubusercontent.com) Change the import and decorator.

As per coding guidelines, backend stateless tsyringe services must use @singleton().

Suggested fix
-import { container, injectable } from "tsyringe";
+import { container, singleton } from "tsyringe";

-@injectable()
+@singleton()
 export class LoggerService extends LoggerServiceOriginal {

Also applies to: 8-12

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/providers/logging.provider.ts` at line 4, Update
LoggerService to use tsyringe’s singleton scope: import singleton instead of
injectable and replace the class decorator with `@singleton`(), preserving the
existing container registration and service behavior.

Source: Coding guidelines

apps/chain-indexer/src/pipeline/block-decoder.service.ts (1)

38-51: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

decodeTxRaw failure halts sync at one block.

decodeTxRaw throws on bytes it cannot parse. The error propagates through decode to sync-runner.service.ts, and the checkpoint never advances past that height. Decide the policy explicitly: either persist the transaction with empty messages and a marker, or let it throw but confirm that operators get an actionable alert. The current behavior is an unattended stall.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/pipeline/block-decoder.service.ts` around lines 38 -
51, Update `#decodeTransaction` and its decode caller to explicitly handle
decodeTxRaw failures so one malformed transaction cannot silently stall block
synchronization. Prefer persisting the transaction with empty messages and a
clear decode-failure marker while retaining its index, hash, fee, and execution
metadata; otherwise propagate the error only through an actionable operator
alert and ensure the checkpoint still follows the intended policy.
apps/chain-indexer/src/rpc/rpc-client-pool.service.ts (1)

57-76: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider one retry pass with backoff before failing.

#get makes exactly one attempt per node, then throws AggregateError. sync-runner.service.ts propagates that failure. A short simultaneous blip on all endpoints therefore aborts the block. Add a bounded second pass with a small delay, or wrap the call in a retry policy at the caller.

Also consider draining or cancelling the body on the non-ok path in #fetchFromNode (Line 81-83) so keep-alive sockets are reused.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/chain-indexer/src/rpc/rpc-client-pool.service.ts` around lines 57 - 76,
Update `#get` to perform one bounded retry pass across RPC candidates after a
short backoff before throwing AggregateError, preserving node health and
inFlight bookkeeping for every attempt. In `#fetchFromNode`, consume or cancel the
response body on non-OK responses before propagating the failure so connections
can be reused.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/chain-indexer/src/config/env.config.ts`:
- Line 20: Update the PORT schema in the environment configuration to require an
integer between 0 and 65535, while preserving its coercion and default value of
3092.

In `@apps/chain-indexer/src/index.ts`:
- Around line 20-27: Validate the result returned by startServer in both the
sync and api branches before proceeding. If startup returns undefined, fail
bootstrap by throwing or setting a nonzero exit code; only start
SyncRunnerService after a successful server start, and prevent the api branch
from exiting successfully.

In `@apps/chain-indexer/src/pipeline/block-committer.service.ts`:
- Around line 74-89: Update
apps/chain-indexer/src/pipeline/block-committer.service.ts:74-89 in
`#internMessageTypes` to select and cache existing MessageTypes rows for the
cache-missed URLs before inserting, insert only the remaining URLs, then perform
the final stillMissing lookup to handle concurrent inserts. Update
apps/chain-indexer/src/db/schema.ts:38 to use serial/integer types for
MessageTypes.id and Messages.typeId, then regenerate the migration and snapshot.

In `@apps/chain-indexer/src/pipeline/block-decoder.service.ts`:
- Around line 24-36: Update decode in the block-decoding service to validate
that txs_results exists and contains one result for every transaction before
mapping rawTxs. Fail fast with an error when the results are missing or shorter
than rawTxs, and do not allow `#decodeTransaction` to default missing results to
successful zero-gas transactions.

In `@apps/chain-indexer/src/pipeline/canonical-json.ts`:
- Around line 1-9: Update replacer to detect the original property value with
Buffer.isBuffer before the existing bigint and Uint8Array checks, converting
Buffers to base64 so JSON.stringify does not serialize them as its toJSON object
representation.

In `@apps/chain-indexer/src/pipeline/sync-runner.service.ts`:
- Around line 124-138: Implement periodic leadership validation by adding
`#assertLeadership` to SyncRunnerService and invoking it at the start of every
outer iteration in `#run`; stop the sync loop when the advisory-lock assertion
fails. Update the IndexerState checkpoint upsert in BlockCommitterService so its
onConflictDoUpdate applies only when excluded.last_height is greater than the
existing indexer_state.last_height, preventing stale writers from rewinding
progress.
- Around line 82-95: Update the sync loop in start() to retry transient
`#syncBlock` failures with bounded backoff, while preserving a hard stop for
`#verifyContinuity` errors. Ensure fatal loop termination marks the service
unhealthy or sets process.exitCode and shuts down so /healthz cannot remain
green. In `#getTipHeight`, validate the parsed height and handle NaN by logging
and delaying before retrying, preventing a busy loop.

In `@apps/chain-indexer/src/providers/db.provider.ts`:
- Around line 20-30: Update migrateDb to acquire a migration-specific PostgreSQL
advisory lock on migrationClient before calling migrate, using a key distinct
from SYNC_LEADER_LOCK_KEY. Release the same lock in the finally path before
migrationClient.end(), ensuring lock acquisition and release use the same
client.

In `@apps/chain-indexer/src/server.ts`:
- Around line 3-5: Replace the console.error call in the bootstrap failure
handler with LoggerService, using a stable event name and preserving the caught
error as a structured field. Keep the existing process.exitCode = 1 behavior
unchanged.

In
`@apps/chain-indexer/src/services/hono-error-handler/hono-error-handler.service.ts`:
- Around line 59-68: In the unrecognized-error branch of the Hono error handler,
log the original error message for server-side diagnostics, but replace the
response’s dynamic message with a constant generic internal-server-error
message. Keep the existing 500 status and error metadata unchanged.

In `@apps/chain-indexer/src/services/shutdown-server/shutdown-server.ts`:
- Around line 21-29: Update the shutdown flow around server.close in the
shutdown handler to track active connections, immediately close idle sockets,
and force-destroy any remaining sockets after a grace period. Ensure the
forced-close path invokes shutdown exactly once so the returned Promise resolves
and disposeContainerOnce can run, while preserving the existing immediate
shutdown and error handling behavior.

In `@apps/chain-indexer/src/services/start-server/start-server.ts`:
- Line 51: Remove the processEvents “exit” listener that invokes shutdown. Keep
the existing SIGTERM and SIGINT shutdown handling unchanged, since the async
shutdown function cannot complete from the synchronous exit event and the once
wrapper can suppress a later real shutdown.
- Around line 43-52: Update the server startup flow around serve() to attach an
error listener to the returned server before returning it. When a listen failure
is emitted, invoke shutdown with the received error so startup aborts and the
sync role does not continue indexing without a listener; preserve the existing
close and process signal handling.

In `@apps/chain-indexer/test/setup-unit-tests.ts`:
- Around line 4-9: Update the afterAll teardown around container.dispose() to
handle the already-disposed case explicitly, while allowing all other disposal
errors to propagate. Remove the blanket suppression and rethrow unexpected
errors so cleanup failures cause the tests to fail.

---

Nitpick comments:
In `@apps/chain-indexer/src/db/schema.ts`:
- Around line 58-62: Update IndexerState to use cosmosSchema.table instead of
pgTable so indexer_state is created in the cosmos schema consistently with the
other tables. Regenerate the associated migration and schema snapshot to reflect
the corrected table location.

In `@apps/chain-indexer/src/pipeline/block-committer.service.ts`:
- Around line 33-41: Update the message-row construction in the block commit
flow to validate the result of typeIds.get(message.typeUrl) before assigning
typeId. Throw an explicit error that includes the missing message.typeUrl when
no ID exists, and remove the unsafe “as number” assertion while preserving
numeric IDs for successful lookups.

In `@apps/chain-indexer/src/pipeline/block-decoder.service.ts`:
- Around line 38-51: Update `#decodeTransaction` and its decode caller to
explicitly handle decodeTxRaw failures so one malformed transaction cannot
silently stall block synchronization. Prefer persisting the transaction with
empty messages and a clear decode-failure marker while retaining its index,
hash, fee, and execution metadata; otherwise propagate the error only through an
actionable operator alert and ensure the checkpoint still follows the intended
policy.

In `@apps/chain-indexer/src/pipeline/sync-runner.service.ts`:
- Around line 140-154: Update `#resolveStartHeight` so that when state exists but
checkpointBlock is absent, it logs a warning describing the missing persisted
block and checkpoint height before setting `#lastHash` to null; preserve the
existing restart behavior and parent-hash fallback.

In `@apps/chain-indexer/src/providers/logging.provider.ts`:
- Line 4: Update LoggerService to use tsyringe’s singleton scope: import
singleton instead of injectable and replace the class decorator with
`@singleton`(), preserving the existing container registration and service
behavior.

In `@apps/chain-indexer/src/rpc/rpc-client-pool.service.ts`:
- Around line 57-76: Update `#get` to perform one bounded retry pass across RPC
candidates after a short backoff before throwing AggregateError, preserving node
health and inFlight bookkeeping for every attempt. In `#fetchFromNode`, consume or
cancel the response body on non-OK responses before propagating the failure so
connections can be reused.

In `@apps/chain-indexer/tsconfig.json`:
- Around line 2-4: Update the include setting in the app tsconfig to ["**/*.ts"]
so IDE diagnostics cover all TypeScript files, including tests and root-level
files. Leave the existing exclude entries and the narrow tsconfig.build.json
configuration unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 059b2ee9-5677-4128-a798-afa1b8361792

📥 Commits

Reviewing files that changed from the base of the PR and between de09757 and 3f3323b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (51)
  • apps/chain-indexer/README.md
  • apps/chain-indexer/drizzle.config.ts
  • apps/chain-indexer/drizzle/0000_nappy_james_howlett.sql
  • apps/chain-indexer/drizzle/meta/0000_snapshot.json
  • apps/chain-indexer/drizzle/meta/_journal.json
  • apps/chain-indexer/env/.env.sample
  • apps/chain-indexer/eslint.config.mjs
  • apps/chain-indexer/package.json
  • apps/chain-indexer/src/app.ts
  • apps/chain-indexer/src/config/env.config.ts
  • apps/chain-indexer/src/db/bytea.ts
  • apps/chain-indexer/src/db/pg-client.service.ts
  • apps/chain-indexer/src/db/schema.ts
  • apps/chain-indexer/src/http-schemas/healthz.schema.ts
  • apps/chain-indexer/src/http-schemas/status.schema.ts
  • apps/chain-indexer/src/index.ts
  • apps/chain-indexer/src/lib/create-route/create-route.ts
  • apps/chain-indexer/src/pipeline/block-committer.service.ts
  • apps/chain-indexer/src/pipeline/block-decoder.service.spec.ts
  • apps/chain-indexer/src/pipeline/block-decoder.service.ts
  • apps/chain-indexer/src/pipeline/canonical-json.spec.ts
  • apps/chain-indexer/src/pipeline/canonical-json.ts
  • apps/chain-indexer/src/pipeline/decoded-block.ts
  • apps/chain-indexer/src/pipeline/sync-runner.service.ts
  • apps/chain-indexer/src/providers/app-config.provider.ts
  • apps/chain-indexer/src/providers/db.provider.ts
  • apps/chain-indexer/src/providers/index.ts
  • apps/chain-indexer/src/providers/logging.provider.ts
  • apps/chain-indexer/src/providers/raw-app-config.provider.ts
  • apps/chain-indexer/src/providers/type-registry.provider.ts
  • apps/chain-indexer/src/routes/healthz/healthz.router.ts
  • apps/chain-indexer/src/routes/index.ts
  • apps/chain-indexer/src/routes/status/status.router.ts
  • apps/chain-indexer/src/rpc/rpc-client-pool.service.spec.ts
  • apps/chain-indexer/src/rpc/rpc-client-pool.service.ts
  • apps/chain-indexer/src/rpc/rpc-types.ts
  • apps/chain-indexer/src/server.ts
  • apps/chain-indexer/src/services/app-config/app-config.service.ts
  • apps/chain-indexer/src/services/config/config.service.ts
  • apps/chain-indexer/src/services/hono-error-handler/hono-error-handler.service.ts
  • apps/chain-indexer/src/services/open-api-hono-handler/open-api-hono-handler.ts
  • apps/chain-indexer/src/services/shutdown-server/shutdown-server.ts
  • apps/chain-indexer/src/services/start-server/start-server.ts
  • apps/chain-indexer/src/services/status/status.service.ts
  • apps/chain-indexer/src/types/app-context.ts
  • apps/chain-indexer/test/setup-unit-env.ts
  • apps/chain-indexer/test/setup-unit-tests.ts
  • apps/chain-indexer/tsconfig.build.json
  • apps/chain-indexer/tsconfig.json
  • apps/chain-indexer/tsup.config.ts
  • apps/chain-indexer/vitest.config.ts

Comment thread apps/chain-indexer/src/config/env.config.ts Outdated
Comment thread apps/chain-indexer/src/index.ts Outdated
Comment thread apps/chain-indexer/src/pipeline/block-committer.service.ts Outdated
Comment thread apps/chain-indexer/src/pipeline/block-decoder.service.ts
Comment thread apps/chain-indexer/src/pipeline/canonical-json.ts Outdated
Comment thread apps/chain-indexer/src/services/hono-error-handler/hono-error-handler.service.ts Outdated
Comment thread apps/chain-indexer/src/services/shutdown-server/shutdown-server.ts
Comment thread apps/chain-indexer/src/services/start-server/start-server.ts Outdated
Comment thread apps/chain-indexer/src/services/start-server/start-server.ts Outdated
Comment thread apps/chain-indexer/test/setup-unit-tests.ts Outdated
Comment thread apps/chain-indexer/src/config/env.config.ts Outdated
Comment thread apps/chain-indexer/src/index.ts
Comment thread apps/chain-indexer/src/providers/type-registry.provider.ts
Comment thread apps/chain-indexer/src/providers/type-registry.provider.ts Outdated
Comment thread apps/chain-indexer/src/pipeline/sync-runner.service.ts
@baktun14
baktun14 marked this pull request as draft August 11, 2026 12:17
@baktun14 baktun14 changed the title feat(indexer): scaffold chain-indexer app with sandbox sync tracer bullet feat(indexer): chain-indexer app (indexer v2) Aug 11, 2026
- fail bootstrap when the HTTP server cannot start and exit the process
  when the sync loop dies so healthz cannot report healthy on a dead sync
- detect advisory-lock session loss via backend pid checks to prevent
  two sync leaders after a transparent reconnect
- retry transient sync errors with capped backoff; continuity breaks and
  leadership loss stay fatal
- serialize migrations with a pg advisory lock
- select existing message types before inserting to stop id sequence
  burn; widen message_types.id to serial and messages.type_id to integer
- fail fast when txs_results does not match block txs instead of
  persisting txs as successful with zero gas
- serialize Buffers as base64 in canonical json despite Buffer#toJSON
- treat empty SYNC_START_HEIGHT as absent; validate PORT range
- force-close keep-alive sockets on shutdown; handle async listen errors;
  drop the no-op exit listener
- return a constant message for unexpected errors; log via LoggerService
- remove dead InjectTypeRegistry export and inline comments
* feat(indexer): add backfill range and tuning env config

* feat(indexer): support ordered batched commits with a parameterized checkpoint stream

* feat(indexer): add backfill range planner

* feat(indexer): implement the backfill role for historical catch-up

* fix(indexer): address backfill review findings

- attach a no-op catch to prefetched blocks so a rejection settling
  before its height is consumed cannot crash as an unhandled rejection
- check range completion before tip validity so a finished range stays
  a no-op even when a lagging node reports a stale tip
- extract the shared PgAdvisoryLeaderLock used by both runners
- use lodash chunk instead of a local helper
- build the pg client test double with a nested mock instead of a cast

* refactor(indexer): share retry backoff and runner-role lifecycle across roles

- extract retryWithBackoff and use it in both runners
- retry backfill startup reads and idempotent batch commits so a
  transient blip no longer kills a multi-hour job
- collapse the sync and backfill bootstrap cases into one helper

* refactor(indexer): move continuity error and transient retry policy to shared modules

* refactor(indexer): retry the continuity seed read like other transient reads

* refactor(indexer): drop advisory-lock leader election for concurrency-safe writes

Inserts were already idempotent; making the indexer_state checkpoint upsert
monotonic (GREATEST) makes overlapping writers harmless, so the leader lock,
its backend-pid liveness checks, and LeadershipLostError can all go. Single
writers per role are enforced at the orchestration level instead.

* refactor(indexer): use the exported LoggerService type instead of a local alias

* fix(indexer): retry an interrupted backfill instead of completing the Job

Addresses review findings on the backfill role:

- A backfill stopped before finishing its range (SIGTERM mid-run) now throws
  RunnerInterruptedError so the process exits non-zero. Backfill runs as a K8s
  Job where exit 0 marks it Complete, so the previous clean exit silently left
  the range unfinished and unretried; it now resumes from its checkpoint on the
  next attempt.
- Drop the unreachable prefetch fallback in the consume loop; the sliding window
  always has the current height inflight before it is awaited.
- Back the retry delay with the global setTimeout so the fetch-retry test can
  advance it with fake timers instead of sleeping a real second.

* fix(indexer): surface env validation errors without the DI wrapper

A misconfigured role (e.g. a backfill Job missing BACKFILL_FROM/TO_HEIGHT)
failed with a tsyringe "Cannot inject the dependency" wrapper around the
ZodError plus an internal stack trace. Validate env eagerly at bootstrap and
log a CONFIG_INVALID event listing the offending fields and messages instead.

* refactor(indexer): parallelize backfill startup reads and signal completion via return

Fetch the checkpoint height and chain tip concurrently instead of serially, hoist the

loop-invariant base height in the contiguity check, and have the backfill run return whether

it completed so start() no longer re-reads the stopped flag.

* fix(indexer): track backfill completion by committed height, not the stopped flag

A shutdown landing during the final commit left the range fully committed but reported it as

interrupted, failing the Job and triggering a spurious retry. Completion is now derived from

the last committed height reaching the range end. Also dedupes the identical getTipHeight

helper into RpcClientPool so both runners share one implementation.
* feat(indexer): add raw block archive layout and zstd ndjson codec

* feat(indexer): add gcs block archive service behind optional ARCHIVE_BUCKET

* feat(indexer): archive staged raw blocks during live sync before commit

* feat(indexer): source backfill blocks from the archive and compact ranges

* test(indexer): cover the chain-id failure path in staged deletes

* feat(indexer): allow overriding the gcs api endpoint for local emulators

* chore(indexer): persist the chain-indexer local verify recipe

* refactor(indexer): tighten archive invariants after code review

* fix(indexer): stop malformed /status from poisoning the chain-id cache

resolveChainId cached getStatus().then(onFulfilled, onRejected); a throw
inside onFulfilled (a /status body missing node_info) rejected the cached
promise without running the reset, so every later archive call failed with
the stale rejection forever. Switch to .then().catch() so both a rejected
fetch and a malformed payload clear the cache and let the retry re-fetch.

Also share a fetchRawBlock helper between sync and backfill, free each
range's buffer as soon as its chunk flush succeeds, and document the
staged-single orphan window left by a crash between chunk put and delete.

* docs(indexer): scope chain-indexer to chain data only

State explicitly that the new chain-indexer owns chain-derived data
only, and that off-chain provider data (status pinging, inventory,
uptime, IP geolocation, GPU breakdown) belongs to apps/provider-inventory.
Pricing and Keybase stay as the deliberate off-chain exception since the
daily USD aggregates and validator records need them.

Note the pending ownership move on the provider-inventory "provider
snapshot" glossary entry.

* fix(indexer): stop a corrupt chunk from poisoning the fetch cache

ArchiveBlockSource#fetchChunk cached getChunk().then(onFulfilled, onRejected);
a throw inside onFulfilled (a corrupt chunk line decoding to a non-object so
record.height throws) rejected the cached promise without running the reset,
so every later read for that range awaited the stale rejection. Switch to
.then().catch() so a mapping failure also clears entry.chunkFetch and the
retry re-fetches, matching the sibling fix in resolveChainId.

Also hoist the duplicated buildRecord test fixture into
test/fakes/build-raw-block-record so the three archive specs share one shape.
@socket-security

socket-security Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​google-cloud/​storage@​7.21.09810010088100

View full report

…#3587)

* feat(indexer): add genesis import framework with account and balance seed

Balance tracking is only trustworthy from a network's genesis, so this
seeds genesis state before the first block: accounts, per-denom balances
(recorded as genesis-reason ledger entries), validators, and staking
delegations, all in one transaction.

It is gated behind GENESIS_IMPORT (default off). When enabled, the sync
role runs a per-module seeder framework exactly once, using a genesis
checkpoint in indexer_state so a restart is a no-op, and it rejects a
fresh start that is not at the network's genesis height. Genesis is
fetched over RPC /genesis_chunked and its chain_id is asserted against
the node being indexed.

This adds the accounts, account_balances, balance_changes, validators,
and delegations tables (L-3); the ongoing per-block ledger lands in L-4.

Refs CON-805

* refactor(indexer): share genesis validator and chunked-insert helpers

Extract mapDescription/mapCommissionRates so the staking and gentx validator
mappers stop duplicating the description and commission field mapping, and add
an insertChunked helper the bank and staking seeders call instead of
hand-rolling the chunked insert loop.

* fix(indexer): warn when genesis import is enabled too late to seed

The seed only runs on a fresh start, so turning GENESIS_IMPORT on after the
indexer already has a sync checkpoint silently skips it and leaves the account
and balance tables empty with no signal. Add a hasSeeded check on resume and
log a warning so the operator knows genesis was never backfilled.
* feat(indexer): capture block/tx events and ledger idempotency schema

Add the L-4 write-path foundation: a 0002 migration adding the
(height, event_index) idempotency key and tx_index to balance_changes,
the account_txs address-activity table, and a staking balance reason.
The block decoder now captures coin/transfer/mint/burn/slash events with
base64-normalized attributes and derives per-tx signer addresses, the
enabler for deriving balances from events. Genesis ledger rows move to
initialHeight-1 so they never collide with block initialHeight's events.

* feat(indexer): derive balance changes from coin events

Add the pure, DB-free derivation layer: parseCoins for coin strings,
a module-address registry deriving Cosmos SDK module accounts by name
(sha256(name)[:20]), an MVP reason classifier, and the balance deriver
that turns coin_spent/coin_received into ordered movements with a
deterministic block-wide event_index and correlated counterparties.

* feat(indexer): intern accounts and write the balance ledger idempotently

Add the account interner (resolve-or-create, no permanent cache) and the
balance writer. The writer seeds each running balance from the ledger
baseline (not the snapshot), appends balance_changes on the
(height, event_index) unique key, and advances account_balances additively
by only the rows it actually inserted — so overlapping sync/backfill
writers apply every delta exactly once and re-commits are no-ops.

* feat(indexer): write balance ledger and activity log per block

Wire balance derivation into the block committer: derive changes and
address-activity rows per block, intern all touched addresses on the base
connection, then inside the commit transaction write the balance ledger
and account_txs after messages and before the checkpoint. Both sync and
backfill flow through commitBatch, so both persist the ledger.

* feat(indexer): add reconciliation script proving ledger matches chain

Add abci_query historical support to the RPC pool and a one-shot
npm run reconcile entrypoint: at the sync checkpoint height it compares
each sampled account's current balance against the node's bank balance
and the ledger's per-denom totals against chain supply, exiting non-zero
on any mismatch so it can gate a deploy.

* fix(indexer): correct balance reason classification and reconcile guard

Scope the slash reason to the coincident burn leg so a validator slash no
longer mislabels the block's routine inflation mint (and other block-level
movements sharing the scope) as slash. Add the missing mint case to the
reason classifier so a mint-module counterparty resolves to mint instead of
falling through to transfer.

Reject a non-numeric RECONCILE_SAMPLE_SIZE instead of coercing it to NaN,
which slice() turned into an empty sample that could report a clean pass
having verified zero accounts.

Drop the unread "message" event type from the decoder whitelist and remove
a dead branch in decodeIfBase64.

* fix(indexer): classify escrow module movements as escrow

The reason classifier fell through to `transfer` for any coin movement
involving the x/escrow module account, because that module wasn't in the
address registry. It mislabeled every ACT (uact) flow — which only moves
through escrow, never peer-to-peer — plus uakt escrow deposits, refunds,
and lease settlements, as generic transfers.

Add the escrow module to the registry and map it to the existing `escrow`
reason. Verified against a mainnet day of blocks: uact movements now
classify as escrow (previously 100% transfer) while per-account and total
supply balances still reconcile exactly to the chain.

* refactor(indexer): use insertChunked for block, transaction and message inserts

* fix(indexer): scope slash reason, correlate counterparty by amount, fail over abci non-zero code

* fix(indexer): reconcile from a consistent snapshot with bounded concurrency

Read the checkpoint height and ledger balances inside one REPEATABLE READ
read-only transaction so a concurrent block commit can't pair a stale height
with post-commit balances and flag spurious mismatches.

Fan out the per-account bank queries with bounded concurrency instead of one
sequential RPC round-trip at a time, letting the RpcClientPool load-balance
the sampled accounts.

* fix(indexer): classify balance reason by the holder's own module role first

For a module-to-module movement (fee_collector to distribution, mint to
fee_collector) both sides carry a role, so preferring the counterparty tagged
each leg with the other module's reason. Prefer the holder's own role and fall
back to the counterparty, so every leg is classified by the module whose
balance actually changed. Account-to-module movements are unaffected: the
account side has no role and still falls through to the counterparty.

* fix(indexer): chunk account existence lookup and document ledger

Chunk the account existence SELECT the same way the insert path already does,
so a batch with tens of thousands of distinct addresses can't exceed
postgres.js's bind-parameter limit and abort the commit transaction.

Document the balance ledger, activity log, and the reconcile script in the
app README alongside the existing feature sections.

* refactor(indexer): compute each account's total once when sampling

Precompute the coin total per account before sorting the reconciliation
sample instead of re-reducing both operands on every comparator call.

* fix(indexer): chunk balance baseline lookup under the bind-parameter limit

#readLedgerBaseline built accountIds/denoms from a whole commitBatch's
intents and passed them into one unchunked inArray SELECT, unlike
#insertChanges/#applyNetDeltas in the same class. A dense backfill batch
near BACKFILL_BATCH_SIZE's 1000-block max can touch tens of thousands of
distinct accounts, exceeding postgres.js's ~65k bind-parameter cap and
aborting the whole commitBatch transaction. Chunk accountIds by
INSERT_CHUNK_SIZE and merge per-chunk rows, matching AccountInterner's
already-chunked select.

* refactor(indexer): validate RECONCILE_SAMPLE_SIZE through the env schema

The reconcile CLI hand-rolled Number() parsing plus a bespoke
Number.isInteger/positive check and its own CONFIG_INVALID log shape,
duplicating the zod envSchema every other numeric env var already flows
through. Add RECONCILE_SAMPLE_SIZE to envSchema as an optional coerced
positive int (same idiom as SYNC_START_HEIGHT) and read it from
parsed.data, so an invalid value fails at the existing safeParse path.
The service keeps its own guard as defense in depth.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant