Migrate HyperFuel client from JS to Rust native binding - #1298
Conversation
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
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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. ChangesHyperFuel Native Client Integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
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 winCritical: Field name mismatches in query type will cause query to fail.
The Rust
Querystruct usesfrom_blockandfield_selection(snake_case). Without@asannotations, the JS object will havefromBlockandfieldSelectionwhich 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 valueAdd explicit type annotation for
Utils.magiccast.Line 104 uses a generic type parameter
'ain the cast. As per coding guidelines,Utils.magicshould 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 valueBlocks are always wrapped in Some, even when empty.
Line 131 always wraps the blocks vector in
Some(), even if the vector is empty. This meansQueryResponseData.blocksis neverNone, onlySome([])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:
- The field should be non-optional in the type definition
- Empty vectors should map to
Nonefor 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 tradeoffPotential overflow in u64 to i64 conversion.
Lines 126-128 cast
u64values toi64using theasoperator. Ifarchive_height,next_block, ortotal_execution_timeexceedi64::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::MAXin practice, but the cast is technically unsafe. Consider either:
- Using
try_into()with error handling for values >i64::MAX- 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 valueConsider explicit validation for negative timeout values.
The current conversion silently drops negative
http_req_timeout_millisvalues 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 tradeoffError 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:
- The full error should be logged at debug level while the truncated version is returned to the user
- Truncation should preserve the first N lines (e.g., 3-5) rather than just 1
- 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
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (13)
packages/cli/Cargo.tomlpackages/cli/src/hyperfuel_source/config.rspackages/cli/src/hyperfuel_source/mod.rspackages/cli/src/hyperfuel_source/query.rspackages/cli/src/hyperfuel_source/types.rspackages/cli/src/lib.rspackages/envio/package.jsonpackages/envio/src/ChainFetcher.respackages/envio/src/Core.respackages/envio/src/sources/HyperFuel.respackages/envio/src/sources/HyperFuel.resipackages/envio/src/sources/HyperFuelClient.respackages/envio/src/sources/HyperFuelSource.res
💤 Files with no reviewable changes (1)
- packages/envio/package.json
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| type cfg = { | ||
| url: string, | ||
| bearerToken?: string, | ||
| http_req_timeout_millis?: int, | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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>, | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| } |
There was a problem hiding this comment.
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.
| 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`.
| 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, | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
packages/envio/src/sources/HyperFuelSource.res (1)
5-5: ⚡ Quick winAvoid 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 fromHyperFuel.getHeightand 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
packages/cli/Cargo.tomlpackages/cli/src/hyperfuel_source/config.rspackages/cli/src/hyperfuel_source/mod.rspackages/cli/src/hyperfuel_source/parse.rspackages/cli/src/hyperfuel_source/query.rspackages/cli/src/hyperfuel_source/types.rspackages/envio/src/sources/EnvioApiClient.respackages/envio/src/sources/HyperFuel.respackages/envio/src/sources/HyperFuel.resipackages/envio/src/sources/HyperFuelClient.respackages/envio/src/sources/HyperFuelSource.resscenarios/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
| 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 |
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
♻️ Duplicate comments (4)
packages/envio/src/sources/HyperFuelClient.res (4)
3-6:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winCritical: Field name mismatch will prevent bearer token from being passed to the Rust client.
The Rust
ClientConfig(inpackages/cli/src/hyperfuel_source/config.rsline 9) uses snake_casebearer_token, but this ReScript type uses camelCasebearerTokenwithout@asannotation. N-API preserves Rust field names, so the JS object needsbearer_tokenbut ReScript will createbearerToken.🐛 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 winCritical: Field name mismatches in receiptSelection will break receipt filtering.
The Rust
ReceiptSelectionuses snake_case field names (following Rust conventions). Add@asannotations forroot_contract_id,receipt_type, andtx_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 winCritical: Response metadata fields will be undefined due to name mismatches.
The Rust
QueryResponseuses snake_case. Add@asannotations forarchive_height,next_block, andtotal_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 winCritical: Receipt response fields will be undefined due to name mismatches.
The Rust
Receiptstruct (inpackages/cli/src/hyperfuel_source/types.rs) uses snake_case. Without@asannotations, 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
packages/cli/Cargo.tomlpackages/cli/src/hyperfuel_source/config.rspackages/cli/src/hyperfuel_source/mod.rspackages/cli/src/hyperfuel_source/types.rspackages/envio/src/sources/HyperFuelClient.resscenarios/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
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 nativehyperfuel-clientRust crate.Key Changes
Rust NAPI binding: Created new
hyperfuel_sourcemodule inpackages/cli/srcwith:HyperfuelClientstruct wrapping the nativehyperfuel_client::ClientQuery,ReceiptSelection, andFieldSelectiontypes for query constructionQueryResponse,Receipt, andBlocktypes for response handlingReScript client simplification: Updated
HyperFuelClient.resto:blockFieldOptionsandreceiptFieldOptionsto only essential fieldstransactionFieldSelectionand related transaction typesurlandbearerTokenCore.getAddon().hyperfuelClientAPI token handling:
apiTokenparameter toHyperFuelSource.makeoptionsENVIO_API_TOKENenvironment variable with helpful error messagebearerTokenconfig fieldQuery interface updates:
inputs,outputs,includeAllBlocks,maxNumBlocks,maxNumTransactionsfrom query optionsBlockDatamodule andqueryBlockDatafunction (block querying now handled via standard receipt queries)GetLogs.queryto acceptapiTokenparameterDependencies:
hyperfuel-client = "3.2.0"andpolars-arrow = "0.42"toCargo.toml@envio-dev/hyperfuel-clientfrompackages/envio/package.jsonImplementation 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
Breaking Changes
ENVIO_API_TOKENenvironment variable for API authentication.Improvements