Skip to content

Support chain IDs above int32 max with configurable storage mode - #1507

Merged
DZakh merged 7 commits into
mainfrom
claude/chain-id-mode-param-ndswh8
Jul 29, 2026
Merged

Support chain IDs above int32 max with configurable storage mode#1507
DZakh merged 7 commits into
mainfrom
claude/chain-id-mode-param-ndswh8

Conversation

@DZakh

@DZakh DZakh commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

Add support for blockchain networks with chain IDs above the int32 maximum (2,147,483,647), such as Tron Shasta (2,494,104,990). The system now automatically detects the widest chain ID in the config and selects appropriate storage modes (int32 or int64) for database columns and generated code.

Key Changes

  • ChainId module (packages/envio/src/ChainId.res): New runtime representation for chain IDs as floats (JS numbers) with a mode type (Int32 | Int64) that tracks the widest scalar needed. Includes validation against Number.MAX_SAFE_INTEGER (9,007,199,254,740,991) and schema for parsing BIGINT columns returned as strings from PostgreSQL.

  • Config resolution (packages/cli/src/config_parsing/system_config.rs): ChainIdMode::resolve() inspects all active chains and selects Int32 if all IDs fit in i32, otherwise Int64. Rejects IDs above MAX_SAFE_INTEGER. The mode is serialized into the public config so resume operations against incompatible schemas are rejected.

  • Database schema generation:

    • Table.res: New ChainId field type that resolves to Integer or BigInt based on mode
    • PgStorage.res: All table creation and insert queries now accept ~chainIdMode parameter
    • ClickHouse.res: Chain ID columns map to Int32 or UInt64 based on mode
    • InternalTable.res: Chains and Checkpoints tables use ChainId field type; address/event reads normalize BIGINT strings through ChainId.normalizeOrThrow
  • Generated indexer code (packages/cli/src/hbs_templating/codegen_templates.rs):

    • Int32 mode: Chain ID type remains a ReScript polyvariant (#chainId) with exhaustive pattern matching in getChainById
    • Int64 mode: Chain ID type falls back to opaque ChainId.t (JS number) since ReScript polyvariants are int32-bound; getChainById looks up chains by stringified ID on the record
  • Config compatibility (Config.res): Added chainIdMode field to public config; diffPaths and throwIfIncompatible detect mode mismatches and reject resume with standard incompatible-config error.

  • Tests (packages/envio-tests/test/lib_tests/ChainIdMode_test.res): Comprehensive test suite covering mode resolution, schema generation for Postgres/ClickHouse, runtime representation, TypeScript surface, and config compatibility checks.

Notable Implementation Details

  • Chain IDs are represented as floats at runtime (JS numbers) rather than ReScript int to avoid truncation. The ChainId.intSchema allows modules still typed as int to work safely since chain IDs are only compared and stringified, never used in int32 arithmetic.
  • BIGINT columns from PostgreSQL come back as strings; ChainId.normalizeOrThrow validates and converts them to the runtime representation.
  • The mode is determined once during config parsing and carried through to code generation and schema creation, ensuring consistency across the entire indexer lifecycle.
  • Older configs without the chainIdMode field default to Int32 (all IDs they can express fit in INTEGER).

https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6

Summary by CodeRabbit

  • New Features
    • Added chainIdMode to the public indexer configuration to control 32-bit vs 64-bit chain ID handling.
    • Code generation, TypeScript/ReScript typings, and chain_id persistence now adapt to the selected mode (Postgres + ClickHouse).
    • The API can additionally return generated indexer code (indexerCode).
  • Bug Fixes
    • Rejects chain IDs that can’t be safely represented without loss and improves large-value normalization.
  • Compatibility
    • Resume/incompatibility checks now include chainIdMode to prevent incorrect restarts.

Chain ids beyond 2^31-1 (Tron Shasta 2494104990, Nile 3448148188) were
rejected by the `S.int` config schema and would not fit the INTEGER
columns the internal tables declare.

The CLI now derives a `ChainIdMode` from the maximum active chain id
(`<= i32::MAX` -> Int32, otherwise Int64) and emits it as `chainIdMode`
in the public config, which is also the persisted envio_info
fingerprint. It sits in its own diff tier, so a resume against a schema
built for the other mode fails with the standard incompatible-config
message instead of silently truncating ids. Ids above
Number.MAX_SAFE_INTEGER are rejected at parse time.

At runtime a new `ChainId` module carries the float-backed
representation plus the validating schema, which also normalizes the
strings Postgres BIGINT and ClickHouse UInt64 columns return. The
internal tables declare their chain-id columns with a `ChainId` field
type that resolves to INTEGER/Int32 or BIGINT/UInt64 from the mode, so
they stay module-level constants and small-id projects keep generating
identical DDL.

Generated APIs are unchanged for small ids; a wide config falls back to
`type chainId = ChainId.t` in ReScript (integer polyvariants are
int32-bound) while TypeScript keeps its numeric literal union.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Chain IDs now resolve to Int32 or Int64 modes, flow through public configuration and generated indexer code, use validated runtime normalization, and select matching Postgres and ClickHouse column and query types.

Changes

Chain ID mode support

Layer / File(s) Summary
Mode resolution and runtime contract
packages/cli/src/config_parsing/*, packages/envio/src/ChainId.res*, packages/envio/src/ChainMap.res, packages/envio/src/Config.res
Configuration resolves the widest supported chain ID, exposes chainIdMode, and adds validated ChainId runtime types and schemas.
Generated chain ID API
packages/cli/src/hbs_templating/codegen_templates.rs, packages/cli/src/napi.rs, packages/envio/src/Core.res
Generated typings and getChainById use literal mappings for Int32 and string-keyed lookups for Int64; generated indexer code is returned through the API.
Runtime ChainId propagation
packages/envio/src/{Internal,ChainState,CrossChainState,EventProcessing,FetchState,Metrics,...}.res
Runtime records, dictionaries, source payloads, metrics, simulation, rollback, and chain-state APIs use ChainId.t and string-based keying.
Database and storage propagation
packages/envio/src/db/*, packages/envio/src/bindings/ClickHouse.res, packages/envio/src/PgStorage.res, packages/envio/src/Sink.res
Schemas, SQL casts, checkpoint operations, initialization, batch writes, ClickHouse setup, and resume paths receive the configured chain ID mode.
Validation and migration tests
packages/envio-tests/test/lib_tests/ChainIdMode_test.res, scenarios/test_codegen/test/**/*
Tests validate resolution, safe parsing, generated types, database schemas, query casts, compatibility reporting, and migrated ChainId fixtures.

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

Sequence Diagram(s)

sequenceDiagram
  participant ConfigParser
  participant PublicConfig
  participant CodeGenerator
  participant Runtime
  participant Storage
  participant Database
  ConfigParser->>ConfigParser: Resolve ChainIdMode from configured chains
  ConfigParser->>PublicConfig: Serialize chainIdMode
  PublicConfig->>CodeGenerator: Provide Int32 or Int64 mode
  CodeGenerator->>Runtime: Generate matching chain ID types and lookups
  Runtime->>Storage: Pass ChainId values and chainIdMode
  Storage->>Database: Create schemas and queries using mode-specific types
Loading

Possibly related PRs

  • enviodev/hyperindex#1501: Exposes generated Indexer.res text through the code-generation API, overlapping with this PR’s generated artifact changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: supporting chain IDs above int32 via configurable storage mode.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/envio/src/PgStorage.res (1)

600-621: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Scope cached batch SQL by chainIdMode.

setQueryCache is keyed only by table, but newQuery embeds mode-specific casts such as integer[] or bigint[]. A later PgStorage.make using Int64 can reuse an Int32 query for the same table and reject wide IDs. Scope this cache to the storage instance, or include pgSchema and chainIdMode in its key.

🤖 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 `@packages/envio/src/PgStorage.res` around lines 600 - 621, Update
setQueryCache usage in setOrThrow so cached batch SQL is scoped by pgSchema and
chainIdMode, or move the cache to the storage instance. Ensure
makeTableBatchSetQuery results using mode-specific casts are never reused across
different schemas or chain-ID modes for the same table.
🤖 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 `@packages/cli/src/hbs_templating/codegen_templates.rs`:
- Around line 1340-1353: Update the chain ID generation around chain_id_type and
the exhaustive switch to derive Int32 cases only from active, non-skipped
chains, matching the filtering used during mode resolution. Ensure skipped wide
IDs are excluded from both generated lists, and add a regression test covering
an active id: 1 with skipped id: 2494104990.

In `@packages/envio/src/bindings/ClickHouse.res`:
- Line 132: Update the ChainId branch in the ClickHouse value schema to apply
the same nullable and array wrapping logic used by the Date and UInt52 branches,
based on f.isNullable and f.isArray, while retaining ChainId.intSchema as the
underlying scalar schema.

In `@packages/envio/src/ChainId.res`:
- Around line 31-45: Update the string-handling branch in the schema preprocess
parser so it validates that the entire input string is a numeric representation
before calling Float.parseFloat. Reject strings containing trailing non-numeric
characters, such as "2494104990junk", while preserving the existing safe-integer
and non-negative range checks for valid values.

In `@packages/envio/src/Config.res`:
- Around line 1276-1286: Normalize both configuration values in the
compatibility diff flow so an omitted chainIdMode is treated as the
Int32/"int32" mode before diffPaths compares them. Update the logic around the
visible chainIdMode diff tier, preserving explicit modes and ensuring legacy
configs without the field match equivalent configs that specify int32.

---

Outside diff comments:
In `@packages/envio/src/PgStorage.res`:
- Around line 600-621: Update setQueryCache usage in setOrThrow so cached batch
SQL is scoped by pgSchema and chainIdMode, or move the cache to the storage
instance. Ensure makeTableBatchSetQuery results using mode-specific casts are
never reused across different schemas or chain-ID modes for the same table.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2a3cdb8c-1e76-4abe-9239-7c262f7b3bd0

📥 Commits

Reviewing files that changed from the base of the PR and between 6284551 and f067f9a.

⛔ Files ignored due to path filters (7)
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_generated_for_evm.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_generated_for_fuel.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_generated_for_svm.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_with_all_options.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_with_lowercase_contract_name.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_with_multiple_contracts.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__internal_config_json_code_with_no_contracts.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • packages/cli/src/config_parsing/public_config.rs
  • packages/cli/src/config_parsing/system_config.rs
  • packages/cli/src/hbs_templating/codegen_templates.rs
  • packages/envio-tests/test/lib_tests/ChainIdMode_test.res
  • packages/envio/src/ChainId.res
  • packages/envio/src/ChainId.resi
  • packages/envio/src/ChainMap.res
  • packages/envio/src/Config.res
  • packages/envio/src/PgStorage.res
  • packages/envio/src/Sink.res
  • packages/envio/src/bindings/ClickHouse.res
  • packages/envio/src/db/InternalTable.res
  • packages/envio/src/db/Table.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res

Comment on lines +1340 to +1353
// ReScript integer polyvariants (`#137`) are int32-bound, so a config
// with a wider id falls back to the opaque runtime representation.
// TypeScript keeps its numeric literal union either way.
let chain_id_type = match cfg.chain_id_mode {
ChainIdMode::Int64 => "type chainId = ChainId.t".to_string(),
ChainIdMode::Int32 => format!(
"type chainId = [{}]",
chain_id_cases
.iter()
.map(|chain_id_case| format!("#{}", chain_id_case))
.collect::<Vec<_>>()
.join(" | "),
),
};

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude skipped chains from generated Int32 cases.

Mode resolution ignores skipped chains, but chain_id_cases and the exhaustive switch include them. An active id: 1 plus skipped id: 2494104990 selects Int32 then emits #2494104990, which ReScript cannot compile. Build both generated Int32 lists from active chains, and add this skipped-wide-ID regression case.

Also applies to: 1485-1521

🤖 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 `@packages/cli/src/hbs_templating/codegen_templates.rs` around lines 1340 -
1353, Update the chain ID generation around chain_id_type and the exhaustive
switch to derive Int32 cases only from active, non-skipped chains, matching the
filtering used during mode resolution. Ensure skipped wide IDs are excluded from
both generated lists, and add a regression test covering an active id: 1 with
skipped id: 2494104990.

dateSchema
}
}
| ChainId => ChainId.intSchema->S.toUnknown

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle nullable and array ChainId fields in the ClickHouse value schema.

