Move Fuel & SVM selection/routing into Rust; unify HyperSync source naming - #1462
Conversation
Routing in the Rust DecoderCore previously picked a single registration per log (owning contract, else wildcard fallback) and rejected duplicate signatures per contract and multiple wildcards at construction. Now every matching registration gets its own item: wildcards always match, contract- bound registrations match iff the log's address is owned by the contract, with no fallback tier. The log is decoded once and each registration's own param names are applied. Since one registration's broader selection can now fetch logs for a narrower sibling, routing is scoped to the query's registration indexes (threaded into process_response and the RPC page fetch) and re-applies each registration's static topic filters and startBlock gate; ContractAddresses markers stay match-any in Rust — the JS clientAddressFilter owns that check. startBlock now crosses the napi boundary on the registration. The RPC page dedup key gains the registration index so fan-out items survive, and selection compression dedupes repeated topic0s and identical filtered selections that multiple same-signature registrations produce. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WN93NjbyYQhuiNgdpjQFFu
A log can only route to registrations belonging to the query selection that fetched it — registration_indexes scoping plus the re-applied static topic filters already guarantee that, and the JS side's partitioning by start block keeps a query from ranging below its registrations' start blocks. The per-log startBlock check was redundant, so the field no longer crosses the napi boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WN93NjbyYQhuiNgdpjQFFu
Registrations sharing (topic0, topic count) were forced onto one positional decoder, so a collision with a different indexed/body split was rejected at construction. Now each distinct layout in the group gets its own decoder (same-layout variants still share one) and a log decodes lazily, once per matched layout, under each registration's own declaration. A layout the log's bytes don't decode under contributes no items; the failure only surfaces as an error when no matched layout decodes, so genuinely malformed data still can't disappear silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WN93NjbyYQhuiNgdpjQFFu
The decoder's MetaKey lookup table (signature map, per-contract variant index, wildcard index, per-query active set) existed to answer one question per log: which of the query's registrations match it. Answer it literally instead: DecoderCore now just holds all registrations by index, a query resolves its selection once into a SelectionDecoder, and routing is a straight scan over that selection — match on signature, emitter (wildcard or owning contract), and static topic filters, then decode under the matching registration's own declaration. This removes the MetaKey/RegisteredEvent/EventVariant structure, the decoder-sharing bookkeeping (each registration owns its decoder), and the per-log HashSet membership checks. A selection is one partition's events, small enough that the scan is also cheaper than the hashed lookups it replaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WN93NjbyYQhuiNgdpjQFFu
OnEventRegistration (the napi input DTO) becomes OnEventRegistrationInput, freeing the name for the decoder's parsed form (was EventRegistration) — matching the JS side's onEventRegistration naming for the same concept. The static topic filter DNF moves behind a TopicFilters type owning its parse and matches, so the registration struct and matching logic no longer carry the raw nested shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WN93NjbyYQhuiNgdpjQFFu
Match the EVM client's shape on both remaining HyperSync sources: the whole per-(event, chain) registration set crosses the napi boundary once at client construction, and a query names only its block range, registration indexes, and current addresses. Fuel: - HyperfuelClient::new takes registrations (index, contractName, kind, logId, isWildcard); a new selection builder ports the receipt-selection grouping from HyperFuelSource.getSelectionConfig, requesting only the receipt columns the selection's kinds read. - get_event_items routes receipts by (rb | receiptType) with fan-out to every matching registration (wildcards plus the owning contract), flattens kind-specific columns onto items (normalising TransferOut's toAddress into to), validates kind-required columns and the block join via the MissingFields protocol, and drops receipts that route nowhere. LogData param decoding stays in JS against the contract ABI. SVM: - SvmHypersyncClient::new takes registrations carrying discriminator, account filters, isInner, includeLogs, field selections, and the Borsh schema pieces — absorbing the programSchemas config option. - get_event_items ports buildInstructionSelections (per-AND-group fan-out, deduplicated) and the per-selection block/transaction/log/tokenBalance field unions, then routes instructions by discriminator longest-first (d8/d4/d2/d1, program-wide fallback) with fan-out and per-registration re-application of account filters and the isInner constraint. Borsh decoding and instruction-scoped log attachment happen inline. Both sources shrink to index-mapped item building, EventRouter.res is deleted, and its duplicate/wildcard-collision validation is inlined into HandlerRegister.finishRegistration. The JS selection/routing tests move to Rust unit tests alongside the new builders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Myo6vsGNF9ACsAbBV6PXSk
…rmance-6dta8l # Conflicts: # packages/cli/src/evm_hypersync_source/decode.rs # packages/cli/src/evm_hypersync_source/mod.rs # packages/cli/src/evm_rpc_source/mod.rs
…aming
Unify the naming across all three ecosystems to `{Evm|Fuel|Svm}HyperSync{Client|Source}`,
fixing the casing drift left after the Fuel/SVM port:
- napi client classes: EvmHypersyncClient -> EvmHyperSyncClient,
SvmHypersyncClient -> SvmHyperSyncClient, HyperfuelClient -> FuelHyperSyncClient
(Rust structs + Core.res @as bindings/field/ctor names + test mock keys).
- ReScript modules: HyperSyncSource -> EvmHyperSyncSource,
HyperFuelSource -> FuelHyperSyncSource, HyperFuelClient -> FuelHyperSyncClient,
HyperFuel -> FuelHyperSync (with .resi).
Only code identifiers are renamed; the "HyperFuel"/"HyperSync" product-name strings
in error messages, URLs, and source `name` fields are left as the real service names.
Dead-code pass: the six Rust "never used" warnings (evm/svm block+transaction
field-name fns, encode_indexed_topic, the shared field_names helper) are all
`#[napi]` exports consumed from ReScript (drift-protection tests, EventConfigBuilder),
so they are live cross-boundary API, not dead code — left in place. ReScript's
`warnings.error = "+a"` makes the compiler reject any unused binding, so the
ReScript side is dead-code-free by construction. No removals were warranted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Myo6vsGNF9ACsAbBV6PXSk
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughHyperSync event retrieval is migrated to registration-driven routing across EVM, SVM, and Fuel. New client contracts, Rust-side selection and routing, event-item APIs, source integrations, schema handling, and registration validation replace older query and EventRouter paths. ChangesHyperSync routing migration
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant EventRegistration
participant HyperSyncSource
participant HyperSyncClient
participant RustRouter
participant HyperSyncAPI
EventRegistration->>HyperSyncSource: configure registrations
HyperSyncSource->>HyperSyncClient: request event items
HyperSyncClient->>RustRouter: build selections and route results
RustRouter->>HyperSyncAPI: execute chain query
HyperSyncAPI-->>RustRouter: raw chain data
RustRouter-->>HyperSyncClient: routed event items
HyperSyncClient-->>HyperSyncSource: paginated response
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
…rmance-6dta8l # Conflicts: # packages/envio-tests/test/EventRouter_svm_test.res
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a097ab335f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| validateEventIdOrThrow( | ||
| ~eventId=eventConfig.id, |
There was a problem hiding this comment.
Include program id when validating SVM wildcard handlers
For SVM configs, eventConfig.id is only the discriminator value (or "none" for program-wide handlers), not the program id. This means two wildcard SVM handlers on different programs with the same discriminator/program-wide id are rejected here as a wildcard collision even though the old router keyed SVM dispatch by (programId, discriminator) and the new Rust routing still scopes matches by program_id. Include the program id in the validation key for SVM so valid multi-program wildcard configs can start.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/envio/src/Internal.res (1)
182-182: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove caller-specific wording from this comment.
Keep the nullable-schema invariant;
RpcSourceandEvmHyperSyncSourceare implementation consumers, not useful durable context.Proposed fix
-// Static sets of nullable field names — used by RpcSource and EvmHyperSyncSource to wrap schemas with S.nullable +// Static sets of field names whose source schemas must allow null.As per coding guidelines, don't write comments that state “which callers use a value.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/envio/src/Internal.res` at line 182, Update the comment above the nullable field-name sets to describe only the invariant that these static sets identify nullable fields used to wrap schemas with S.nullable. Remove the caller-specific references to RpcSource and EvmHyperSyncSource.Source: Coding guidelines
packages/envio/src/sources/FuelHyperSync.res (1)
71-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAddress the archiveHeight default TODO. Defaulting
res.archiveHeightto0when the source omits it flows intoFuelHyperSyncSource(Line 105) asknownHeight, so a genuineNonefrom HyperFuel would report a chain height of0rather than surfacing the missing value. Consider treating an absentarchiveHeightas an explicit error or retry rather than silently defaulting.Want me to open an issue to track removing this default and defining the absent-
archiveHeightbehavior?🤖 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/FuelHyperSync.res` at line 71, Remove the `Option.getOr(0)` fallback when constructing `archiveHeight` in the source response flow, and define explicit handling for an absent value—either return an error or trigger the established retry path. Ensure `FuelHyperSyncSource` does not receive `knownHeight` as `0` when HyperFuel omits `archiveHeight`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/envio/src/HandlerRegister.res`:
- Around line 626-629: In the wildcard event validation error within the
HandlerRegister logic, correct the message text from “interfer” to “interfere”
while preserving the existing error construction and context.
In `@packages/envio/src/sources/SvmHyperSyncSource.res`:
- Around line 17-20: Update synthLogIndex so its per-transaction stride exceeds
every possible addrSum, preventing nested instruction paths from colliding with
later transactions; retain the existing addrSum encoding while correcting the
outer-only mapping so [i] produces transactionIndex times the stride plus i + 1.
---
Nitpick comments:
In `@packages/envio/src/Internal.res`:
- Line 182: Update the comment above the nullable field-name sets to describe
only the invariant that these static sets identify nullable fields used to wrap
schemas with S.nullable. Remove the caller-specific references to RpcSource and
EvmHyperSyncSource.
In `@packages/envio/src/sources/FuelHyperSync.res`:
- Line 71: Remove the `Option.getOr(0)` fallback when constructing
`archiveHeight` in the source response flow, and define explicit handling for an
absent value—either return an error or trigger the established retry path.
Ensure `FuelHyperSyncSource` does not receive `knownHeight` as `0` when
HyperFuel omits `archiveHeight`.
🪄 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: 7b72d0f1-d548-4b01-994a-59606ec79cd0
📒 Files selected for processing (38)
packages/cli/src/evm_hypersync_source/mod.rspackages/cli/src/fuel_hypersync_source/mod.rspackages/cli/src/fuel_hypersync_source/query.rspackages/cli/src/fuel_hypersync_source/selection.rspackages/cli/src/fuel_hypersync_source/types.rspackages/cli/src/svm_hypersync_source/borsh_decoder.rspackages/cli/src/svm_hypersync_source/config.rspackages/cli/src/svm_hypersync_source/mod.rspackages/cli/src/svm_hypersync_source/selection.rspackages/cli/src/svm_hypersync_source/types.rspackages/envio-tests/test/EventRouter_svm_test.respackages/envio-tests/test/HyperSyncClient_test.respackages/envio-tests/test/SvmHyperSyncSource_test.respackages/envio/src/ChainState.respackages/envio/src/Core.respackages/envio/src/FetchState.respackages/envio/src/HandlerRegister.respackages/envio/src/Internal.respackages/envio/src/sources/EventRouter.respackages/envio/src/sources/EvmChain.respackages/envio/src/sources/EvmHyperSyncSource.respackages/envio/src/sources/FuelHyperSync.respackages/envio/src/sources/FuelHyperSync.resipackages/envio/src/sources/FuelHyperSyncClient.respackages/envio/src/sources/FuelHyperSyncSource.respackages/envio/src/sources/HyperFuel.respackages/envio/src/sources/HyperFuel.resipackages/envio/src/sources/HyperFuelClient.respackages/envio/src/sources/HyperFuelSource.respackages/envio/src/sources/HyperSyncClient.respackages/envio/src/sources/SvmHyperSyncClient.respackages/envio/src/sources/SvmHyperSyncSource.resscenarios/fuel_test/test/HyperFuelHeight_test.resscenarios/fuel_test/test/HyperFuelSource_test.resscenarios/svm_flow_xray/src/indexer.test.tsscenarios/svm_metaplex_demo/src/indexer.test.tsscenarios/test_codegen/test/SourceBlockHashes_test.resscenarios/test_codegen/test/lib_tests/EventRouter_test.res
💤 Files with no reviewable changes (10)
- packages/envio/src/sources/HyperFuel.resi
- scenarios/test_codegen/test/lib_tests/EventRouter_test.res
- scenarios/fuel_test/test/HyperFuelSource_test.res
- packages/envio-tests/test/EventRouter_svm_test.res
- packages/cli/src/svm_hypersync_source/config.rs
- packages/cli/src/fuel_hypersync_source/query.rs
- packages/envio/src/sources/EventRouter.res
- packages/envio/src/sources/HyperFuelSource.res
- packages/envio/src/sources/HyperFuelClient.res
- packages/envio/src/sources/HyperFuel.res
| let synthLogIndex = (~transactionIndex, ~instructionAddress) => { | ||
| let addrSum = instructionAddress->Array.reduce(0, (acc, n) => acc * 1024 + n + 1) | ||
| transactionIndex * 65536 + addrSum | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect how logIndex participates in FetchState ordering/dedup.
rg -nP -C4 '\blogIndex\b' packages/envio/src/FetchState.resRepository: enviodev/hyperindex
Length of output: 2153
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- SvmHyperSyncSource.res around synthLogIndex ---'
sed -n '1,80p' packages/envio/src/sources/SvmHyperSyncSource.res
echo
echo '--- FetchState ordering around compareBufferItem ---'
sed -n '690,740p' packages/envio/src/FetchState.res
echo
echo '--- FetchState block item logIndex generation ---'
sed -n '820,870p' packages/envio/src/FetchState.resRepository: enviodev/hyperindex
Length of output: 6859
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
def synth(tx, instr):
addr_sum = 0
for n in instr:
addr_sum = addr_sum * 1024 + n + 1
return tx * 65536 + addr_sum
samples = [
(0, [0]),
(0, [63]),
(0, [63,0]),
(0, [5,10]),
(1, [0]),
]
for tx, instr in samples:
print(tx, instr, synth(tx, instr))
# Explore the max addrSum for bounded values length <= 10 and each n <= 1000.
# This is just a growth probe, not an exhaustive search over 1001^10.
addr_sum = 0
for i in range(10):
addr_sum = addr_sum * 1024 + 1000 + 1
print(i+1, addr_sum)
PYRepository: enviodev/hyperindex
Length of output: 424
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -nP -C3 '\bblockNumberAndLogIndex\b|\bcompareBufferItem\b|same log routed to the same registration|logIndex:' packages/envio/src/FetchState.resRepository: enviodev/hyperindex
Length of output: 2365
synthLogIndex needs a wider per-transaction stride. addrSum can exceed 65536 for nested paths, so distinct instructions can collide on logIndex across transactions (for example, [63,0] and the next transaction’s [0] both land on 65537). FetchState treats equal (blockNumber, logIndex, registration index) as a duplicate, so that can silently drop an event. The outer-only example is also off: a single [i] maps to tx * 65536 + i + 1.
🤖 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/SvmHyperSyncSource.res` around lines 17 - 20,
Update synthLogIndex so its per-transaction stride exceeds every possible
addrSum, preventing nested instruction paths from colliding with later
transactions; retain the existing addrSum encoding while correcting the
outer-only mapping so [i] produces transactionIndex times the stride plus i + 1.
- Scope the SVM wildcard/duplicate validation key by (programId, discriminator) instead of the discriminator alone. SVM eventConfig.id is only the discriminator (or "none"), so two wildcard handlers on different programs sharing a discriminator were falsely rejected as a wildcard collision even though Rust routing scopes matches by program_id (Codex P2). - Drop the caller list from the nullable-field-names comment in Internal.res. - Fix "interfer" -> "interfere" in the wildcard-collision error message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Myo6vsGNF9ACsAbBV6PXSk
- Fuel: replace `Registration.kind: FuelEventKind` + `rb: Option<u64>` with an internal `RegistrationKind` enum whose `LogData` variant carries `rb`, making the invalid states unrepresentable and dropping the `unwrap_or_default`. - Guard integer overflow on the inclusive→exclusive `to_block`/`to_slot` conversion (checked_add), and make SVM `hex_to_bytes` slice on char boundaries so a malformed discriminator returns an error instead of panicking. - Extract the per-chain dispatch-id validation out of `finishRegistration` into `makeEventIdValidator`/`validateEventIdOrThrow` and restore unit tests for duplicate-event and wildcard-collision detection (replacing the deleted EventRouter tests), including program-scoped SVM ids. - Restore `tx_status = [1]` coverage in the Fuel selection tests and add a focused test asserting every built selection filters to successful txs. - Rename the stale HyperFuelHeight test file to FuelHyperSyncSourceHeight. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E887S2x7EfB1hcVvvVLVz3
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/envio-tests/test/HandlerRegisterValidation_test.res`:
- Line 7: Remove the refactor-history comment near the top of the test file,
while preserving the existing comments on lines 3–6 that document the collision
risk.
🪄 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: ccc427ff-2914-4e4b-a183-8213e43cf5c0
📒 Files selected for processing (7)
packages/cli/src/fuel_hypersync_source/mod.rspackages/cli/src/fuel_hypersync_source/selection.rspackages/cli/src/svm_hypersync_source/mod.rspackages/envio-tests/test/HandlerRegisterValidation_test.respackages/envio/src/HandlerRegister.respackages/envio/src/HandlerRegister.resiscenarios/fuel_test/test/FuelHyperSyncSourceHeight_test.res
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/cli/src/fuel_hypersync_source/selection.rs
- packages/envio/src/HandlerRegister.res
- packages/cli/src/fuel_hypersync_source/mod.rs
- packages/cli/src/svm_hypersync_source/mod.rs
- fuel_hypersync_source/mod.rs: FuelEventKind is only used by the #[cfg(test)] module now that non-test routing uses RegistrationKind, so move it out of the crate-level import into the test module. Fixes `cargo clippy -- -D warnings`. - HandlerRegisterValidation_test.res: drop the refactor-history line per the no-narrate-the-refactor guideline (keep the collision-risk rationale). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Myo6vsGNF9ACsAbBV6PXSk
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f6a79053d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let mut selection = BuiltInstructionSelection { | ||
| program_id: reg.program_id.clone(), | ||
| discriminator_hex: reg.discriminator_hex.clone(), |
There was a problem hiding this comment.
Scope SVM selections to the current partition
When an SVM chain has multiple non-wildcard programs split across FetchState partitions, normalSelection still contains all address-dependent registrations, but each partition's addressesByContractName only contains that partition's programs. This builder emits an InstructionSelection for every selected registration from its static program_id, so every partition fetches every normal SVM program and then drops the rows whose program is not in that partition during routing; on noisy programs this can burn the maxNumInstructions page budget on unusable instructions and make the intended partition crawl or starve. Pass the partition address map into the SVM selection builder and skip non-wildcard registrations whose contract has no address in the current partition, as the Fuel builder does.
Useful? React with 👍 / 👎.
Summary
Brings the Fuel and Solana (SVM) HyperSync sources up to the same architecture the EVM source already uses (merged in #1446): the full per-(event, chain) registration set crosses the napi boundary once at client construction, and each query sends only
{block range, registration indexes, addresses}. Selection-building, routing, discriminator/receipt dispatch, and (SVM) Borsh decoding now run in Rust instead of crossing the boundary per-item in JS. Also unifies the naming across all three ecosystems.Because the EVM fan-out PR is already on
main, this PR's diff is only the Fuel/SVM work plus the naming cleanup.What changed
Fuel → Rust (
packages/cli/src/fuel_hypersync_source/)FuelHyperSyncClient::new(cfg, userAgent, registrations)takes registrations(index, contractName, kind, logId, isWildcard); a newselection.rsports the receipt-selection grouping from the oldgetSelectionConfigand requests only the receipt columns the selection's kinds read.get_event_itemsroutes receipts by(receiptType | rb)+ contract ownership, fans each out to every matching registration (wildcards + owning contract), flattens kind-specific columns onto items (normalisingTransferOut'stoAddressintoto), and validates kind-required columns / block joins via the sharedMissingFieldsprotocol.LogDataparam decoding stays in JS against the contract ABI.SVM → Rust (
packages/cli/src/svm_hypersync_source/)SvmHyperSyncClient::newabsorbs the oldprogramSchemasconfig: each registration carries its Borsh schema pieces (accounts/args/definedTypes), grouped per program in Rust.get_event_itemsportsbuildInstructionSelections(per-AND-group fan-out, deduplicated) and the block/transaction/log/tokenBalance field unions, then routes instructions by discriminator longest-first (d8→d4→d2→d1, program-wide fallback) with fan-out and per-registration re-application of account filters and theisInnerconstraint. Borsh decoding, instruction-scoped log attachment, and the failed-transaction (isCommitted) filter run inline.Shared
EventRouter.resis deleted; routing now lives in Rust and its duplicate / wildcard-collision validation is inlined intoHandlerRegister.finishRegistration(identical error messages).Naming — unified to
{Evm|Fuel|Svm}HyperSync{Client|Source}:EvmHypersyncClient→EvmHyperSyncClient,SvmHypersyncClient→SvmHyperSyncClient,HyperfuelClient→FuelHyperSyncClient.HyperSyncSource→EvmHyperSyncSource,HyperFuelSource→FuelHyperSyncSource,HyperFuelClient→FuelHyperSyncClient,HyperFuel→FuelHyperSync."HyperFuel"/"HyperSync"product-name strings in error messages, URLs, and sourcenamefields are left as the real service names.Behavior change
Fan-out routing (matches the EVM change already on
main): a returned receipt/instruction now routes to every matching registration rather than one. If both a wildcard handler and a contract-specific handler are registered for the same signature, both now fire for a matching item from that contract. The common case (no overlapping registrations) is unchanged.End-user impact
Testing
cargo test --libgreen (Fuel 21, SVM 21, EVM 30; 401 total). New unit tests cover grouping, scoping, longest-first routing, filter re-application, missing-field errors,isCommitteddrops, and log attachment.envio,test_codegen,fuel_test,svm_test(which enforceswarnings.error = "+a", so no unused bindings).SvmHyperSyncSource_test, the Fuel suite, and the addon-boundary suites (BlockStore_test/TransactionStore_test/EventFilters_test, which exercise the renamed napi classes) all pass. CI runs the full suite.#[napi]exports consumed from ReScript, i.e. live cross-boundary API — nothing to remove.🤖 Generated with Claude Code
https://claude.ai/code/session_01Myo6vsGNF9ACsAbBV6PXSk
Generated by Claude Code
Summary by CodeRabbit
New Features
Breaking Changes
Bug Fixes