Skip to content

Migrate HyperFuel client from JS to Rust native binding - #1298

Merged
DZakh merged 14 commits into
mainfrom
claude/kind-goodall-z4vrkl
Jun 19, 2026
Merged

Migrate HyperFuel client from JS to Rust native binding#1298
DZakh merged 14 commits into
mainfrom
claude/kind-goodall-z4vrkl

Conversation

@DZakh

@DZakh DZakh commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary

Migrates the HyperFuel client implementation from a JavaScript library (@envio-dev/hyperfuel-client) to a native Rust binding using NAPI. This reduces external dependencies and improves type safety by leveraging the native hyperfuel-client Rust crate.

Key Changes

  • Rust NAPI binding: Created new hyperfuel_source module in packages/cli/src with:

    • HyperfuelClient struct wrapping the native hyperfuel_client::Client
    • Query, ReceiptSelection, and FieldSelection types for query construction
    • QueryResponse, Receipt, and Block types for response handling
    • Arrow format conversion logic to transform native responses into NAPI-compatible types
  • ReScript client simplification: Updated HyperFuelClient.res to:

    • Remove unused type definitions and query field options (transactions, inputs, outputs, etc.)
    • Reduce blockFieldOptions and receiptFieldOptions to only essential fields
    • Remove transactionFieldSelection and related transaction types
    • Simplify configuration to only url and bearerToken
    • Use new NAPI binding via Core.getAddon().hyperfuelClient
  • API token handling:

    • Added apiToken parameter to HyperFuelSource.make options
    • Require API token from ENVIO_API_TOKEN environment variable with helpful error message
    • Pass token to client via bearerToken config field
  • Query interface updates:

    • Removed inputs, outputs, includeAllBlocks, maxNumBlocks, maxNumTransactions from query options
    • Removed BlockData module and queryBlockData function (block querying now handled via standard receipt queries)
    • Updated GetLogs.query to accept apiToken parameter
  • Dependencies:

    • Added hyperfuel-client = "3.2.0" and polars-arrow = "0.42" to Cargo.toml
    • Removed @envio-dev/hyperfuel-client from packages/envio/package.json

Implementation Details

The Rust binding uses Arrow columnar format for efficient data transfer. Response conversion extracts typed data from Arrow batches with proper hex encoding for binary fields and BigInt conversion for numeric values. The client configuration supports optional bearer token and HTTP timeout settings, with retry logic delegated to the indexer layer.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK

Summary by CodeRabbit

  • New Features

    • Added HyperFuel client integration with native support for authenticated API requests.
    • Added height retrieval functionality for HyperFuel data sources.
  • Breaking Changes

    • HyperFuel data source now requires ENVIO_API_TOKEN environment variable for API authentication.
  • Improvements

    • Simplified HyperFuel query response schemas for optimized data retrieval.
    • Removed unnecessary dependencies.

claude added 2 commits June 9, 2026 14:57
Replace the @envio-dev/hyperfuel-client npm package with the hyperfuel-client
cargo crate, exposed through a native napi HyperfuelClient class — mirroring
how the Hypersync client is rendered. The crate vendors its capnp-generated
code, so building no longer needs the capnp compiler.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
Pass the user's API token through to the HyperFuel client as the bearer
token (mirroring HyperSync) instead of a hardcoded value, and prune the
now-unused query/response surface: block-data querying, transaction and
input/output selections, and the unused field-option variants. Build the
client config through serde so we don't need a direct url dependency.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
@coderabbitai

coderabbitai Bot commented Jun 10, 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

This PR integrates a native Rust HyperFuel client into the CLI, replacing the JavaScript dependency. It adds query input contracts and Arrow-based response parsing, then rewires ReScript flows to authenticate via API token bearer headers and minimizes the type surface to match native client capabilities.

Changes

HyperFuel Native Client Integration

Layer / File(s) Summary
Rust module and dependency setup
packages/cli/src/lib.rs, packages/cli/Cargo.toml
Internal hyperfuel_source module is declared, and hyperfuel-client and polars-arrow (with IPC features) are added as dependencies.
Query input types and JS-to-Rust conversion
packages/cli/src/hyperfuel_source/query.rs
N-API Query, ReceiptSelection, and FieldSelection objects define JS-facing input shape, with conversion helpers that validate block ranges, decode hex hash strings, map BigInt to unsigned integers, and convert into hyperfuel_client::net_types.
Response types and Arrow batch parsing
packages/cli/src/hyperfuel_source/types.rs
N-API response types (QueryResponse, Receipt, Block) and typed ConvertError are defined; Arrow batch converters extract rows, validate required fields (returning typed missing-field errors), hex-encode binary data, and map u64 to BigInt.
HyperfuelClient HTTP wrapper and methods
packages/cli/src/hyperfuel_source/config.rs, packages/cli/src/hyperfuel_source/mod.rs
ClientConfig struct and TryFrom conversion initialize the underlying client; HyperfuelClient wrapper exposes async get_height() and get_selected_data(query) methods with JSON error payloads for missing fields.
Native addon binding in ReScript Core
packages/envio/src/Core.res
hyperfuelClientCtor type and hyperfuelClient addon field are added alongside existing bindings.
ReScript HyperFuelClient API and type minimization
packages/envio/src/sources/HyperFuelClient.res
cfg removes http_req_timeout_millis; QueryTypes narrows to block/receipt-only; response payload shrinks to { receipts, blocks }; client construction moves to native classNew; getHeight method is exported.
HyperFuel source module caching and query updates
packages/envio/src/sources/HyperFuel.res, packages/envio/src/sources/HyperFuel.resi
CachedClients.getClient accepts apiToken and passes bearerToken to client construction; GetLogs.query adds apiToken parameter and parses exception JSON to extract missing field names; BlockData module is removed and replaced with getHeight helper that calls native client.
API token requirement and height call behavior
packages/envio/src/sources/HyperFuelSource.res, packages/envio/src/ChainFetcher.res, packages/envio/package.json
HyperFuelSource requires apiToken option and throws if not provided; token is threaded through ChainFetcher; height retrieval calls native getHeight and blocks indefinitely on 401 Unauthorized; @envio-dev/hyperfuel-client dependency is removed.
Test suite and behavior validation
scenarios/fuel_test/test/HyperFuelHeight_test.res
Integration tests validate that height requests include correct bearer token header and that 401 responses cause indefinite blocking (detected via Promise race).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the primary change: migrating HyperFuel client from JavaScript to Rust native binding, which is the main focus across all modified files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

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

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/sources/HyperFuelClient.res (1)

44-68: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Field name mismatches in query type will cause query to fail.

The Rust Query struct uses from_block and field_selection (snake_case). Without @as annotations, the JS object will have fromBlock and fieldSelection which won't match.

🐛 Proposed fix
 type query = {
   /** The block to start the query from */
+  `@as`("from_block")
   fromBlock: int,
   /**
    * The block to end the query at. ...
    */
   `@as`("toBlock")
   toBlockExclusive?: int,
   /**
    * List of receipt selections...
    */
   receipts?: array<receiptSelection>,
   /**
    * Field selection...
    */
+  `@as`("field_selection")
   fieldSelection: fieldSelection,
 }