DDL uses f.isNullable and f.isArray, but this branch always uses scalar ChainId.intSchema. Nullable or array ChainId entity values will fail conversion before insertion. Apply the same wrapping used for Date/UInt52.

Proposed fix
-          | ChainId => ChainId.intSchema->S.toUnknown
+          | ChainId => {
+              let chainIdSchema = ChainId.intSchema->S.toUnknown
+              if f.isNullable {
+                S.null(chainIdSchema)->S.toUnknown
+              } else if f.isArray {
+                S.array(chainIdSchema)->S.toUnknown
+              } else {
+                chainIdSchema
+              }
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| ChainId => ChainId.intSchema->S.toUnknown
| ChainId => {
let chainIdSchema = ChainId.intSchema->S.toUnknown
if f.isNullable {
S.null(chainIdSchema)->S.toUnknown
} else if f.isArray {
S.array(chainIdSchema)->S.toUnknown
} else {
chainIdSchema
}
}
🤖 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 `@packages/envio/src/bindings/ClickHouse.res` at line 132, Update the ChainId
branch in the ClickHouse value schema to apply the same nullable and array
wrapping logic used by the Date and UInt52 branches, based on f.isNullable and
f.isArray, while retaining ChainId.intSchema as the underlying scalar schema.

Comment on lines +31 to +45
let schema: S.t<t> = S.float->S.preprocess(s => {
parser: value => {
let number = switch value->typeof {
| #string => value->(Utils.magic: unknown => string)->Float.parseFloat
| _ => value->(Utils.magic: unknown => float)
}
if !isSafeInteger(number) || number < 0. {
s.fail(
`Expected a chain id between 0 and ${maxSafe->Float.toString}, received ${value->(
Utils.magic: unknown => string
)}`,
)
}
number
},

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In ReScript 12, does Float.parseFloat("123junk") accept the numeric prefix, and what documented API is recommended for strict numeric-string validation?

💡 Result:

In ReScript 12 (as in previous versions), Float.parseFloat("123junk") does accept the numeric prefix and returns 123.0 [1][2]. This function maintains the underlying behavior of JavaScript's global parseFloat, which parses the string from left to right, stops at the first character that cannot be part of a valid numeric literal, and ignores all subsequent characters [3][4][5]. The official ReScript documentation explicitly recommends using fromString (available in @rescript/core or Belt) for scenarios where you need to ensure the input represents a valid float and avoid the lenient, potentially error-prone behavior of parseFloat [1][2][6]. Unlike parseFloat, fromString returns an option (e.g., option<float>), providing None if the input cannot be fully or correctly parsed as a number [6][7]. For strict validation requirements where you must ensure a string is formatted entirely as a valid number, the community standard is to use a schema validation library such as rescript-struct [8] or rescript-schema [9]. These libraries allow you to define stricter rules, such as using regular expressions (e.g., S.string->S.pattern(%re("/^-?\d+(\.\d+)?$/"))) to validate that the entire input matches a strictly numeric format before proceeding with conversion [5][9][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/envio/src/ChainId.res =="
wc -l packages/envio/src/ChainId.res
sed -n '1,120p' packages/envio/src/ChainId.res

echo
echo "== parseFloat behavior probe =="
node - <<'JS'
for (const v of ["2494104990junk", "2494104990", "12.3abc", "abc", "9007199254740992junk"]) {
  const n = parseFloat(v);
  console.log(JSON.stringify({v, n, isSafeInteger: Number.isSafeInteger(n), nonNegative: n >= 0}));
}
JS

Repository: enviodev/hyperindex

Length of output: 2929


Reject strings with trailing junk before parsing. Float.parseFloat accepts numeric prefixes, so values like "2494104990junk" still pass the range check and normalize to a chain id. Require a full-string numeric check first.

🤖 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 `@packages/envio/src/ChainId.res` around lines 31 - 45, Update the
string-handling branch in the schema preprocess parser so it validates that the
entire input string is a numeric representation before calling Float.parseFloat.
Reject strings containing trailing non-numeric characters, such as
"2494104990junk", while preserving the existing safe-integer and non-negative
range checks for valid values.

Comment on lines +1276 to +1286
// chainIdMode sits right after version: it decides the physical type of
// every chain-id column, so a change to it is reported on its own rather
// than buried under the chain diffs that always accompany it.
let tiers = [
["version"],
["chainIdMode"],
["name"],
["storage"],
["evm", "fuel", "svm"],
["entities"],
]

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat a missing legacy mode as int32 during compatibility checks.

fromPublic defaults an absent field to Int32, but diffPaths compares raw JSON. Existing persisted configs without chainIdMode will therefore fail resume against a new equivalent config containing "chainIdMode": "int32". Normalize an omitted mode to int32 before diffing.

🤖 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 `@packages/envio/src/Config.res` around lines 1276 - 1286, Normalize both
configuration values in the compatibility diff flow so an omitted chainIdMode is
treated as the Int32/"int32" mode before diffPaths compares them. Update the
logic around the visible chainIdMode diff tier, preserving explicit modes and
ensuring legacy configs without the field match equivalent configs that specify
int32.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f067f9ab80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fn resolve(chains: &ChainMap) -> Result<Self> {
let max_id = chains
.values()
.filter(|chain| !chain.skip)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include skipped chains when selecting the codegen mode

When an active chain fits in int32 but a skipped chain has an ID such as 2494104990, this filter selects Int32; however, code generation intentionally includes skipped chains and emits that ID as a polyvariant pattern in getChainById. ReScript rejects the resulting #2494104990 value with an int32-range error, so the generated project cannot compile even though skip is documented as not affecting code generation. The mode selection (and maximum-safe-ID validation) therefore needs to account for every chain represented in generated code.

Useful? React with 👍 / 👎.

// than buried under the chain diffs that always accompany it.
let tiers = [
["version"],
["chainIdMode"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat an absent stored chain mode as int32

When an existing envio_info row predates chainIdMode but its version and remaining config otherwise match, the current generated config now contains "chainIdMode": "int32", and this tier treats the missing/present pair as incompatible. Resume consequently throws and asks the user to reset compatible data whose chain columns are already INTEGER. The fromPublic default does not help because diffPaths compares the raw JSON objects, so the stored side should be normalized to int32 before this comparison.

Useful? React with 👍 / 👎.

Comment thread packages/envio/src/ChainId.res Outdated
// The same runtime schema, typed for the modules that still annotate chain ids
// as `int`. Safe because ReScript's `int` is a JS number at runtime — chain ids
// are only ever compared and stringified, never used in int32 arithmetic.
let intSchema = schema->(Utils.magic: S.t<t> => S.t<int>)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Replace int32-only chain-ID string parsing

For any active chain ID above 2147483647, this schema exposes the wide runtime number as a value typed int, but production code still reparses chain-ID dictionary keys with ReScript's int32-limited Int.fromString. In EventProcessing.processEventBatch, both the started and finished logging paths call chainId->Int.fromString->Option.getUnsafe, so the first progressed batch for a wide chain throws before normal handler processing; TestIndexer and Internal.EffectCache.parseChainId contain the same conversion. These paths need a chain-ID-specific safe-number parser rather than relying on the int type cast.

Useful? React with 👍 / 👎.

`ChainId.t` was only used at the config/table boundary; every module in
between still annotated chain ids as `int`, which is exactly the type
that can't represent them. Those annotations are now `ChainId.t`, so the
compiler — not a comment — is what keeps a chain id from being treated
as an int32.

`ChainMap.Chain` is backed by it directly, and chain-keyed dictionaries
go through `ChainId.Dict` instead of the int-keyed `Utils.Dict` helpers
(which remain for the block-number-keyed dicts in FetchState and
ReorgDetection). `ChainId.intSchema` is gone — `schema` is the only one
left.

Two boundaries deliberately stay `int` so no user code changes:
`context.chain.id` (`Internal.chainInfo`) and `Envio.effectChain.id`.
`fromInt`/`toInt` are the identity at runtime and mark those crossings,
along with the int literals that construct chain ids in configs and
tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6

@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: 1

🤖 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 `@packages/envio/src/TestIndexer.res`:
- Around line 313-314: Update the chain ID handling in the block-range
configuration flow around parseBlockRange to parse each key once with
ChainId.normalizeOrThrow, avoiding Int.fromString and its range limitation.
Retain the parsed ChainId.t values for sorting and validation, and preserve the
existing invalid-ID error behavior.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d213ed63-3eb0-416a-83b3-4b318369f5a7

📥 Commits

Reviewing files that changed from the base of the PR and between f067f9a and d8a1703.

📒 Files selected for processing (80)
  • packages/envio-tests/test/ClientAddressFilter_test.res
  • packages/envio-tests/test/RateLimit_test.res
  • packages/envio-tests/test/ReorgDetection_test.res
  • packages/envio-tests/test/SvmHyperSyncSource_test.res
  • packages/envio-tests/test/UserApiValidation_test.res
  • packages/envio-tests/test/lib_tests/ChainState_materialize_test.res
  • packages/envio-tests/test/lib_tests/EffectCache_test.res
  • packages/envio-tests/test/lib_tests/Metrics_test.res
  • packages/envio/src/Batch.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainId.res
  • packages/envio/src/ChainId.resi
  • packages/envio/src/ChainMap.res
  • packages/envio/src/ChainMap.resi
  • packages/envio/src/ChainMetadata.res
  • packages/envio/src/ChainState.res
  • packages/envio/src/Config.res
  • packages/envio/src/ContractRegisterContext.res
  • packages/envio/src/CrossChainState.res
  • packages/envio/src/EventConfigBuilder.res
  • packages/envio/src/EventProcessing.res
  • packages/envio/src/FetchState.res
  • packages/envio/src/HandlerRegister.res
  • packages/envio/src/HandlerRegister.resi
  • packages/envio/src/IndexerState.res
  • packages/envio/src/Internal.res
  • packages/envio/src/LoadLayer.res
  • packages/envio/src/LogSelection.res
  • packages/envio/src/Main.res
  • packages/envio/src/Metrics.res
  • packages/envio/src/Persistence.res
  • packages/envio/src/PgStorage.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/RollbackCommit.res
  • packages/envio/src/SafeCheckpointTracking.res
  • packages/envio/src/SimulateDeadInputTracker.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/TestIndexer.res
  • packages/envio/src/UserContext.res
  • packages/envio/src/bindings/ClickHouse.res
  • packages/envio/src/db/EntityHistory.res
  • packages/envio/src/db/InternalTable.res
  • packages/envio/src/sources/Evm.res
  • packages/envio/src/sources/Fuel.res
  • packages/envio/src/sources/HyperSync.resi
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/SourceManager.resi
  • packages/envio/src/sources/Svm.res
  • packages/envio/src/tui/Tui.res
  • packages/envio/src/tui/components/CustomHooks.res
  • scenarios/fuel_test/test/FuelHyperSyncSourceHeight_test.res
  • scenarios/test_codegen/test/ChainMeta_test.res
  • scenarios/test_codegen/test/E2E_test.res
  • scenarios/test_codegen/test/EventBlockFilter_test.res
  • scenarios/test_codegen/test/EventFilters_test.res
  • scenarios/test_codegen/test/HandlerRegisterLifecycle_test.res
  • scenarios/test_codegen/test/IndexerStateStall_test.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/LoadLayer_test.res
  • scenarios/test_codegen/test/RpcSourceContract_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SourceBlockHashes_test.res
  • scenarios/test_codegen/test/__mocks__/MockConfig.res
  • scenarios/test_codegen/test/__mocks__/MockEvents.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res
  • scenarios/test_codegen/test/lib_tests/ChainState_test.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/DynamicContractsStartupSize_test.res
  • scenarios/test_codegen/test/lib_tests/EntityIdType_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res
  • scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res
  • scenarios/test_codegen/test/lib_tests/IndexerLoop_test.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res
  • scenarios/test_codegen/test/lib_tests/SameSignatureEventDecode_test.res
  • scenarios/test_codegen/test/lib_tests/SourceManager_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
  • scenarios/test_codegen/test/rollback/Rollback_test.res
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/envio/src/ChainMap.res
  • packages/envio/src/Config.res
  • packages/envio/src/db/InternalTable.res
  • packages/envio/src/bindings/ClickHouse.res
  • packages/envio/src/PgStorage.res

Comment thread packages/envio/src/TestIndexer.res Outdated
`Chain.t` was an alias for `ChainId.t`, and `makeUnsafe`/`toChainId`
were both `%identity` — a second name for the same type, with a
constructor that no longer constructed anything. Callers now pass
`ChainId.t` directly; `ChainId.fromInt` is the one way to make a chain
id from an int literal.

ChainMap keeps its Belt.Map wrapper, keyed on ChainId.t.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/envio/src/Internal.res (1)

367-368: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use ChainId.t for Internal.chainInfo.id
handlerContext.chain.id is read by generated handlers, and Int64 mode already allows chain ids above the ReScript int range. Keeping this field as int will truncate wide ids.

🤖 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 `@packages/envio/src/Internal.res` around lines 367 - 368, Change the
`Internal.chainInfo.id` field from `int` to `ChainId.t`, and update any directly
associated construction or access code to preserve the full chain ID without
narrowing or truncation. Keep the handler-facing `context.chain.id` behavior
intact while using the existing `ChainId.t` representation.

Source: MCP tools

🤖 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.

Outside diff comments:
In `@packages/envio/src/Internal.res`:
- Around line 367-368: Change the `Internal.chainInfo.id` field from `int` to
`ChainId.t`, and update any directly associated construction or access code to
preserve the full chain ID without narrowing or truncation. Keep the
handler-facing `context.chain.id` behavior intact while using the existing
`ChainId.t` representation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ebe4276-fd35-49e5-a94b-025c67fa03b4

📥 Commits

Reviewing files that changed from the base of the PR and between d8a1703 and dca49f7.

📒 Files selected for processing (48)
  • packages/envio-tests/test/ClientAddressFilter_test.res
  • packages/envio-tests/test/RateLimit_test.res
  • packages/envio-tests/test/SvmHyperSyncSource_test.res
  • packages/envio-tests/test/lib_tests/ChainIdMode_test.res
  • packages/envio-tests/test/lib_tests/ChainState_materialize_test.res
  • packages/envio/src/Batch.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/ChainMap.res
  • packages/envio/src/ChainMap.resi
  • packages/envio/src/ChainState.res
  • packages/envio/src/Config.res
  • packages/envio/src/ContractRegisterContext.res
  • packages/envio/src/CrossChainState.res
  • packages/envio/src/CrossChainState.resi
  • packages/envio/src/IndexerState.res
  • packages/envio/src/IndexerState.resi
  • packages/envio/src/Internal.res
  • packages/envio/src/Main.res
  • packages/envio/src/RawEvent.res
  • packages/envio/src/Rollback.res
  • packages/envio/src/SimulateDeadInputTracker.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/TestIndexer.res
  • packages/envio/src/sources/Evm.res
  • packages/envio/src/sources/EvmHyperSyncSource.res
  • packages/envio/src/sources/Fuel.res
  • packages/envio/src/sources/FuelHyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/SimulateSource.res
  • packages/envio/src/sources/Source.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/sources/Svm.res
  • packages/envio/src/sources/SvmHyperSyncSource.res
  • scenarios/fuel_test/test/FuelHyperSyncSourceHeight_test.res
  • scenarios/test_codegen/test/IndexerStateStall_test.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/RpcSourceContract_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/SourceBlockHashes_test.res
  • scenarios/test_codegen/test/__mocks__/MockConfig.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res
  • scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
🚧 Files skipped from review as they are similar to previous changes (32)
  • packages/envio/src/ContractRegisterContext.res
  • scenarios/fuel_test/test/FuelHyperSyncSourceHeight_test.res
  • scenarios/test_codegen/test/SourceBlockHashes_test.res
  • packages/envio-tests/test/lib_tests/ChainState_materialize_test.res
  • scenarios/test_codegen/test/lib_tests/FetchState_onBlock_test.res
  • scenarios/test_codegen/test/IndexerStateStall_test.res
  • scenarios/test_codegen/test/RpcSourceContract_test.res
  • packages/envio-tests/test/SvmHyperSyncSource_test.res
  • packages/envio/src/sources/Evm.res
  • scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res
  • packages/envio/src/sources/Svm.res
  • scenarios/test_codegen/test/lib_tests/FetchState_test.res
  • scenarios/test_codegen/test/rollback/ChainMocking.res
  • packages/envio-tests/test/RateLimit_test.res
  • scenarios/test_codegen/test/helpers/RpcSourcePins.res
  • packages/envio/src/Main.res
  • packages/envio/src/sources/Fuel.res
  • packages/envio/src/IndexerState.res
  • packages/envio/src/Batch.res
  • packages/envio/src/ChainFetching.res
  • packages/envio/src/CrossChainState.res
  • scenarios/test_codegen/test/helpers/MockIndexer.res
  • packages/envio/src/sources/SourceManager.res
  • packages/envio/src/ChainState.res
  • packages/envio-tests/test/lib_tests/ChainIdMode_test.res
  • packages/envio/src/SimulateDeadInputTracker.res
  • packages/envio/src/Config.res
  • scenarios/test_codegen/test/IndexerState_test.res
  • scenarios/test_codegen/test/lib_tests/PgStorage_test.res
  • packages/envio/src/Rollback.res
  • scenarios/test_codegen/test/lib_tests/CrossChainState_test.res
  • packages/envio/src/TestIndexer.res

Dead since the ChainId migration (or before it): `ChainId.toFloat`,
`ChainId.equal` (`===` works on the opaque type and is what callers
use), `ChainId.maxSafe`'s export, `ChainMap.set`/`entries`/`map`/`size`/
`update`, and `Utils.Dict.incrementByInt`. `ChainId.compare` now returns
`int`, which is what Belt's `cmp` wants — the only caller was undoing a
float.

Naming: a `ChainId.t` is now called `chainId` everywhere internal.
`Internal.item` spelled its field `chain` while `onBlockRegistration` in
the same file spelled it `chainId`, and `getItemChainId` existed to
bridge the two; sources, IndexerState, ChainFetching and CrossChainState
each picked their own. `IndexerState.chain`, an alias for `ChainId.t`,
is gone.

`context.chain` and `Config.chain` are untouched — the first is the
handler-facing API, the second is a record, not an id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cbba022b6c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

~pgSchema,
~table,
~itemSchema=itemSchema->S.toUnknown,
~chainIdMode,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Key cached batch queries by chain-ID mode

When the same Node process writes through an Int32 storage and later reinitializes the same schema/table in Int64 mode, setQueryCache is still keyed only by the module-level table object. Because chainIdMode is consulted only on a cache miss, the second write reuses the first query's INTEGER[] cast; a wide value such as 2494104990 is then rejected by PostgreSQL as out of range instead of being written to the new BIGINT column. Include the mode in the cache key or maintain separate cached queries per mode.

Useful? React with 👍 / 👎.

Int32 is what every config predating the field implies, so serializing
it changes the public config JSON — and with it the persisted envio_info
fingerprint — for every existing small-id project, which would demand a
reindex on upgrade for no reason. Skipping it on the default restores
the six config-JSON snapshots to byte-identical with main.

The generated ReScript chainId type was covered by a Rust unit test
reaching into ProjectTemplate. `from_user_api` now returns the generated
Indexer.res alongside the .d.ts it already returned — same parse, same
`with_indexer_types` flag — so the assertion lives with the rest of the
chain-id coverage in ChainIdMode_test.res and runs against the real
NAPI boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6
Two defects from review.

Mode resolution filtered out skipped chains while codegen emits a
`chainId` case for every chain in config.yaml. An active chain 1 beside
a skipped 2494104990 resolved Int32 and then emitted `#2494104990` — an
int polyvariant ReScript can't represent — and a skipped id above
MAX_SAFE_INTEGER skipped validation entirely. Resolution now covers
every configured chain, which also keeps the physical column types
stable when a chain is skipped and unskipped.

`context.chain.id` was `int`, reached through an identity cast from the
float-backed representation. ReScript ints are 32-bit, so that type was
wrong for exactly the ids this branch adds support for — a handler
can't even write `chain.id == 2494104990`, the literal is out of range.
The generated Indexer.res now declares its own `handlerChain` using the
generated `chainId`, matching `contractRegisterChain` and `indexerChain`,
and `Internal.chainInfo` goes back to the internal `ChainId.t`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECqzHdVS4cLxfCXPw8zpb6

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8dd631c2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +7 to +9
// Escapes for the boundaries that stay `int`: the handler-facing
// `context.chain.id` / `Envio.effectChain.id`, and int literals in configs and
// tests. Both are the identity at runtime — an `int` is already a JS number.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Type chain-scoped effect IDs as ChainId.t

For an Int64 config, generated handlers expose chain IDs as ChainId.t, but this boundary deliberately remains int and Envio.effectChain is still declared as {id: int} in packages/envio/src/Envio.res:194. Consequently, a chain-scoped effect cannot pass or compare context.chain.id with generated indexer.chainIds or use ChainId APIs without an explicit unsafe identity conversion, so the ReScript API remains int32-oriented for precisely the wide-chain scenario this change supports. Change effectChain.id to ChainId.t instead of retaining the int boundary.

Useful? React with 👍 / 👎.

// with a wider id falls back to the opaque runtime representation.
// TypeScript keeps its numeric literal union either way.
let chain_id_type = match cfg.chain_id_mode {
ChainIdMode::Int64 => "type chainId = ChainId.t".to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Generate wide-safe IDs in imported ReScript handlers

When envio contract-import generates ReScript handlers for an Int64 project, event.chainId now has this opaque ChainId.t type, but Event::get_entity_id_code in packages/cli/src/hbs_templating/contract_import_templates.rs:546 still emits (event.chainId :> int)->Belt.Int.toString. ReScript rejects that generated code because ChainId.t is not a subtype of int, so contract import produces a project that cannot compile for any wide chain. Generate the entity ID with ChainId.toString in Int64 mode, or use a representation-independent conversion.

Useful? React with 👍 / 👎.

@DZakh
DZakh merged commit cc53847 into main Jul 29, 2026
7 of 8 checks passed
@DZakh
DZakh deleted the claude/chain-id-mode-param-ndswh8 branch July 29, 2026 12:22
DZakh pushed a commit that referenced this pull request Jul 29, 2026
…tart-at-head-recovery

The base picked up main, bringing configurable chain-id storage (#1507) and
generic mock indexer entity queries (#1509).

One conflict, in makeSetReadyAtQuery: #1507 turned it into a one-row-at-a-time
update because the id column is INTEGER or BIGINT depending on ChainId.mode, so
`= ANY($2::int[])` no longer holds for every configuration. Took that shape and
kept the `IS NULL` guard on top; the caller loops the chain ids, so the guard
applies per row exactly as it did to the array form.

Source.make's `~chain` became `~chainId` in #1509; renamed at the call sites in
the resume tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DL4RDYUpZcK2YyydfSvqLc
DZakh pushed a commit that referenced this pull request Jul 29, 2026
Reconciles main's ChainId.t migration (#1507) with the address-store work.
Source options keep main's `chainId: ChainId.t` alongside this branch's
`addressStore` handle, and SimulateSource takes both — the branch builds it
from ChainState (via SimulateSourceConfig) so it can pass the store, rather
than from SimulateItems as main still does.

Fetch-state fixtures take main's ChainId.fromInt call sites and drop the
removed `contractConfigs` field. ClientAddressFilter_test keeps this branch's
trimmed form: main's buildAddressFilterBody / filterByClientAddress cases
cover a mechanism this branch replaces with the Rust routing gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kjxcBCs4nHF5FNNB8FaEu
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.

2 participants