🤖 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/sources/HyperFuelClient.res` around lines 44 - 68, The
query record fields use camelCase (fromBlock, fieldSelection) but the Rust side
expects snake_case (from_block, field_selection); annotate the corresponding
fields in the type query with `@as` to map to the Rust names (add
`@as`("from_block") to the fromBlock field and `@as`("field_selection") to the
fieldSelection field) so the generated JS matches the Rust Query struct names
and queries succeed.
🧹 Nitpick comments (5)
packages/envio/src/sources/HyperFuel.res (1)

103-107: 💤 Low value

Add explicit type annotation for Utils.magic cast.

Line 104 uses a generic type parameter 'a in the cast. As per coding guidelines, Utils.magic should have explicit input and output type annotations.

Suggested fix
     blocks
-    ->(Utils.magic: option<'a> => 'a)
+    ->(Utils.magic: option<array<HyperFuelClient.FuelTypes.block>> => array<HyperFuelClient.FuelTypes.block>)
     ->Array.forEach(block => {
🤖 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/sources/HyperFuel.res` around lines 103 - 107, The cast
using Utils.magic currently uses a generic 'a; replace that generic with an
explicit input/output type that matches the actual type of blocks (for example
option<array<Block>> => array<Block> or whatever concrete block container type
your code uses) so the unwrap is fully typed, while leaving the later height
conversion cast (Utils.magic: int => string) intact; update the occurrence of
(Utils.magic: option<'a> => 'a) to (Utils.magic: option<ConcreteBlockContainer>
=> ConcreteBlockContainer) referencing Utils.magic and
blocksDict->Dict.set/block.height to locate the code to change.

Source: Coding guidelines

packages/cli/src/hyperfuel_source/types.rs (2)

131-131: 💤 Low value

Blocks are always wrapped in Some, even when empty.

Line 131 always wraps the blocks vector in Some(), even if the vector is empty. This means QueryResponseData.blocks is never None, only Some([]) when there are no blocks.

Based on the ReScript consumer code (HyperFuel.res:97-146), the code uses a magic cast to unwrap the optional, suggesting it expects it to always be present. However, the type signature declares it as optional. Consider whether:

  1. The field should be non-optional in the type definition
  2. Empty vectors should map to None for clarity
🤖 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/hyperfuel_source/types.rs` at line 131, The current
construction always wraps blocks in Some by calling blocks:
Some(blocks_from_arrow(&res.data.blocks)), which makes QueryResponseData.blocks
never None; decide whether blocks should be non-optional or empty vectors should
map to None. To fix, either (A) update the type definition of
QueryResponseData.blocks to be a plain Vec so it is non-optional, and remove the
Some() wrapper where blocks are constructed (adjust any callers/consumers
accordingly), or (B) change the construction to check the resulting Vec from
blocks_from_arrow(&res.data.blocks) and set blocks: None when it is empty,
otherwise blocks: Some(vec) so empty vectors map to None; update any ReScript
interop/tests expecting the current behavior. Ensure you modify the construction
site that references blocks_from_arrow and the QueryResponseData type
accordingly.

126-128: ⚖️ Poor tradeoff

Potential overflow in u64 to i64 conversion.

Lines 126-128 cast u64 values to i64 using the as operator. If archive_height, next_block, or total_execution_time exceed i64::MAX, this will silently overflow and produce incorrect negative values.

For block heights and execution times from a blockchain, values are unlikely to exceed i64::MAX in practice, but the cast is technically unsafe. Consider either:

  1. Using try_into() with error handling for values > i64::MAX
  2. Documenting that overflow is impossible given the domain constraints
🤖 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/hyperfuel_source/types.rs` around lines 126 - 128, The code
is unsafely casting u64 -> i64 for archive_height, next_block, and
total_execution_time using `as`, which can silently overflow; replace those `as`
casts with fallible conversions (e.g., `i64::try_from(...)` or
`u64::try_into()`), handle the Result (propagate an error or map to None for
`archive_height.map(...)`), and return or surface a clear error when values
exceed `i64::MAX` so `archive_height`, `next_block`, and `total_execution_time`
from `res` cannot silently become negative.
packages/cli/src/hyperfuel_source/config.rs (1)

23-26: 💤 Low value

Consider explicit validation for negative timeout values.

The current conversion silently drops negative http_req_timeout_millis values using .ok(). While this provides a safe fallback, it might hide user configuration errors. Consider adding explicit validation to warn or error when a negative timeout is provided, rather than silently ignoring it.

💡 Suggested approach
-            "http_req_timeout_millis": config
-                .http_req_timeout_millis
-                .and_then(|v| u64::try_from(v).ok())
-                .filter(|v| *v > 0),
+            "http_req_timeout_millis": match config.http_req_timeout_millis {
+                Some(v) if v < 0 => return Err(anyhow::anyhow!("http_req_timeout_millis must be non-negative")),
+                Some(v) if v == 0 => None,
+                Some(v) => Some(v as u64),
+                None => None,
+            },
🤖 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/hyperfuel_source/config.rs` around lines 23 - 26, The
conversion for http_req_timeout_millis currently swallows negative values via
.and_then(|v| u64::try_from(v).ok()) — add explicit validation on
config.http_req_timeout_millis to detect negatives and either return a
configuration error or log a warning instead of silently dropping the value.
Replace the .and_then(...).ok() chain with an explicit match/if let that checks
for Some(v) and if v < 0 produce an Err or call the logger (or validation
collector) with a clear message referencing http_req_timeout_millis, otherwise
perform u64::try_from(v) and propagate any conversion errors so negatives are
not ignored.
packages/cli/src/hyperfuel_source/mod.rs (1)

33-39: ⚖️ Poor tradeoff

Error message truncation may lose diagnostic context.

Lines 36-37 truncate the error message to only the first line to avoid verbose debug dumps. While this improves retry UX by keeping logs readable, it may discard valuable diagnostic information such as:

  • Root cause details from nested errors
  • Parameter values that caused the failure
  • Stack traces or error chains

Consider whether:

  1. The full error should be logged at debug level while the truncated version is returned to the user
  2. Truncation should preserve the first N lines (e.g., 3-5) rather than just 1
  3. Important structured error fields (if any) should be extracted before truncation
🤖 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/hyperfuel_source/mod.rs` around lines 33 - 39, The current
map_err on self.inner.get_arrow truncates the error to the first line (variables
message/summary), which may lose useful diagnostics; change the closure to log
the full error at debug (e.g., using process or crate logger) and return a
concise message to the caller—preferably keep the first N lines (3–5) instead of
one—or extract important structured fields from the error before truncation;
update the map_err closure around get_arrow to (1) capture the full error
string, (2) emit the full error at debug level, and (3) build the
napi::Error::from_reason using a truncated plural-line summary (or extracted
fields) so retries remain readable but full diagnostics are preserved in logs.
🤖 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/Cargo.toml`:
- Around line 48-49: Verify the pinned dependency versions for hyperfuel-client
and polars-arrow against Cargo.lock and ensure Cargo.lock is
committed/up-to-date (check that hyperfuel-client = 3.2.0 and polars-arrow =
0.42.0 resolve correctly), then run a security scan with cargo audit in the
packages/cli crate to detect advisories in direct and transitive dependencies;
if cargo audit reports issues, update the offending crate(s) (or run cargo
update -p <crate>) and re-run the audit until clean, and finally commit the
updated Cargo.lock and any Cargo.toml version bumps.

In `@packages/cli/src/hyperfuel_source/query.rs`:
- Around line 41-46: The current bigints_to_u64 function discards sign and
lossless from BigInt::get_u64(); change it to validate losslessness and sign and
propagate an error instead of silently truncating: update bigints_to_u64 to
return Result<Vec<u64>, YourErrorType> (or Result with anyhow::Error), iterate
each BigInt from v.unwrap_or_default(), call let (sign, val, lossless) =
b.get_u64(); if sign || !lossless return Err(...) with a clear message
identifying the offending BigInt, otherwise push val into the Vec and return
Ok(vec). Reference symbols: bigints_to_u64 and BigInt::get_u64; ensure callers
of bigints_to_u64 (the code building the ReceiptSelection rb filter) are updated
to handle the Result or surface the error to the user.

In `@packages/cli/src/hyperfuel_source/types.rs`:
- Around line 86-91: The Receipt construction is silently inserting sentinel
defaults for required identity fields (tx_id, receipt_index, block_height) using
hex_at(...).unwrap_or_else("0x") and u64_at(...).unwrap_or_default(), which can
produce invalid receipts and unsafe casts; update the extraction to fail fast
(return a Result/propagate an error or make the Receipt creation fallible) when
hex_at/ u64_at/ u8_at return None instead of supplying defaults, and replace
unchecked casts like "as i64" with checked conversions (e.g., i64::try_from or
.try_into()) to detect overflow; locate the Receipt struct construction in
types.rs and change calls to hex_at, u64_at, u8_at so missing/null columns cause
an error path rather than default values.

In `@packages/envio/src/sources/HyperFuel.res`:
- Around line 8-16: The cache key in getClient currently uses only serverUrl,
causing stale bearer tokens; modify getClient so the cache key includes apiToken
(e.g., combine serverUrl and apiToken) or detect token mismatch and replace the
cached client: when calling cache->Utils.Dict.dangerouslyGetNonOption(serverUrl)
use a composite key derived from serverUrl and apiToken (or check the existing
client's bearer token and call cache->Dict.set with the new client from
HyperFuelClient.make when tokens differ) so HyperFuelClient.make is always used
with the correct token and cache->Dict.set stores/upserts by that composite key.

In `@packages/envio/src/sources/HyperFuelClient.res`:
- Around line 3-6: The ReScript type cfg uses camelCase "bearerToken" which
won't match Rust's ClientConfig snake_case "bearer_token"; update the cfg type
definition so the JS/N-API field name matches Rust (either rename the field to
bearer_token or add an `@as`("bearer_token") annotation on the bearerToken field)
and keep it optional to mirror bearerToken?: string; locate the type cfg in
HyperFuelClient.res and apply the change so the Rust client receives the
bearer_token field.
- Around line 37-42: The GraphQL/Rust field names in the receiptSelection type
(receiptSelection) are camelCase but the Rust side expects snake_case; update
the type definition by adding `@as` annotations for each field to map them to the
Rust names (e.g., map rootContractId -> "root_contract_id", receiptType ->
"receipt_type", txStatus -> "tx_status" while keeping rb as-is if its Rust name
is also "rb") so the fields used by the FuelSDK/Fuel ReceiptSelection align with
the Rust ReceiptSelection struct.
- Around line 72-86: The ReScript type "receipt" declares camelCase fields but
the incoming JS/N-API Receipt uses snake_case names, so properties will be
undefined; update the "receipt" type to map each field to the actual JS key
using `@as` annotations (e.g., map receiptIndex -> `@as`("receipt_index"),
rootContractId -> `@as`("root_contract_id"), txId -> `@as`("tx_id"), blockHeight ->
`@as`("block_height"), receiptType -> `@as`("receipt_type"), subId -> `@as`("sub_id"),
assetId -> `@as`("asset_id"), toAddress -> `@as`("to_address"), etc.), ensuring
optional fields keep their ? and types (Address.t, string, bigint,
FuelSDK.receiptType) and that every ReScript identifier in the type matches the
corresponding snake_case JS property via `@as`.
- Around line 100-113: The response type fields in queryResponseTyped use
camelCase but the Rust JSON uses snake_case, so add `@as` annotations to map them:
annotate archiveHeight with `@as`("archive_height"), nextBlock with
`@as`("next_block"), and totalExecutionTime with `@as`("total_execution_time")
(leave data as-is if it already matches), updating the queryResponseTyped type
definition to use these `@as` attributes so the deserializer correctly maps the
snake_case JSON to the Rescript/Rust bindings.

---

Outside diff comments:
In `@packages/envio/src/sources/HyperFuelClient.res`:
- Around line 44-68: The query record fields use camelCase (fromBlock,
fieldSelection) but the Rust side expects snake_case (from_block,
field_selection); annotate the corresponding fields in the type query with `@as`
to map to the Rust names (add `@as`("from_block") to the fromBlock field and
`@as`("field_selection") to the fieldSelection field) so the generated JS matches
the Rust Query struct names and queries succeed.

---

Nitpick comments:
In `@packages/cli/src/hyperfuel_source/config.rs`:
- Around line 23-26: The conversion for http_req_timeout_millis currently
swallows negative values via .and_then(|v| u64::try_from(v).ok()) — add explicit
validation on config.http_req_timeout_millis to detect negatives and either
return a configuration error or log a warning instead of silently dropping the
value. Replace the .and_then(...).ok() chain with an explicit match/if let that
checks for Some(v) and if v < 0 produce an Err or call the logger (or validation
collector) with a clear message referencing http_req_timeout_millis, otherwise
perform u64::try_from(v) and propagate any conversion errors so negatives are
not ignored.

In `@packages/cli/src/hyperfuel_source/mod.rs`:
- Around line 33-39: The current map_err on self.inner.get_arrow truncates the
error to the first line (variables message/summary), which may lose useful
diagnostics; change the closure to log the full error at debug (e.g., using
process or crate logger) and return a concise message to the caller—preferably
keep the first N lines (3–5) instead of one—or extract important structured
fields from the error before truncation; update the map_err closure around
get_arrow to (1) capture the full error string, (2) emit the full error at debug
level, and (3) build the napi::Error::from_reason using a truncated plural-line
summary (or extracted fields) so retries remain readable but full diagnostics
are preserved in logs.

In `@packages/cli/src/hyperfuel_source/types.rs`:
- Line 131: The current construction always wraps blocks in Some by calling
blocks: Some(blocks_from_arrow(&res.data.blocks)), which makes
QueryResponseData.blocks never None; decide whether blocks should be
non-optional or empty vectors should map to None. To fix, either (A) update the
type definition of QueryResponseData.blocks to be a plain Vec so it is
non-optional, and remove the Some() wrapper where blocks are constructed (adjust
any callers/consumers accordingly), or (B) change the construction to check the
resulting Vec from blocks_from_arrow(&res.data.blocks) and set blocks: None when
it is empty, otherwise blocks: Some(vec) so empty vectors map to None; update
any ReScript interop/tests expecting the current behavior. Ensure you modify the
construction site that references blocks_from_arrow and the QueryResponseData
type accordingly.
- Around line 126-128: The code is unsafely casting u64 -> i64 for
archive_height, next_block, and total_execution_time using `as`, which can
silently overflow; replace those `as` casts with fallible conversions (e.g.,
`i64::try_from(...)` or `u64::try_into()`), handle the Result (propagate an
error or map to None for `archive_height.map(...)`), and return or surface a
clear error when values exceed `i64::MAX` so `archive_height`, `next_block`, and
`total_execution_time` from `res` cannot silently become negative.

In `@packages/envio/src/sources/HyperFuel.res`:
- Around line 103-107: The cast using Utils.magic currently uses a generic 'a;
replace that generic with an explicit input/output type that matches the actual
type of blocks (for example option<array<Block>> => array<Block> or whatever
concrete block container type your code uses) so the unwrap is fully typed,
while leaving the later height conversion cast (Utils.magic: int => string)
intact; update the occurrence of (Utils.magic: option<'a> => 'a) to
(Utils.magic: option<ConcreteBlockContainer> => ConcreteBlockContainer)
referencing Utils.magic and blocksDict->Dict.set/block.height to locate the code
to change.
🪄 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: cb35504d-98e7-4c4c-a316-13e1e4966242

📥 Commits

Reviewing files that changed from the base of the PR and between b01ff70 and c616b87.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • packages/cli/Cargo.toml
  • packages/cli/src/hyperfuel_source/config.rs
  • packages/cli/src/hyperfuel_source/mod.rs
  • packages/cli/src/hyperfuel_source/query.rs
  • packages/cli/src/hyperfuel_source/types.rs
  • packages/cli/src/lib.rs
  • packages/envio/package.json
  • packages/envio/src/ChainFetcher.res
  • packages/envio/src/Core.res
  • packages/envio/src/sources/HyperFuel.res
  • packages/envio/src/sources/HyperFuel.resi
  • packages/envio/src/sources/HyperFuelClient.res
  • packages/envio/src/sources/HyperFuelSource.res
💤 Files with no reviewable changes (1)
  • packages/envio/package.json

Comment thread packages/cli/Cargo.toml Outdated
Comment thread packages/cli/src/hyperfuel_source/query.rs Outdated
Comment thread packages/cli/src/hyperfuel_source/types.rs Outdated
Comment on lines 8 to 16
let getClient = (~serverUrl, ~apiToken) => {
switch cache->Utils.Dict.dangerouslyGetNonOption(serverUrl) {
| Some(client) => client
| None =>
let newClient = HyperFuelClient.make({url: url})
cache->Dict.set(url, newClient)
let newClient = HyperFuelClient.make({url: serverUrl, bearerToken: apiToken})
cache->Dict.set(serverUrl, newClient)
newClient
}
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cache key does not account for apiToken, risking stale credentials.

The cache uses only serverUrl as the key, but clients are now constructed with a bearerToken. If getClient is called twice with the same URL but different tokens, the second call returns the client created with the first token—silently ignoring the new token.

Consider including apiToken in the cache key or invalidating/updating the cached client when the token changes.

Suggested fix: include apiToken in cache key
 module CachedClients = {
   let cache: dict<HyperFuelClient.t> = Dict.make()

   let getClient = (~serverUrl, ~apiToken) => {
-    switch cache->Utils.Dict.dangerouslyGetNonOption(serverUrl) {
+    let cacheKey = serverUrl ++ ":" ++ apiToken
+    switch cache->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
     | Some(client) => client
     | None =>
       let newClient = HyperFuelClient.make({url: serverUrl, bearerToken: apiToken})
-      cache->Dict.set(serverUrl, newClient)
+      cache->Dict.set(cacheKey, newClient)
       newClient
     }
   }
 }
📝 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
let getClient = (~serverUrl, ~apiToken) => {
switch cache->Utils.Dict.dangerouslyGetNonOption(serverUrl) {
| Some(client) => client
| None =>
let newClient = HyperFuelClient.make({url: url})
cache->Dict.set(url, newClient)
let newClient = HyperFuelClient.make({url: serverUrl, bearerToken: apiToken})
cache->Dict.set(serverUrl, newClient)
newClient
}
}
let getClient = (~serverUrl, ~apiToken) => {
let cacheKey = serverUrl ++ ":" ++ apiToken
switch cache->Utils.Dict.dangerouslyGetNonOption(cacheKey) {
| Some(client) => client
| None =>
let newClient = HyperFuelClient.make({url: serverUrl, bearerToken: apiToken})
cache->Dict.set(cacheKey, newClient)
newClient
}
}
🤖 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/sources/HyperFuel.res` around lines 8 - 16, The cache key
in getClient currently uses only serverUrl, causing stale bearer tokens; modify
getClient so the cache key includes apiToken (e.g., combine serverUrl and
apiToken) or detect token mismatch and replace the cached client: when calling
cache->Utils.Dict.dangerouslyGetNonOption(serverUrl) use a composite key derived
from serverUrl and apiToken (or check the existing client's bearer token and
call cache->Dict.set with the new client from HyperFuelClient.make when tokens
differ) so HyperFuelClient.make is always used with the correct token and
cache->Dict.set stores/upserts by that composite key.

Comment on lines 3 to 6
type cfg = {
url: string,
bearerToken?: string,
http_req_timeout_millis?: int,
}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Field name mismatch will prevent bearer token from being passed to the Rust client.

The Rust ClientConfig uses snake_case field names (bearer_token), but this ReScript type uses camelCase without @as annotation. N-API object fields retain their Rust names, so the JS object will have bearerToken but Rust expects bearer_token.

🐛 Proposed fix
 type cfg = {
   url: string,
+  `@as`("bearer_token")
   bearerToken?: string,
 }
📝 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
type cfg = {
url: string,
bearerToken?: string,
http_req_timeout_millis?: int,
}
type cfg = {
url: string,
`@as`("bearer_token")
bearerToken?: string,
}
🤖 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/sources/HyperFuelClient.res` around lines 3 - 6, The
ReScript type cfg uses camelCase "bearerToken" which won't match Rust's
ClientConfig snake_case "bearer_token"; update the cfg type definition so the
JS/N-API field name matches Rust (either rename the field to bearer_token or add
an `@as`("bearer_token") annotation on the bearerToken field) and keep it optional
to mirror bearerToken?: string; locate the type cfg in HyperFuelClient.res and
apply the change so the Rust client receives the bearer_token field.

Comment on lines 37 to 42
type receiptSelection = {
rootContractId?: array<Address.t>,
toAddress?: array<string>,
assetId?: array<string>,
receiptType?: array<FuelSDK.receiptType>,
sender?: array<string>,
recipient?: array<string>,
contractId?: array<Address.t>,
ra?: array<bigint>,
rb?: array<bigint>,
rc?: array<bigint>,
rd?: array<bigint>,
txStatus?: array<int>,
}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Field name mismatches in receiptSelection will break receipt filtering.

The Rust ReceiptSelection uses snake_case field names. Add @as annotations:

🐛 Proposed fix
 type receiptSelection = {
+  `@as`("root_contract_id")
   rootContractId?: array<Address.t>,
+  `@as`("receipt_type")
   receiptType?: array<FuelSDK.receiptType>,
   rb?: array<bigint>,
+  `@as`("tx_status")
   txStatus?: array<int>,
 }
📝 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
type receiptSelection = {
rootContractId?: array<Address.t>,
toAddress?: array<string>,
assetId?: array<string>,
receiptType?: array<FuelSDK.receiptType>,
sender?: array<string>,
recipient?: array<string>,
contractId?: array<Address.t>,
ra?: array<bigint>,
rb?: array<bigint>,
rc?: array<bigint>,
rd?: array<bigint>,
txStatus?: array<int>,
}
type receiptSelection = {
`@as`("root_contract_id")
rootContractId?: array<Address.t>,
`@as`("receipt_type")
receiptType?: array<FuelSDK.receiptType>,
rb?: array<bigint>,
`@as`("tx_status")
txStatus?: array<int>,
}
🤖 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/sources/HyperFuelClient.res` around lines 37 - 42, The
GraphQL/Rust field names in the receiptSelection type (receiptSelection) are
camelCase but the Rust side expects snake_case; update the type definition by
adding `@as` annotations for each field to map them to the Rust names (e.g., map
rootContractId -> "root_contract_id", receiptType -> "receipt_type", txStatus ->
"tx_status" while keeping rb as-is if its Rust name is also "rb") so the fields
used by the FuelSDK/Fuel ReceiptSelection align with the Rust ReceiptSelection
struct.

Comment on lines 72 to 86
type receipt = {
/** Index of the receipt in the block */
receiptIndex: int,
/** Contract that produced the receipt */
rootContractId?: Address.t,
/** transaction that this receipt originated from */
txId: string,
/** The status type of the transaction this receipt originated from */
txStatus: int,
/** block that the receipt originated in */
blockHeight: int,
/** The value of the program counter register $pc, which is the memory address of the current instruction. */
pc?: int,
/** The value of register $is, which is the pointer to the start of the currently-executing code. */
is?: int,
/** The recipient contract */
to?: string,
/** The recipient address */
toAddress?: string,
/** The amount of coins transferred. */
amount?: bigint,
/** The asset id of the coins transferred. */
assetId?: string,
/** The gas used for the transaction. */
gas?: int,
/** The first parameter for a CALL receipt type, holds the function selector. */
param1?: bigint,
/** The second parameter for a CALL receipt type, typically used for the user-specified input to the ABI function being selected. */
param2?: bigint,
/** The value of registers at the end of execution, used for debugging. */
val?: bigint,
/** The value of the pointer register, used for debugging. */
ptr?: bigint,
/** A 32-byte String of MEM[$rC, $rD]. The syntax MEM[x, y] means the memory range starting at byte x, of length y bytes. */
digest?: string,
/** The decimal string representation of an 8-bit unsigned integer for the panic reason. Only returned if the receipt type is PANIC. */
reason?: int,
/** The value of register $rA. */
ra?: bigint,
/** The value of register $rB. */
rb?: bigint,
/** The value of register $rC. */
rc?: bigint,
/** The value of register $rD. */
rd?: bigint,
/** The length of the receipt. */
len?: bigint,
/** The type of receipt. */
receiptType: FuelSDK.receiptType,
/** 0 if script exited successfully, any otherwise. */
result?: int,
/** The amount of gas consumed by the script. */
gasUsed?: int,
/** The receipt data. */
data?: string,
/** The address of the message sender. */
sender?: string,
/** The address of the message recipient. */
recipient?: string,
/** The nonce value for a message. */
nonce?: string,
/** Current context if in an internal context. null otherwise */
contractId?: Address.t,
/** The sub id. */
rb?: bigint,
val?: bigint,
subId?: string,
}

// Unused - in indexer currently
type input = {
txId: string,
blockHeight: int,
inputType: int,
utxoId?: string,
owner?: string,
amount?: bigint,
assetId?: string,
txPointerBlockHeight?: int,
txPointerTxIndex?: int,
witnessIndex?: int,
predicateGasUsed?: int,
predicate?: string,
predicateData?: string,
balanceRoot?: string,
stateRoot?: string,
contract?: string,
sender?: string,
recipient?: string,
nonce?: string,
data?: string,
}

// Unused in indexer currently
type output = {
txId: string,
blockHeight: int,
outputType: int,
to?: string,
amount?: bigint,
assetId?: string,
inputIndex?: int,
balanceRoot?: string,
stateRoot?: string,
contract?: string,
toAddress?: string,
}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Receipt response fields will be undefined due to name mismatches.

The Rust Receipt struct uses snake_case. Without @as annotations, ReScript will look for camelCase properties that don't exist in the JS object returned by N-API.

🐛 Proposed fix
 type receipt = {
+  `@as`("receipt_index")
   receiptIndex: int,
+  `@as`("root_contract_id")
   rootContractId?: Address.t,
+  `@as`("tx_id")
   txId: string,
+  `@as`("block_height")
   blockHeight: int,
+  `@as`("receipt_type")
   receiptType: FuelSDK.receiptType,
   data?: string,
   rb?: bigint,
   val?: bigint,
+  `@as`("sub_id")
   subId?: string,
   amount?: bigint,
+  `@as`("asset_id")
   assetId?: string,
   to?: string,
+  `@as`("to_address")
   toAddress?: string,
 }
📝 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
type receipt = {
/** Index of the receipt in the block */
receiptIndex: int,
/** Contract that produced the receipt */
rootContractId?: Address.t,
/** transaction that this receipt originated from */
txId: string,
/** The status type of the transaction this receipt originated from */
txStatus: int,
/** block that the receipt originated in */
blockHeight: int,
/** The value of the program counter register $pc, which is the memory address of the current instruction. */
pc?: int,
/** The value of register $is, which is the pointer to the start of the currently-executing code. */
is?: int,
/** The recipient contract */
to?: string,
/** The recipient address */
toAddress?: string,
/** The amount of coins transferred. */
amount?: bigint,
/** The asset id of the coins transferred. */
assetId?: string,
/** The gas used for the transaction. */
gas?: int,
/** The first parameter for a CALL receipt type, holds the function selector. */
param1?: bigint,
/** The second parameter for a CALL receipt type, typically used for the user-specified input to the ABI function being selected. */
param2?: bigint,
/** The value of registers at the end of execution, used for debugging. */
val?: bigint,
/** The value of the pointer register, used for debugging. */
ptr?: bigint,
/** A 32-byte String of MEM[$rC, $rD]. The syntax MEM[x, y] means the memory range starting at byte x, of length y bytes. */
digest?: string,
/** The decimal string representation of an 8-bit unsigned integer for the panic reason. Only returned if the receipt type is PANIC. */
reason?: int,
/** The value of register $rA. */
ra?: bigint,
/** The value of register $rB. */
rb?: bigint,
/** The value of register $rC. */
rc?: bigint,
/** The value of register $rD. */
rd?: bigint,
/** The length of the receipt. */
len?: bigint,
/** The type of receipt. */
receiptType: FuelSDK.receiptType,
/** 0 if script exited successfully, any otherwise. */
result?: int,
/** The amount of gas consumed by the script. */
gasUsed?: int,
/** The receipt data. */
data?: string,
/** The address of the message sender. */
sender?: string,
/** The address of the message recipient. */
recipient?: string,
/** The nonce value for a message. */
nonce?: string,
/** Current context if in an internal context. null otherwise */
contractId?: Address.t,
/** The sub id. */
rb?: bigint,
val?: bigint,
subId?: string,
}
// Unused - in indexer currently
type input = {
txId: string,
blockHeight: int,
inputType: int,
utxoId?: string,
owner?: string,
amount?: bigint,
assetId?: string,
txPointerBlockHeight?: int,
txPointerTxIndex?: int,
witnessIndex?: int,
predicateGasUsed?: int,
predicate?: string,
predicateData?: string,
balanceRoot?: string,
stateRoot?: string,
contract?: string,
sender?: string,
recipient?: string,
nonce?: string,
data?: string,
}
// Unused in indexer currently
type output = {
txId: string,
blockHeight: int,
outputType: int,
to?: string,
amount?: bigint,
assetId?: string,
inputIndex?: int,
balanceRoot?: string,
stateRoot?: string,
contract?: string,
toAddress?: string,
}
type receipt = {
`@as`("receipt_index")
receiptIndex: int,
`@as`("root_contract_id")
rootContractId?: Address.t,
`@as`("tx_id")
txId: string,
`@as`("block_height")
blockHeight: int,
`@as`("receipt_type")
receiptType: FuelSDK.receiptType,
data?: string,
rb?: bigint,
val?: bigint,
`@as`("sub_id")
subId?: string,
amount?: bigint,
`@as`("asset_id")
assetId?: string,
to?: string,
`@as`("to_address")
toAddress?: string,
}
🤖 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/sources/HyperFuelClient.res` around lines 72 - 86, The
ReScript type "receipt" declares camelCase fields but the incoming JS/N-API
Receipt uses snake_case names, so properties will be undefined; update the
"receipt" type to map each field to the actual JS key using `@as` annotations
(e.g., map receiptIndex -> `@as`("receipt_index"), rootContractId ->
`@as`("root_contract_id"), txId -> `@as`("tx_id"), blockHeight ->
`@as`("block_height"), receiptType -> `@as`("receipt_type"), subId -> `@as`("sub_id"),
assetId -> `@as`("asset_id"), toAddress -> `@as`("to_address"), etc.), ensuring
optional fields keep their ? and types (Address.t, string, bigint,
FuelSDK.receiptType) and that every ReScript identifier in the type matches the
corresponding snake_case JS property via `@as`.

Comment on lines 100 to 113
type queryResponseTyped = {
/** Current height of the source hypersync instance */
/** Current height of the source HyperFuel instance */
archiveHeight?: int,
/**
* Next block to query for, the responses are paginated so
* the caller should continue the query from this block if they
* didn't get responses up to the to_block they specified in the Query.
*/
nextBlock: int,
/** Total time it took the hypersync instance to execute the query. */
/** Total time it took the HyperFuel instance to execute the query. */
totalExecutionTime: int,
/** Response data */
data: queryResponseDataTyped,
}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Response metadata fields will be undefined due to name mismatches.

The Rust QueryResponse uses snake_case. Add @as annotations:

🐛 Proposed fix
 type queryResponseTyped = {
   /** Current height of the source HyperFuel instance */
+  `@as`("archive_height")
   archiveHeight?: int,
   /**
    * Next block to query for...
    */
+  `@as`("next_block")
   nextBlock: int,
+  `@as`("total_execution_time")
   /** Total time it took the HyperFuel instance to execute the query. */
   totalExecutionTime: int,
   /** Response data */
   data: queryResponseDataTyped,
 }
🤖 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/sources/HyperFuelClient.res` around lines 100 - 113, The
response type fields in queryResponseTyped use camelCase but the Rust JSON uses
snake_case, so add `@as` annotations to map them: annotate archiveHeight with
`@as`("archive_height"), nextBlock with `@as`("next_block"), and totalExecutionTime
with `@as`("total_execution_time") (leave data as-is if it already matches),
updating the queryResponseTyped type definition to use these `@as` attributes so
the deserializer correctly maps the snake_case JSON to the Rescript/Rust
bindings.

The hyperfuel-client crate's Client supports neither custom user agents
nor authorized /height requests, so requests are now made directly with
reqwest (hyperindex/{version} user agent plus bearer token on both
/height and /query/arrow-ipc), with the crate kept for its wire types
and capnp/arrow response parsing.

- Missing required response columns now surface as a typed MissingFields
  error (same JSON payload protocol as hypersync_source) which the
  ReScript side converts to UnexpectedMissingParams, instead of silently
  defaulting to zero values.
- Fix the dead `HyperSync.GetLogs.Error` catch in HyperFuelSource that
  swallowed WrongInstance and missing-params errors into the generic
  retry path.
- A rejected API token (401) on the height path now logs an actionable
  ENVIO_API_TOKEN message and blocks instead of retrying, mirroring
  HyperSync; covered by tests against a local HTTP server.
- Reject rb filter values that don't fit u64 instead of truncating.
- Drop the now-unused EnvioApiClient module and heightRoute.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK

@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

🧹 Nitpick comments (1)
packages/envio/src/sources/HyperFuelSource.res (1)

5-5: ⚡ Quick win

Avoid keying the 401 path off exception text.

String.includes("401 Unauthorized") makes the non-retryable branch depend on the exact message emitted by the lower layer. A wording change will silently turn auth failures back into the generic retry path. Please surface a typed status/error from HyperFuel.getHeight and branch on that instead.

Also applies to: 475-477

🤖 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/sources/HyperFuelSource.res` at line 5, The current
isUnauthorizedError helper in HyperFuelSource.res relies on exception text
(String.includes("401 Unauthorized")) which is brittle; instead modify
HyperFuel.getHeight to surface a typed status or error (e.g., return a result
type or throw a HyperFuelError with a status/code field) and update
isUnauthorizedError to check that typed status/code (not message); then change
all places that catch or inspect errors from HyperFuel.getHeight (including the
other occurrences around lines referenced 475-477) to branch on the typed
status/code (e.g., error.status === 401 or error.code === "UNAUTHORIZED") and
handle non-retryable auth failures accordingly.
🤖 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/hyperfuel_source/mod.rs`:
- Around line 25-33: The reqwest client in HyperfuelClient::new hardcodes
.timeout(Duration::from_secs(30)) even though ClientConfig currently only has
url and bearer_token; add an optional timeout field (e.g., Option<Duration> or
u64 seconds) to ClientConfig in config.rs and pass it into HyperfuelClient::new
to set the reqwest timeout (or explicitly document the fixed 30s if you choose
not to expose it). Also prevent unbounded buffering in get_selected_data by
replacing res.bytes().await with a streaming/limited read: either read the
response body via a bytes_stream / chunked reader and stop after a safe max size
(configurable via a new max_response_size in ClientConfig) or use
Response::take(max) to cap the read before parsing Arrow IPC, and return an
error if the payload exceeds the cap; update HyperfuelClient::new and
get_selected_data to accept and use the new ClientConfig fields (timeout and
max_response_size).
- Around line 82-87: The code currently buffers the entire response in
HyperfuelClient::get_selected_data using res.bytes().await which allows
unbounded memory use; instead stream the response via the reqwest Response API
(e.g., call res.chunk().await in a loop or use .stream()) and accumulate into a
Vec<u8> while tracking total bytes, rejecting and returning an error once a
configured maximum threshold is exceeded (match the ~512MiB cap intended in
parse.rs/nesting_limit); then pass the collected bytes into
parse::parse_query_response (still via tokio::task::spawn_blocking if needed).
Ensure the rejection uses the same map_err path and clear error context so
oversized responses are rejected before being fully buffered.

In `@packages/envio/src/sources/HyperFuelSource.res`:
- Around line 477-480: The code currently logs a 401 and then awaits a
never-resolving Promise (Promise.make) which parks startup forever; change this
to fail fast by replacing the infinite await with a terminal failure—either
throw a descriptive Error or call process.exit(1) after Logging.error so the
process exits immediately on invalid tokens; update the block around
Logging.error and the Promise.make usage in HyperFuelSource.res to
return/propagate the error instead of blocking.

In `@scenarios/fuel_test/test/HyperFuelHeight_test.res`:
- Around line 28-54: The test currently only calls server->close after the
successful path, leaving the temp server running on failures; wrap the
interaction and assertions that use startServer, capturedHeaders,
HyperFuelSource.make and source.getHeightOrThrow in a try ... finally (or an
equivalent ensure/cleanup helper) and call server->close in the finally block so
the server is always closed on both success and failure; apply the same pattern
to the other similar test (the block that covers lines 56-73).

---

Nitpick comments:
In `@packages/envio/src/sources/HyperFuelSource.res`:
- Line 5: The current isUnauthorizedError helper in HyperFuelSource.res relies
on exception text (String.includes("401 Unauthorized")) which is brittle;
instead modify HyperFuel.getHeight to surface a typed status or error (e.g.,
return a result type or throw a HyperFuelError with a status/code field) and
update isUnauthorizedError to check that typed status/code (not message); then
change all places that catch or inspect errors from HyperFuel.getHeight
(including the other occurrences around lines referenced 475-477) to branch on
the typed status/code (e.g., error.status === 401 or error.code ===
"UNAUTHORIZED") and handle non-retryable auth failures accordingly.
🪄 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: a741977d-fde0-4351-b0f0-ae74ac0751b6

📥 Commits

Reviewing files that changed from the base of the PR and between c616b87 and 3e60839.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • packages/cli/Cargo.toml
  • packages/cli/src/hyperfuel_source/config.rs
  • packages/cli/src/hyperfuel_source/mod.rs
  • packages/cli/src/hyperfuel_source/parse.rs
  • packages/cli/src/hyperfuel_source/query.rs
  • packages/cli/src/hyperfuel_source/types.rs
  • packages/envio/src/sources/EnvioApiClient.res
  • packages/envio/src/sources/HyperFuel.res
  • packages/envio/src/sources/HyperFuel.resi
  • packages/envio/src/sources/HyperFuelClient.res
  • packages/envio/src/sources/HyperFuelSource.res
  • scenarios/fuel_test/test/HyperFuelHeight_test.res
💤 Files with no reviewable changes (2)
  • packages/envio/src/sources/EnvioApiClient.res
  • packages/cli/src/hyperfuel_source/config.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/cli/Cargo.toml
  • packages/envio/src/sources/HyperFuel.resi
  • packages/cli/src/hyperfuel_source/query.rs
  • packages/envio/src/sources/HyperFuel.res
  • packages/envio/src/sources/HyperFuelClient.res

Comment thread packages/cli/src/hyperfuel_source/mod.rs Outdated
Comment thread packages/cli/src/hyperfuel_source/mod.rs Outdated
Comment on lines +477 to +480
Logging.error(`Your ENVIO_API_TOKEN was rejected by HyperFuel (401 Unauthorized). The indexer will not be able to fetch events. Update the token and try again using 'envio start' or 'envio dev'. For more info: https://docs.envio.dev/docs/HyperSync/api-tokens`)
// Retrying an unauthorized request can never succeed, so block forever
let _ = await Promise.make((_, _) => ())
0

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on invalid tokens instead of parking forever.

A 401 here is terminal until config changes. Awaiting a never-resolving promise makes startup hang indefinitely rather than exiting with a clear failure, which is rough on CI, supervisors, and users trying to recover from a bad ENVIO_API_TOKEN.

🤖 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/sources/HyperFuelSource.res` around lines 477 - 480, The
code currently logs a 401 and then awaits a never-resolving Promise
(Promise.make) which parks startup forever; change this to fail fast by
replacing the infinite await with a terminal failure—either throw a descriptive
Error or call process.exit(1) after Logging.error so the process exits
immediately on invalid tokens; update the block around Logging.error and the
Promise.make usage in HyperFuelSource.res to return/propagate the error instead
of blocking.

Comment thread scenarios/fuel_test/test/HyperFuelHeight_test.res
Close the server in both the success and failure paths so a failing
assertion can't leak a listener and destabilize later test runs.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
Go back to driving queries and height checks through
hyperfuel_client::Client (get_arrow + get_height), which handles HTTP,
auth, and Cap'n Proto/Arrow decoding internally. This drops the direct
capnp dependency and the parse.rs envelope decoder, keeping capnp purely
transitive — matching how the Hypersync client is wired.

The only thing lost is the custom hyperindex/{version} user-agent, which
the crate's Client can't set and the previous JS client never sent. Bearer
auth still applies to both /height and /query, so the 401 handling and
header test are unaffected.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK

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

♻️ Duplicate comments (4)
packages/envio/src/sources/HyperFuelClient.res (4)

3-6: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Field name mismatch will prevent bearer token from being passed to the Rust client.

The Rust ClientConfig (in packages/cli/src/hyperfuel_source/config.rs line 9) uses snake_case bearer_token, but this ReScript type uses camelCase bearerToken without @as annotation. N-API preserves Rust field names, so the JS object needs bearer_token but ReScript will create bearerToken.

🐛 Proposed fix
 type cfg = {
   url: string,
+  `@as`("bearer_token")
   bearerToken?: string,
 }
🤖 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/sources/HyperFuelClient.res` around lines 3 - 6, The
ReScript type cfg defines bearerToken in camelCase which won't match the Rust
ClientConfig's snake_case bearer_token; update the cfg type in
HyperFuelClient.res so the JS object uses the Rust field name—either rename the
field to bearer_token or annotate the existing bearerToken with
`@as`("bearer_token") so the produced object matches the Rust ClientConfig; ensure
the url field remains unchanged and verify consumer code using cfg is updated if
you rename the field.

37-42: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Field name mismatches in receiptSelection will break receipt filtering.

The Rust ReceiptSelection uses snake_case field names (following Rust conventions). Add @as annotations for root_contract_id, receipt_type, and tx_status.

🐛 Proposed fix
 type receiptSelection = {
+  `@as`("root_contract_id")
   rootContractId?: array<Address.t>,
+  `@as`("receipt_type")
   receiptType?: array<FuelSDK.receiptType>,
   rb?: array<bigint>,
+  `@as`("tx_status")
   txStatus?: array<int>,
 }
🤖 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/sources/HyperFuelClient.res` around lines 37 - 42, The
TypeScript/ReasonML type receiptSelection currently uses camelCase fields
(rootContractId, receiptType, txStatus) that must map to Rust's snake_case
names; update the type definition for receiptSelection by adding `@as` annotations
on rootContractId -> "root_contract_id", receiptType -> "receipt_type", and
txStatus -> "tx_status" so the generated bindings match the Rust
ReceiptSelection field names and receipt filtering works correctly.

100-113: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Response metadata fields will be undefined due to name mismatches.

The Rust QueryResponse uses snake_case. Add @as annotations for archive_height, next_block, and total_execution_time.

🐛 Proposed fix
 type queryResponseTyped = {
   /** Current height of the source HyperFuel instance */
+  `@as`("archive_height")
   archiveHeight?: int,
   /**
    * Next block to query for, the responses are paginated so
    * the caller should continue the query from this block if they
    * didn't get responses up to the to_block they specified in the Query.
    */
+  `@as`("next_block")
   nextBlock: int,
+  `@as`("total_execution_time")
   /** Total time it took the HyperFuel instance to execute the query. */
   totalExecutionTime: int,
   /** Response data */
   data: queryResponseDataTyped,
 }
🤖 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/sources/HyperFuelClient.res` around lines 100 - 113, The
TypeScript/Reason type queryResponseTyped currently uses camelCase field names
but the Rust QueryResponse uses snake_case, so archive_height, next_block, and
total_execution_time will be undefined; update the type definition for
queryResponseTyped to map the Rust names by adding `@as` annotations for
archive_height -> archiveHeight, next_block -> nextBlock, and
total_execution_time -> totalExecutionTime on the corresponding fields (i.e., on
archiveHeight, nextBlock, totalExecutionTime) so the deserializer binds the
snake_case Rust fields to these properties.

72-86: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Receipt response fields will be undefined due to name mismatches.

The Rust Receipt struct (in packages/cli/src/hyperfuel_source/types.rs) uses snake_case. Without @as annotations, ReScript will look for camelCase properties that don't exist in the JS object returned by N-API.

🐛 Proposed fix
 type receipt = {
+  `@as`("receipt_index")
   receiptIndex: int,
+  `@as`("root_contract_id")
   rootContractId?: Address.t,
+  `@as`("tx_id")
   txId: string,
+  `@as`("block_height")
   blockHeight: int,
+  `@as`("receipt_type")
   receiptType: FuelSDK.receiptType,
   data?: string,
   rb?: bigint,
   val?: bigint,
+  `@as`("sub_id")
   subId?: string,
   amount?: bigint,
+  `@as`("asset_id")
   assetId?: string,
   to?: string,
+  `@as`("to_address")
   toAddress?: string,
 }
🤖 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/sources/HyperFuelClient.res` around lines 72 - 86, The
ReScript record type `receipt` uses camelCase field names but the Rust/N-API
JSON uses snake_case, so fields will be undefined; update the `type receipt`
declaration to map each camelCase field to its snake_case JS key using `@as`
annotations (e.g., map receiptIndex -> "receipt_index", rootContractId ->
"root_contract_id", txId -> "tx_id", blockHeight -> "block_height", receiptType
-> "receipt_type", subId -> "sub_id", assetId -> "asset_id", toAddress ->
"to_address", etc.) so the JS object keys from N-API are correctly bound to the
ReScript fields. Ensure every field that differs in casing (including amount,
rb, val, data, to, etc.) is annotated to the exact snake_case name.
🤖 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.

Duplicate comments:
In `@packages/envio/src/sources/HyperFuelClient.res`:
- Around line 3-6: The ReScript type cfg defines bearerToken in camelCase which
won't match the Rust ClientConfig's snake_case bearer_token; update the cfg type
in HyperFuelClient.res so the JS object uses the Rust field name—either rename
the field to bearer_token or annotate the existing bearerToken with
`@as`("bearer_token") so the produced object matches the Rust ClientConfig; ensure
the url field remains unchanged and verify consumer code using cfg is updated if
you rename the field.
- Around line 37-42: The TypeScript/ReasonML type receiptSelection currently
uses camelCase fields (rootContractId, receiptType, txStatus) that must map to
Rust's snake_case names; update the type definition for receiptSelection by
adding `@as` annotations on rootContractId -> "root_contract_id", receiptType ->
"receipt_type", and txStatus -> "tx_status" so the generated bindings match the
Rust ReceiptSelection field names and receipt filtering works correctly.
- Around line 100-113: The TypeScript/Reason type queryResponseTyped currently
uses camelCase field names but the Rust QueryResponse uses snake_case, so
archive_height, next_block, and total_execution_time will be undefined; update
the type definition for queryResponseTyped to map the Rust names by adding `@as`
annotations for archive_height -> archiveHeight, next_block -> nextBlock, and
total_execution_time -> totalExecutionTime on the corresponding fields (i.e., on
archiveHeight, nextBlock, totalExecutionTime) so the deserializer binds the
snake_case Rust fields to these properties.
- Around line 72-86: The ReScript record type `receipt` uses camelCase field
names but the Rust/N-API JSON uses snake_case, so fields will be undefined;
update the `type receipt` declaration to map each camelCase field to its
snake_case JS key using `@as` annotations (e.g., map receiptIndex ->
"receipt_index", rootContractId -> "root_contract_id", txId -> "tx_id",
blockHeight -> "block_height", receiptType -> "receipt_type", subId -> "sub_id",
assetId -> "asset_id", toAddress -> "to_address", etc.) so the JS object keys
from N-API are correctly bound to the ReScript fields. Ensure every field that
differs in casing (including amount, rb, val, data, to, etc.) is annotated to
the exact snake_case name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f800089-fa67-4ca9-a6cc-3505c20f54b7

📥 Commits

Reviewing files that changed from the base of the PR and between 84d0808 and 682096b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • packages/cli/Cargo.toml
  • packages/cli/src/hyperfuel_source/config.rs
  • packages/cli/src/hyperfuel_source/mod.rs
  • packages/cli/src/hyperfuel_source/types.rs
  • packages/envio/src/sources/HyperFuelClient.res
  • scenarios/fuel_test/test/HyperFuelHeight_test.res
💤 Files with no reviewable changes (1)
  • packages/cli/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/hyperfuel_source/types.rs

4.0.0 adds new_with_agent (intended for language bindings / HyperIndex),
so the hyperindex/{version} user-agent is set again while still going
through the crate's Client — no custom HTTP layer or capnp dependency.

The config field was renamed bearer_token -> api_token, and the client
now validates that the token is a UUID, so the height tests use a
UUID-shaped token.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
The IPC features were only needed by the removed parse.rs envelope
decoder. The client now returns already-decoded ArrowBatches, so
hyperfuel_source only reads polars-arrow array types.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
…4vrkl

# Conflicts:
#	packages/cli/Cargo.toml
#	packages/cli/src/lib.rs
#	packages/envio/src/ChainFetcher.res
#	packages/envio/src/Core.res
Match the evm_hypersync_source / svm_hypersync_source naming convention.
The napi HyperfuelClient class name is unchanged, so no JS/ReScript
references need updating.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
…4vrkl

# Conflicts:
#	packages/cli/src/svm_hypersync_source/mod.rs
#	packages/envio/src/Core.res
…4vrkl

# Conflicts:
#	packages/envio/src/ChainFetcher.res
Use api_token (apiToken in JS) to match hyperfuel_client::ClientConfig's
field name, and extract the shared "first line of the error" mapping into
a request_err helper instead of duplicating the closure.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
Make the napi config's api_token required (the source already throws when
it's missing), and drop the per-URL CachedClients dict. The client is now
built once in HyperFuelSource.make with a mkLogAndRaise guard and threaded
into GetLogs.query / getHeight as ~client, matching how HyperSyncSource
manages its client.

https://claude.ai/code/session_01JtXswv5kkvQWpiZGvY17AK
@DZakh
DZakh merged commit 6a325d3 into main Jun 19, 2026
8 checks passed
@DZakh
DZakh deleted the claude/kind-goodall-z4vrkl branch June 19, 2026 09:54
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