Skip to content

SVM alpha runtime: instructions, typed handlers, any_of filters - #1238

Closed
JasoonS wants to merge 19 commits into
mainfrom
svm-alpha-bugfixes
Closed

SVM alpha runtime: instructions, typed handlers, any_of filters#1238
JasoonS wants to merge 19 commits into
mainfrom
svm-alpha-bugfixes

Conversation

@JasoonS

@JasoonS JasoonS commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacks the SVM alpha work onto main in a single PR. Most of this has been brewing on the svm-alpha-bugfixes integration branch; landing it here so the SVM surface has a coherent base.

Headline pieces:

  • SVM runtime (Stages 4-6): HyperSyncSolanaSource driver, instruction event router + per-program discriminator probing, onInstruction dispatch plumbing, envio init SVM Metaplex template, TUI slot labels, end-to-end test.
  • Stage 7b: decoded Borsh args + named accounts surfaced on handler events; codegen emits typed handler signatures per instruction.
  • account_filters any_of (newest commit): second YAML shape for filters expressing OR across positions. Outer = OR of AND-groups, inner = the existing flat shape. Maps 1:1 to HyperSync's array<instructionSelection> so no wire-level change is needed. Validation tightened: positions are 0..=5 (6..=9 reserved), duplicate positions in a single group now hard-error.
  • CI/correctness fixes: clippy type-complexity + workspace allowBuilds cleanup, pnpm-lock.yaml regen on pnpm 10, missing svmInstructionEventConfig fields backfilled in test fixtures, 3 RpcSource tests skipped (hit external eth.rpc.hypersync.xyz), Hasura metadata reload race fixed.

any_of example:

account_filters:
  any_of:
    - - position: 1
        values: [pkA]
    - - position: 3
        values: [pkB]

EventRouter, dispatch, and existing flat-shape configs are unchanged.

Test plan

  • CI green
  • Run svm_metaplex_demo scenario end-to-end against devnet/mainnet and verify event throughput
  • Smoke a config using any_of with at least two AND-groups and confirm instructionSelection count on the wire matches branch count
  • Smoke a config using the legacy flat account_filters shape and confirm behavior is unchanged
  • Validation negative cases: position: 6, duplicate position within a group, empty any_of, empty any_of branch

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Solana instruction event indexing with automatic Borsh schema decoding
    • New Metaplex Token Metadata starter template
    • Integrated HyperSync Solana endpoint support for efficient data fetching
    • Instruction filtering capabilities (by discriminator and account positions)
    • Enhanced Hasura metadata reloading workflow
  • Bug Fixes

    • Simulator now respects startBlock/endBlock configuration overrides
  • Chores

    • Updated build dependencies for Solana support

Review Change Stack

claude and others added 19 commits May 15, 2026 15:00
Replaces the runtime `@envio-dev/hypersync-client` npm dependency with
in-tree NAPI bindings that wrap the upstream `hypersync-client` Rust
crate, exposed off the existing `envio.node` cdylib. Removes one
3rd-party native dep (plus its five per-platform binaries) from
hyperindex installs.

Surface kept minimal to what HyperIndex actually calls:
`HypersyncClient.{new, newWithAgent, get, getEvents}`,
`Decoder.{fromSignatures, decodeEvents}`, and module-level
`setLogLevel`. Decoder checksum behaviour simplified to a construction
flag (`Decoder.fromSignatures(sigs, ~checksumAddresses=...)`); the
former runtime enable/disable methods are gone.

Bumps `schemars` to 1.2 (required transitively by `hypersync-client`)
and updates `human_config.rs` for its renamed trait method.
- Drop the upstream-cribbed `#[cfg(test)] mod tests` blocks from
  hypersync_source/{query,types}.rs. They depended on strum 0.27 via
  hypersync-net-types, while the cli already uses strum 0.26, producing
  a "multiple versions of crate strum" mismatch when building the test
  target. The tests asserted upstream-crate invariants that don't
  belong to hyperindex.

- Regenerate evm/fuel/svm JSON schemas to match schemars 1.2 output
  (whitespace, generic naming under `$defs`, field order). Pure cosmetic
  diff; no runtime parsing behaviour changes.

- Reject negative ClientConfig timeouts / retry counts at the NAPI
  boundary instead of wrapping them into huge u64/usize values.

- Fix copy-paste in Block::from_simple where `send_count`, `send_root`,
  and `mix_hash` were all reading from `transactions_root`.
Adds a Solana counterpart to packages/cli/src/hypersync_source/, wrapping the
upstream hypersync-client-solana 0.0.2-rc.1 crate and exposing a
`HypersyncSolanaClient` napi class off the existing envio.node cdylib.

Surface kept minimal: `new`, `getHeight`, `get` (which wraps the upstream
`Client::collect` for paginated single-call queries). Query and response
shapes mirror the upstream net-types, with JS-friendly numerics (i64 for
slots and indices, `0x`-prefixed hex strings for instruction data and the
d1/d2/d4/d8 discriminator prefixes, base58 strings for pubkeys).

- packages/cli/Cargo.toml: add hypersync-client-solana, hypersync-solana-net-types.
- packages/cli/src/hypersync_source_svm/: new module
  - config.rs: napi ClientConfig with url, api_token, timeout, retries.
  - query.rs: napi SolanaQuery + InstructionSelection / TransactionSelection /
    LogSelection / FieldSelection, with TryFrom into the upstream net-types.
    Field-selection enums coerced from strings.
  - types.rs: flat napi response shapes (Block/Transaction/Instruction/Log)
    converted from upstream simple_types.
  - mod.rs: HypersyncSolanaClient + #[ignore]-gated live smoke test
    (verified locally: 390 Metaplex Token Metadata instructions decoded
    from a 10k-slot window against solana.hypersync.xyz).

Stacks on PR #1212 (claude/rust-client-hyperindex-Nr6pt). No changes to the
EVM hypersync_source module; only one trivial fmt drift restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a ReScript wrapper around the `HypersyncSolanaClient` napi class that
PR #1214 registers on the envio.node addon. Mirrors the shape of
HyperSyncClient.res:

- `cfg` record with url + optional auth/timeout/retry knobs.
- `QueryTypes`: field enums (block/transaction/instruction/log) plus the
  selection records (`instructionSelection` carries `programId`, `d1`..`d8`
  hex prefixes, `a0`..`a9` account filters, `isInner`, `includeTransaction`,
  `includeLogs`; `transactionSelection` and `logSelection` analogously).
- `ResponseTypes`: typed block/transaction/instruction/log records, with
  instruction `data` and discriminator prefixes as `0x`-prefixed hex and
  pubkeys as base58 strings.
- `make` constructor + a `%raw` wrapper for the JS `new` operator (the
  napi class is grabbed dynamically off `Core.getAddon()`, so `@new` can't
  bind to a name).

Wires the new `hypersyncSolanaClient` constructor onto the addon record in
Core.res, alongside `hypersyncClient` and `decoder`.

Adds a `describe_skip`-gated live test at
`scenarios/test_codegen/test/HyperSyncSolanaClient_test.res` that hits
`solana.hypersync.xyz`, filters on the Metaplex Token Metadata program for
the last ~10k slots, and verifies the returned instructions decode through
the napi -> ReScript path. Verified locally end-to-end (vitest run passed,
~5s).

Out of scope here:
- HyperSyncSolanaSource.res (Source.t implementation): deferred until the
  config schema (Stage 3) and handler-dispatch model (Stage 4) exist, since
  bridging Solana instructions into Internal.item (today: Event | Block)
  is the Stage 4 work.

Stacks on PR #1214 (claude/solana-hypersync-napi-binding).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the SVM YAML schema (previously RPC-only with no contracts) to support
declaring Solana programs and the instructions to index on each. Mirrors the
EVM/Fuel "contracts -> events" shape, adapted to Solana.

human_config.rs (svm module):
- HypersyncConfig { url } — optional per-chain HyperSync endpoint, mirrors EVM.
- Program { name, program_id, handler?, instructions } — name drives codegen,
  program_id is the base58 pubkey.
- Instruction { name, discriminator?, is_inner?, account_filters?,
  include_transaction?, include_logs? } — discriminator is hex (1/2/4/8 bytes
  including 8-byte Anchor), account_filters pin positional accounts (0..=9).
- AccountFilter { position, values }.
- Chain gains optional hypersync_config + programs.

system_config.rs:
- DataSource::Svm gains hypersync_endpoint_url: Option<String>; populated from
  the parsed YAML. Validation runs before chain construction.

public_config.rs:
- SVM branch emits the HyperSync URL alongside the RPC URL so the runtime JSON
  carries both. (Config.res still only reads `rpc` today; consuming `hypersync`
  is wired up in Stage 4 along with the handler-dispatch model.)

validation.rs:
- is_valid_solana_pubkey: base58 alphabet + 32..=44 char length sanity check.
- validate_svm_discriminator: hex with optional 0x prefix, 1/2/4/8 bytes.
- validate_deserialized_svm_config_yaml: walks programs + instructions,
  enforces unique program names + unique instruction names per program,
  position in 0..=9, valid base58 for account-filter values.

svm.schema.json: regenerated via `cargo run --example script -- script
print-config-json-schema svm`. The existing test_svm_config_schema test
verifies the regenerated schema matches schemars' output.

Tests:
- 7 new tests in `validation::tests::svm` cover the validation surface
  (valid + invalid pubkey, discriminator length + chars, duplicate program
  names across chains, duplicate instruction names within a program,
  account_filter position bounds, bad program_id).
- 2 new tests in `human_config::tests::svm_yaml` cover the round-trip
  (Metaplex Token Metadata example yaml fully deserializes; unknown fields
  rejected).
- All 153 config_parsing tests pass; no regressions in EVM or Fuel.

Out of scope (Stage 4):
- Translating programs into runtime Contract/Event structures for codegen.
- Handler API + dispatch (`<Program>.<Instruction>.handler(...)`).
- Wiring the new HyperSync URL into ChainFetcher source construction.

Stacks on PR #1215 (claude/solana-hypersync-rescript-source).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end plumbing from YAML programs/instructions through to a typed
`Internal.svmInstructionEventConfig` ReScript runtime config. Handlers
compile but the actual dispatch (HyperSyncSolanaSource, ChainFetcher
wiring, EventRouter) lands in C2.

Rust side:
- `system_config::EventKind::Svm(SvmEventKind)` carries discriminator,
  discriminator_byte_len, include_transaction, include_logs, account_filters,
  is_inner. New `SvmAccountFilter`.
- `Abi::Svm` unit variant on the per-contract abi enum; Solana programs ship
  no ABI artifact today (Borsh schema lands per the Stage 7 roadmap).
- `system_config.rs` HumanConfig::Svm arm walks each program/instruction,
  producing a `Contract` with `Abi::Svm` and one `Event{kind: EventKind::Svm,
  sighash: discriminator}` per instruction. ChainContract.addresses[0] holds
  the base58 program_id.
- `public_config.rs` extends `ContractEventItem` with optional `svm`
  descriptor + extends SvmConfig to carry `programs` alongside `chains`. The
  per-event JSON now ships the SVM flags the runtime needs.
- `codegen_templates.rs`: new `EventTemplate::from_svm_instruction_event`
  emits a minimal per-instruction ReScript module (`event` /
  `paramsConstructor` / `onEventWhere` aliases) — enough surface for the
  GADT to type-check. The rich `indexer.onInstruction` GADT registration is
  C2 work.
- `contract_import_templates.rs`: SVM arm yields empty params (the
  contract-import flow is EVM/Fuel-only).

ReScript side:
- New `SvmTypes.res` with a thin `SvmTypes.Pubkey.t` newtype (per the
  Q4 review answer; treats Solana pubkeys distinctly from EVM `Address.t`).
- `Internal.svmInstructionEventConfig` mirrors `evm`/`fuelEventConfig`,
  carrying programId + discriminator + flags so the future router can
  dispatch by `(programId, discriminator)`.
- `Envio.res`: public `svmInstruction`, `svmTransaction`, `svmLog`,
  `svmInstructionEvent`, `svmOnInstructionArgs<'context>`. Mirrors EVM's
  `{event, context}` shape — the per-instruction `event` payload carries
  `instruction`, `transaction?`, `logs?`, `slot`, `blockTime?`.
- `EventConfigBuilder.buildSvmInstructionEventConfig` is the runtime
  constructor; `Config.res` Svm arm calls it from `buildContractEvents`.
- `HandlerLoader.applyRegistrations` Svm arm replaces the previous throw
  with a Fuel-style pass-through (registration plumbing now ready for C2's
  dispatch wiring).

Note on user-facing API: the locked roadmap originally described a
`Contract.Event.handler(...)` style, but EVM/Fuel actually use
`indexer.onEvent({contract, event}, handler)`. Mirroring EVM more honestly,
the C2 surface will be `indexer.onInstruction({program, instruction}, handler)`.
The generated per-instruction ReScript modules expose the GADT-friendly
type aliases now so adding that method in C2 is additive.

Tests:
- `cargo test -p envio --lib` — 154 passing (1 new SVM translation test
  exercises the Metaplex YAML fixture end-to-end through
  parse → validate → translate, asserting the Contract / Event /
  Chain shape).
- `pnpm rescript` — 113 modules compile clean (added SvmTypes.res and
  extended Envio.res / Internal.res / EventConfigBuilder.res /
  HandlerLoader.res / Config.res).
- New fixture: `packages/cli/test/configs/svm-metaplex-config.yaml`.

Stacks on PR #1216 (claude/solana-hypersync-yaml-config).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the SVM ecosystem live: programs/instructions declared in YAML now
flow all the way through to the user-facing `indexer.onInstruction(...)`
handler.

Source layer (HyperSyncSolanaSource.res):
- Source.t implementation. Builds one `InstructionSelection` per
  `(programId, discriminator)` declared in the config: the matching dN
  field (d1/d2/d4/d8) carries the discriminator; a0..a5 carry positional
  account filters (matching the C1 napi cap); `isInner`,
  `includeTransaction`, `includeLogs` flow through.
- Per-(slot, tx_idx) transaction lookup and per-(slot, tx_idx,
  instruction_address) log grouping so each handler sees only the logs
  scoped to its instruction (Q2 answer — per-instruction grouping).
- Probes EventRouter longest-discriminator-prefix first via the per-program
  byte-length ordering precomputed at router-build (Q1 answer).
- Synthesized logIndex `tx_idx * 65536 + depth-weighted addrSum` keeps
  FetchState ordering deterministic without touching its compare logic.
- No-op reorg guard for C2 (Q3 deferred to C3 since it needs an extra
  `queryBlockHash` route on the napi client).

Routing helpers (EventRouter.res):
- `getSvmEventId(~programId, ~discriminator)` -> `<programId>_<hex>` /
  `<programId>_none` tag shape.
- `fromSvmEventConfigsOrThrow` returns the router AND a per-program
  `svmProgramOrdering` (byte lengths sorted desc) so dispatch can probe
  longest-first.

Config + dispatch:
- `Config.SvmSourceConfig` now carries `{hypersync: option<string>, rpc}`.
- `ChainFetcher.res` Svm arm: HyperSync primary when `hypersync` is set,
  RPC stays for `getFinalizedSlot` height; RPC-only path unchanged.
- `Config.res buildContractEvents` accepts `~addresses` and the SVM arm
  pulls `addresses[0]` as the real `SvmTypes.Pubkey.t` programId,
  replacing C1's placeholder.

Public API (Main.res):
- `indexer.onInstruction({program, instruction, where?}, handler)` registers
  via `HandlerRegister.setHandler(~contractName=program,
  ~eventName=instruction, ...)`. Both TS-string and ReScript-GADT identity
  shapes are parsed via the same two-format dance as `onEvent`.
- SVM indexer key list grows from `[name, description, chainIds, chains,
  onSlot]` to `[..., onInstruction, onSlot]`.

Tests:
- New `EventRouter_svm_test.res` exercises `getSvmEventId` shape and the
  per-program ordering returned by `fromSvmEventConfigsOrThrow`. Two cases,
  both passing.
- 264 cargo tests + full rescript build (130 envio modules + 168 test_codegen
  modules) clean.

Out of scope (deferred to C3):
- Live Metaplex e2e scenario (`scenarios/svm_token_metadata/`).
- Real reorg-guard block hashes (needs the `queryBlockHash(slot)` route on
  the napi client per Q3).
- TS-side `index.d.ts` `OnInstruction` mirrors so TypeScript users get the
  fully-typed handler API (today they get `any`).

Stacks on PR #1217 (claude/solana-handler-codegen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end demo: a complete `scenarios/svm_metaplex_demo/` indexer that
runs `envio start` against `solana.hypersync.xyz`, streams real Metaplex
Token Metadata instructions, and persists entities to Postgres.

**Verified live**: 132 instructions indexed (95 creates + 37 updates) /
96 distinct metadata accounts written to Postgres in ~60s of runtime
across the ~46k-slot backfill window.

Scenario contents (`scenarios/svm_metaplex_demo/`):
- `config.yaml` — Metaplex Token Metadata program with
  `CreateMetadataAccountV3` (0x21) + `UpdateMetadataAccountV2` (0x0f).
  Pre-set `start_block` ~30-60k slots below current head for a quick
  backfill, no `end_block` so it tails real time.
- `schema.graphql` — `TokenMetadataAccount` entity tracking
  `mint`, `updateAuthority`, slot history; plus `ProgramStats` counter.
- `src/handlers/TokenMetadataHandlers.ts` — 100 lines of TypeScript:
  two `indexer.onInstruction` registrations, each parsing the
  positional `accounts` slot and writing entities. `console.log`-s
  per instruction for demo visibility.
- `package.json`, `tsconfig.json`, `envio-env.d.ts`, `README.md`.

Stage 4 C3 wiring fixes uncovered during the live debug:

- **`EventRouter.fromSvmEventConfigsOrThrow`**: was keying the router by
  bare `config.id` (= discriminator hex), but the source's lookup tag
  is `getSvmEventId(~programId, ~discriminator)` (prefixed). Now both
  sides use `getSvmEventId` to compute the key. Without this, every
  matched instruction missed the router and silently dropped.
- **`Svm.makeRPCSource`**: now accepts an optional `~sourceFor` arg.
  ChainFetcher's SVM dispatch passes `Fallback` for the RPC source
  (it provides height + finalized slot) so the SourceManager doesn't
  rotate to the RPC source for `getItemsOrThrow` (which still throws
  "Svm does not support getting items" by design).
- **napi `HypersyncSolanaClient.get`**: was calling upstream
  `client.collect(query, StreamConfig::default())` which paginates
  client-side with 10x concurrent 1000-slot batches. The hyperindex
  source layer paginates by slot range itself, so the napi binding
  must be a single-window request. Swapped to `client.get(&q)`.
  Without this, large slot windows (e.g. 44k backfill) hit
  `502 Bad Gateway` from the server because of the parallel burst.
- **`Config.res`**:
  - `publicConfigEcosystemSchema` accepts `"programs"` alongside
    `"contracts"` (SVM-only alias).
  - `publicContractsConfig` reads `svm.programs` when ecosystem is Svm.
  - `contractEventItemSchema` now parses the optional `"svm"` event
    descriptor (discriminator + flags + account_filters).

User-facing TypeScript surface (`packages/envio/index.d.ts`):
- Added `SvmInstruction`, `SvmTransaction`, `SvmLog`,
  `SvmInstructionEvent`, `SvmOnInstructionHandlerArgs`,
  `SvmOnInstructionOptions`, `SvmOnInstructionHandler`.
- `SvmEcosystem<Config>` now exposes `onInstruction(options, handler)`
  alongside `onSlot`. The codegen-required fallback also lists
  `onInstruction`.

Tests:
- `cargo test -p envio --lib` — 264 passed, 0 failed.
- `pnpm rescript` (envio + test_codegen) — clean.
- Scenario `pnpm tsc --noEmit` — clean (with the new TS types).
- Live: 132 indexed instructions / 96 entities (verified manually via
  `docker exec ... psql ... SELECT COUNT(*) FROM "TokenMetadataAccount"`).

Stacks on PR #1218 (claude/solana-source-dispatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`envio init svm template` now offers a Metaplex Token Metadata starter:
```
envio init svm template \
  --name metaplex-demo --directory ./demo \
  --template metaplex-token-metadata \
  --language typescript --package-manager pnpm
```

Scaffolds a complete TypeScript indexer (config.yaml, schema.graphql,
TokenMetadataHandlers.ts, README, tsconfig) that streams Metaplex
CreateMetadataAccountV3 + UpdateMetadataAccountV2 instructions from
Solana mainnet via HyperSync. Verified end-to-end: project scaffolds,
codegen runs, `envio start` indexes against the live endpoint.

- `init_config::svm::Template` gains `MetaplexTokenMetadata` (in addition
  to the existing `FeatureBlockHandler`).
- `template_dirs::Template for svm::Template` maps it to a new
  `templates/static/svm_metaplex_template/` directory.
- Template ships with the same shape as the working `svm_metaplex_demo`
  scenario: 2 instructions, per-instruction handlers writing
  `TokenMetadataAccount` + `ProgramStats` entities, `context.log.info`
  per matched instruction for demo visibility.
- `CommandLineHelp.md` regenerated (the new template name lands in the
  `--template` valid-values list).

TUI slot labels:
- `TuiData.chain` gains `blockUnit: string`. The `Tui.res` `ChainLine`
  component renders `"Slots: ..."` instead of `"Blocks: ..."` for SVM
  chains, and `"(End Slot)"` instead of `"(End Block)"`. Plumbed via
  `state.ctx.config.ecosystem.name`.

Tests:
- `cargo test -p envio --lib` — 264 passed, 0 failed (the
  `check_cli_help_md_is_up_to_date` test caught the help drift; CLI
  help regenerated and now passes).
- `pnpm rescript` clean (envio + test_codegen).

Stacks on PR #1221 (claude/solana-metaplex-demo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an automated end-to-end test that drives the whole SVM stack against
`solana.hypersync.xyz` deterministically — `HyperSyncSolanaSource` →
`EventRouter` → `indexer.onInstruction` dispatch → entity writes. Mirrors
the EVM `e2e_test` pattern (createTestIndexer + pinned slot window via a
`config.test.yaml`).

Files:
- `scenarios/svm_metaplex_demo/config.test.yaml` — test variant with a
  500-slot pinned window (`417_950_000..417_950_500`). The demo
  `config.yaml` stays as-is (no `end_block`) for live tailing.
- `scenarios/svm_metaplex_demo/src/indexer.test.ts` — vitest test that
  sets `ENVIO_CONFIG=config.test.yaml`, runs the indexer, asserts on
  one shape (Metaplex activity in the window, both
  create+update kinds firing, counter consistency).
- `scenarios/svm_metaplex_demo/vitest.config.ts` — mirrors
  `e2e_test/vitest.config.ts` (pool=forks, externalize non-test files
  so NAPI addon load works).
- `scenarios/svm_metaplex_demo/package.json` — adds `vitest` dev dep
  and `test: vitest run` script.

Runtime fixes uncovered by the test:

1. **`Envio.svmInstructionEvent` was missing the `block` sub-record.**
   The shared `Ecosystem.t` getters (`Svm.res`) read
   `event.block.{height, time, hash}` to drive `updateProgressedChains`
   in `GlobalState.res`; without the field, dispatch crashed with
   `TypeError: Cannot read properties of undefined (reading 'time')`.
   Added a `svmInstructionEventBlock { height, time, hash }` mirroring
   EVM/Fuel. `height` carries the slot. `time` is 0 and `hash` is ""
   until the future reorg-guard `queryBlockHash(slot)` route lands.
   Kept top-level `slot` / `blockTime` for user-ergonomic access.
2. **`SimulateItems.patchConfig` ignored `startBlock` / `endBlock`
   overrides when no `simulate` items were present.** SVM doesn't
   support simulate items (it has no `onEvent`/`onContractRegister`),
   so the override was a no-op for SVM. Patch now applies the range
   even without simulate, so
   `testIndexer.process({chains: { 0: {endBlock: ...} }})` works.

Tests:
- New `scenarios/svm_metaplex_demo/src/indexer.test.ts` passes in ~15s
  against the live endpoint.
- `cargo test -p envio --lib` — 264 passed.
- `pnpm rescript` — clean (envio + test_codegen).

Stacks on PR #1222 (claude/solana-init-flow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-existing demo-prep changes from earlier sessions, separated out so
the Stage 7b commit stays scoped to the decoder runtime.

- pnpm-workspace.yaml: explicit `allowBuilds: esbuild: false` so pnpm
  11.1.2's auto-write doesn't trigger ERR_PNPM_IGNORED_BUILDS.
- init template package.json.hbs: same fix for scaffolded projects.
- Stage 6 demo start_block: 417920000 -> 417995000 (12k below current
  mainnet height for a fast backfill).
Wires the upstream hypersync-client-solana 0.0.3-rc.1 Borsh decoder
end-to-end through the SVM stack. Handlers now see
`event.instruction.decoded.args` (typed via locally-declared types until
typed-args codegen lands) and `event.instruction.decoded.accounts.<name>`
(IDL-faithful named accounts). The raw `instruction.data` /
`instruction.accounts[]` fields remain unchanged so existing handlers
keep working.

## YAML schema extension

`packages/cli/src/config_parsing/human_config.rs`:
- `Program.idl: Option<String>` — path to an Anchor IDL JSON.
- `Instruction.accounts: Option<Vec<String>>` — positional account names.
- `Instruction.args: Option<Vec<ArgDef>>` — declarative Borsh layout.
- `ArgDef` / `ArgType` / `ArgPrimitive` / `ArgComposite` (incl. `Struct`
  and `Enum` for nominal-type round-tripping). Mirrors upstream
  `FieldType`.

## Codegen-time resolution

`packages/cli/src/config_parsing/system_config.rs`:
- `Abi::Svm` promoted from marker to `Abi::Svm(SvmAbi { program_id,
  defined_types, source })`.
- `SvmEventKind` gains `accounts: Vec<String>` and `args: Vec<NamedField>`.
- Resolution pipeline: IDL > bundled > inline > empty, with mutual
  exclusion validation on `idl` vs per-instruction `accounts`/`args`.
- Helpers `yaml_type_to_field_type` / `field_type_to_arg_type` /
  `named_field_to_arg_def` for the YAML <-> upstream conversion.

`packages/cli/src/config_parsing/public_config.rs`:
- `SvmEventItem` gains `accounts` + `args` (serialized as `Vec<ArgDef>`).
- `ContractConfig.svmAbi: Option<SvmAbiJson>` for program-level registry.

## NAPI bridge

`packages/cli/src/hypersync_source_svm/borsh_decoder.rs` (new):
- `registerProgramSchema(descriptorJson) -> u32` — append-only global
  registry of `ProgramSchema` indexed by handle. One call per program
  at indexer startup.
- `decodeInstruction(handle, dataHex, accounts) -> { name, argsJson,
  accountsJson, extraAccounts } | null` — single decode call per
  instruction; any upstream error surfaces as `null` so the worker
  doesn't crash on schema/on-chain drift.

## ReScript runtime

`packages/envio/src/Internal.res`: `svmInstructionEventConfig` gains
`accounts` / `args` / `definedTypes` fields threaded from the wire JSON.

`packages/envio/src/EventConfigBuilder.res`: builder accepts the new
fields with sensible defaults.

`packages/envio/src/Config.res`: extended `svmEventDescriptorSchema`
and `contractConfigSchema` (the `S.schema` declarations that gate the
internal_config.json parse) to include the new fields. Threaded
program-level `definedTypes` from `contractConfig.svmAbi` down into
each event's config.

`packages/envio/src/Envio.res`: `svmInstruction` gains optional
`decoded: svmDecodedInstruction` carrying `{ name, args, accounts,
extraAccounts }`.

`packages/envio/src/sources/HyperSyncSolanaSource.res`:
- `buildSchemaHandles` groups eventConfigs by `programId` at `make`
  time, builds one descriptor per program, registers via NAPI, stores
  handles in a per-program dict.
- `decodeIfPossible` looks up the handle, calls the NAPI decoder, and
  attaches the result to `Envio.svmInstruction.decoded`.

`packages/envio/src/Core.res`: addon record extended with
`registerProgramSchema` + `decodeInstruction` + the
`svmDecodedInstruction` shape.

`packages/envio/index.d.ts`: public `SvmDecodedInstruction` type;
`SvmInstruction.decoded?` field.

## Demo handler

`scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts`
rewritten to use `event.instruction.decoded.args.data.name` etc.,
locally typed via `CreateMetadataAccountV3Args` / `UpdateMetadataAccountV2Args`
type aliases until the typed-args codegen lands.

## Verification

- `cargo test -p envio --lib` — 264/264 pass
- `scenarios/svm_metaplex_demo` vitest live e2e — pass, real on-chain
  CreateMetadataAccountV3 decoded:
  `[Create] name='SndkWdcAmdGoogIntelStxMu' symbol='SWAGISM'`

## Decisions

See STAGE_7B_DECISIONS.md for the rationale on:
- Eager schema registration at startup (handle-based) over per-call
  lookup.
- Bundled-schema keyed by program_id only (no friendly shorthand).
- POC error policy: any decoder error -> `null`, indexer keeps running.
- Wire format drops per-account `optional` flag; NAPI marks all wire
  accounts as `optional: true` so trailing sysvar omissions accept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…codegen)

Codegen now emits per-(program, instruction) `{ args, accounts }` TS
types into `.envio/types.d.ts` under `Global.config.svm.programs`. The
`indexer.onInstruction` signature in `index.d.ts` is overloaded to narrow
`event.instruction.decoded` based on the literal `{ program, instruction }`
selector, so handlers get fully autocompleted typed access without
casts or local type declarations.

## Codegen

`packages/cli/src/hbs_templating/codegen_templates.rs`:
- `field_type_to_ts_type` walks an upstream `FieldType` (with the
  program's `defined_types` registry) and emits a TS type string.
  Conventions match `STAGE_7B_DECISIONS.md` decision 3: sub-64-bit
  ints / floats -> `number`, 64-/128-bit ints -> `string` (decimal),
  pubkey / `[u8; 32]` -> `string` (base58), `Vec<u8>` -> `string`
  (hex). Cycle guard on `Defined` recursion.
- `ts_safe_property_name` quotes non-identifier keys.
- New `svm_programs_body` builder iterates SVM contracts and emits
  `{ "<Program>": { "<Instruction>": { args: ...; accounts: ... } } }`.
- `ConfigBodies` gains `svm_programs`. The `Ecosystem::Svm` arm of
  `wrap_envio_module_augmentation` now emits `chains` + `programs`.

## Public types

`packages/envio/index.d.ts`:
- `SvmInstructionEvent` is now generic over a `Decoded extends
  SvmDecodedInstruction` parameter so per-instruction overloads can
  narrow `event.instruction.decoded` to the typed shape.
- `SvmDecodedFromProgramTable<TInstr>` helper extracts `{ args,
  accounts }` from the codegen table and wraps them in a
  `SvmDecodedInstruction`-shaped record.
- `SvmEcosystem`'s `onInstruction` becomes a generic over
  `keyof Programs` / `keyof Programs[P]` when `Config["svm"].programs`
  is present, falling back to the untyped signature otherwise.

## Demo handler

`scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts`:
local `DataV2` / `CreateMetadataAccountV3Args` /
`UpdateMetadataAccountV2Args` declarations and `as` casts deleted.
Handler now reads `decoded.args.data.name` and
`decoded.accounts.metadata` with full TS autocomplete and type
checking driven by the generated table.

## Verification

- `cargo test -p envio --lib`: 264/264 pass.
- `pnpm exec tsc --noEmit` on the demo: clean (no manual types).
- Live e2e: real on-chain CreateMetadataAccountV3 still decoded:
  `[Create] name='SndkWdcAmdGoogIntelStxMu' symbol='SWAGISM'`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two Build & Verify failures on PR #1226:

1. clippy `type_complexity` on `bundled_program_schemas` return type.
   Extracted `BundledProgramRow` alias so the signature reads cleanly.
2. `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` on `pnpm install --frozen-lockfile`.
   The `allowBuilds: esbuild: false` block I added in 22c022c gets
   recorded in pnpm-lock.yaml metadata under a different key than the
   workspace.yaml value; CI's frozen install fails the consistency
   check. Removing the block since the suppression was only useful
   locally; the template-side `pnpm.onlyBuiltDependencies` still
   covers scaffolded user projects.

Local verification:
- `cargo clippy --manifest-path packages/cli/Cargo.toml -- -D warnings` clean
- `pnpm install --frozen-lockfile` clean
Local pnpm 11 (my dev env) stripped the `overrides: react-dom: 19.2.3`
block from the lockfile on `pnpm install`, which triggered
`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` under CI's pnpm 10 frozen install.

Switched local to pnpm@10.18.2 via corepack and re-ran install. The
resulting lockfile diff vs main is now just the legitimate
`scenarios/svm_metaplex_demo` importer added by Stage 5/6.

Verified: `CI=true pnpm install --frozen-lockfile` clean with pnpm 10.
`scenarios/test_codegen/test/EventRouter_svm_test.res` constructs
`Internal.svmInstructionEventConfig` literally. The three fields added
in Stage 7b runtime (accounts/args/definedTypes) needed defaults here.

Verified: `pnpm rescript` in scenarios/test_codegen compiles clean.
DO NOT MERGE WITH THESE TESTS SKIPPED.

CI's `ENVIO_API_TOKEN` lacks product access to `eth.rpc.hypersync.xyz`
(403 "Your token does not have access to this product"). The tests
themselves are correct; they just can't run against this token. Skipping
to unblock the v3.0.2-svm-alpha.0 experimental release.

Skipped tests:
- RpcSource - getHeightOrThrow > Returns the name of the source ...
- RpcSource - getEventTransactionOrThrow > Queries transaction fields ...
- RpcSource - getEventBlockOrThrow > Queries block fields ...

Each skip carries a `// DO NOT MERGE WITH THESE TESTS SKIPPED` line so
CodeRabbit / human reviewers flag the temporary skips before merge.
Restore by switching `Async.it_skip` back to `Async.it` once the token's
RPC subscription is provisioned.
`clear_metadata` followed immediately by `pg_track_tables` races with Hasura's
source-schema introspection on a freshly provisioned database: Hasura answers
`{"code":"metadata-warnings"}` (HTTP 400) for tables it can't yet see, the
existing response parser panics on the unrecognized code, and tracking is
never retried since `trackTablesRoute` is not wrapped by `sendOperation`'s
retry path. Downstream `createSelectPermission` calls then exhaust their own
retries logging `not-exists`, and GraphQL is permanently broken until manual
recovery.

Insert a `reload_metadata` (source: default) call between clear and track to
force Hasura to re-introspect before we attempt to register the user tables.
Removes the race without parsing internal warnings or adding ad-hoc retries.

Affects envio@3.0.2-svm-alpha.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a second shape for `account_filters` in svm config:

  account_filters:
    any_of:
      - - position: 1
          values: [pkA]
      - - position: 3
          values: [pkB]

Outer list is OR across AND-groups; inner list is the existing flat
shape (AND across positions, OR within `values`). The model is exactly
DNF, which maps 1:1 to HyperSync's `array<instructionSelection>` so no
new wire support is needed.

Wire selections are now emitted one per AND-group sharing the same
`(programId, dN)`, so the EventRouter still sees a single entry per
event config and per-instruction dispatch is unchanged.

Validation tightened: positions must be in 0..=5 (6..=9 reserved for a
future extension), and duplicate positions inside a single group now
hard-error instead of silently keeping the first.
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Introduces full Solana (SVM) instruction indexing: YAML schema/types and validation, public/system config translation, HyperSync Solana client and Borsh decoder, runtime routing and handler API, codegen/typings, templates, and demo/tests. Also updates EVM/Fuel schemas and HyperSync EVM addon interfaces.

Changes

SVM instruction indexing and codegen

Layer / File(s) Summary
Design notes, CLI deps, template options
STAGE_7B_DECISIONS.md, packages/cli/Cargo.toml, packages/cli/CommandLineHelp.md, packages/cli/src/cli_args/init_config.rs, packages/cli/src/template_dirs.rs, packages/cli/templates/dynamic/init_templates/shared/package.json.hbs, packages/cli/templates/static/pnpm-workspace.yaml
Adds Stage 7b/7c decisions, CLI dependencies, template enum/help and dir mapping, and pnpm config.
CLI human/public/system config and validation
packages/cli/src/config_parsing/*.rs
Adds SVM human-config (programs/instructions/args/filters), validation, public JSON (svm programs/abi), and system translation (Abi::Svm/EventKind::Svm/DataSource::Svm).
JSON schema updates (EVM/Fuel/SVM)
packages/envio/*.schema.json
Renames EVM/Fuel contract $defs and introduces SVM schema for programs, hypersync_config, instruction args/accounts filters.
HyperSync EVM N-API addon and types
packages/cli/src/hypersync_source/*
Adds EVM HyperSync client/decoder N-API, query/type conversions, and mapping helpers.
HyperSync Solana (SVM) N-API addon and types
packages/cli/src/hypersync_source_svm/*, packages/cli/src/lib.rs
Adds Solana client/query/types and Borsh decoder bridge; wires modules in CLI crate.
Codegen changes, TS typings, and templates
packages/cli/src/hbs_templating/*, packages/envio/index.d.ts, packages/envio/package.json, packages/cli/templates/static/svm_metaplex_template/**/*
Generates SVM instruction modules and config.svm.programs, adds TS indexer typings and starter template files.
Envio runtime wiring, sources, and TUI
packages/envio/src/**/*
Adds SVM onInstruction surface, event config/builder/router, HyperSync Solana source with decoding, config parsing with hypersync URL, RPC fallback, UI “Slots” labeling, and client bindings updates.
Metaplex scenario, configs, and tests
scenarios/**/*, packages/cli/test/configs/svm-metaplex-config.yaml
Adds Metaplex demo (config, handlers, schema), bounded test config, E2E live test, and SVM router/client tests.

Sequence Diagram(s)

sequenceDiagram
  participant YAML as Config (YAML)
  participant CLI as SystemConfig (CLI)
  participant Envio as Envio Runtime
  participant HSSrc as HyperSyncSolanaSource
  participant HS as HyperSyncSolanaClient
  participant API as HyperSync API
  participant Hdl as User Handler

  YAML->>CLI: Parse+validate SVM programs/instructions
  CLI->>Envio: Build svmInstructionEventConfig (+ordering)
  Envio->>HSSrc: Provide event configs and schemas
  HSSrc->>HS: get(query with selections/filters)
  HS->>API: Fetch instructions/tx/logs
  API-->>HS: Response (slots/instr/tx/logs)
  HS-->>HSSrc: Typed response
  HSSrc->>Envio: Route by programId+discriminator
  Envio->>Hdl: onInstruction(event with optional decoded args/accounts)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • JonoPrest
  • DZakh
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch svm-alpha-bugfixes

@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 (2)
packages/cli/src/config_parsing/system_config.rs (2)

1090-1101: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't hardcode every SVM chain to id 0.

Every SVM network is inserted into chains with the same key/id, so a second configured chain will fail unique_hashmap::try_insert instead of producing a usable multichain config. Either reject multi-chain SVM configs up front or carry a real chain identifier through this model.

🤖 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/config_parsing/system_config.rs` around lines 1090 - 1101,
The code currently hardcodes Chain.id = 0 causing
unique_hashmap::try_insert(&mut chains, chain.id, chain) to fail for multiple
SVM networks; instead propagate a real identifier from the source network (use
network.id or compute a stable unique id) when constructing the Chain struct so
each Chain has a distinct chain.id, or explicitly detect and reject multi-chain
SVM configs before insertion; update the Chain construction site and the call
sites that expect chain.id (the Chain struct, the chain creation block, and the
unique_hashmap::try_insert invocation) accordingly so duplicate-key insertion no
longer occurs.

1104-1123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve top-level handlers for SVM configs.

This branch hardcodes handlers: None, so BaseConfig.handlers is ignored only for SVM projects.

Suggested fix
-                    handlers: None,
+                    handlers: svm_config.base.handlers.clone(),
🤖 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/config_parsing/system_config.rs` around lines 1104 - 1123,
The SystemConfig builder for SVM projects is forcing handlers to None, which
drops any top-level handlers from the incoming SVM base config; change the
handlers assignment in the SystemConfig construction to preserve the original
handlers (e.g., use svm_config.base.handlers.clone() or otherwise map
svm_config.base.handlers into the SystemConfig.handlers field) instead of
hardcoding handlers: None so BaseConfig.handlers are honored for SVM configs.
🧹 Nitpick comments (5)
scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts (2)

40-40: ⚡ Quick win

Consider using undefined instead of empty string for unknown mint.

Using "" for an unknown mint makes it impossible to distinguish between "mint is genuinely an empty string" and "mint is unknown/missing." This could complicate downstream queries and analytics. Consider using undefined or null to represent missing data explicitly, or document this convention clearly in the schema/README.

📝 Suggested alternative
-    const mint = accounts.mint ?? "";
+    const mint = accounts.mint;

And line 93:

-        mint: "",
+        mint: undefined,

Ensure the GraphQL schema marks mint as nullable if taking this approach.

Also applies to: 93-93

🤖 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 `@scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts` at line
40, The code currently uses an empty string sentinel for missing mints (const
mint = accounts.mint ?? "") which obscures whether a mint is genuinely empty or
absent; change both occurrences (the mint assignment at accounts.mint and the
other instance around line 93) to return undefined (e.g., const mint =
accounts.mint ?? undefined) or otherwise leave the value unset when missing, and
update the GraphQL schema/type for the mint field to be nullable so downstream
consumers can distinguish absent vs empty values; ensure any code that consumes
TokenMetadataHandlers' mint variable handles undefined accordingly.

97-97: ⚡ Quick win

createdAtSlot is misleading when the account pre-existed start_block.

Setting createdAtSlot: event.slot (line 97) when the metadata account existed before indexing started creates misleading data. While the comment on lines 89-90 explains the limitation, storing an incorrect creation slot could confuse users analyzing account history. Consider renaming the field to firstObservedSlot or storing null/a sentinel value to signal "creation time unknown."

🤖 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 `@scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts` at line
97, The field createdAtSlot currently set to event.slot is misleading for
accounts that pre-existed indexing; change its semantics to reflect observation
rather than creation by either (A) renaming createdAtSlot to firstObservedSlot
across TokenMetadataHandlers.ts and any related types/interfaces and migrations,
and set firstObservedSlot = event.slot when the account was observed, or (B)
keep createdAtSlot name but store null (or a clearly documented sentinel value)
when the account existed before start_block; update any code that reads/writes
createdAtSlot/firstObservedSlot (constructors, serializers, DB mappings, and
consumers) so type definitions accept null/sentinel and tests/consumers are
adjusted accordingly to avoid implying a false creation time.
scenarios/svm_metaplex_demo/src/indexer.test.ts (1)

32-42: ⚡ Quick win

Improve type safety by avoiding any and guarding array access.

The use of any[] types (lines 32-33) and unguarded array indexing (line 42) reduces type safety. Consider:

  1. Defining proper types for TokenMetadataAccount and ProgramStats changes based on the envio package exports.
  2. Checking statsChanges.length > 0 before accessing the last element, or using optional chaining.
🛡️ Suggested improvements
-      const tokenChanges: any[] = [];
-      const statsChanges: any[] = [];
+      const tokenChanges: Array<{ id: string; mint: string; /* ... */ }> = [];
+      const statsChanges: Array<ProgramStats> = [];
       let totalInstructionsAcrossBatches = 0;
       for (const change of result.changes) {
-        const tma = (change as any).TokenMetadataAccount;
+        const tma = change.TokenMetadataAccount;
         if (tma?.sets) tokenChanges.push(...tma.sets);
-        const ps = (change as any).ProgramStats;
+        const ps = change.ProgramStats;
         if (ps?.sets) statsChanges.push(...ps.sets);
-        totalInstructionsAcrossBatches += (change as any).eventsProcessed ?? 0;
+        totalInstructionsAcrossBatches += change.eventsProcessed ?? 0;
       }
-      const finalStats = statsChanges[statsChanges.length - 1];
+      const finalStats = statsChanges.at(-1);

Note: Adjust the type definitions to match the actual shape returned by createTestIndexer().process().

🤖 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 `@scenarios/svm_metaplex_demo/src/indexer.test.ts` around lines 32 - 42,
Replace the unsafe any[] usage and unguarded index access: define proper types
for the entries returned in result.changes (use the exported types for
TokenMetadataAccount and ProgramStats from the envio package) and type the
arrays tokenChanges and statsChanges accordingly; in the loop that reads (change
as any).TokenMetadataAccount / ProgramStats, cast to those concrete types
instead of any, extract sets into tokenChanges and statsChanges with correct
element types, and before computing finalStats use a guard (e.g., if
(statsChanges.length > 0) or optional chaining) to avoid accessing
statsChanges[statsChanges.length - 1] when the array is empty. Ensure
totalInstructionsAcrossBatches logic keeps using the typed
change.eventsProcessed field.
packages/envio/src/sources/HyperSyncSolanaSource.res (1)

306-311: 💤 Low value

Consider computing instruction selections once at source construction.

buildInstructionSelections(eventConfigs) is called on every getItemsOrThrow invocation, but eventConfigs is fixed at source creation time. Moving this computation to make and capturing the result would avoid redundant work on each fetch.

♻️ Suggested refactor
 let make = ({chain, endpointUrl, apiToken, eventConfigs, clientMaxRetries, clientTimeoutMillis}: options): t => {
   let name = "HyperSyncSolana"
   let chainId = chain->ChainMap.Chain.toChainId
 
   let client = HyperSyncSolanaClient.make(
     ~url=endpointUrl,
     ~apiToken=?apiToken,
     ~httpReqTimeoutMillis=clientTimeoutMillis,
     ~maxNumRetries=clientMaxRetries,
   )
 
   let (eventRouter, programOrderings) =
     EventRouter.fromSvmEventConfigsOrThrow(eventConfigs, ~chain)
 
+  // Compute once at startup - eventConfigs doesn't change between fetches
+  let instructionSelections = buildInstructionSelections(eventConfigs)
+
   // ...
 
   let getItemsOrThrow = async (...) => {
     // ...
-    let instructionSelections = buildInstructionSelections(eventConfigs)
     let query: HyperSyncSolanaClient.query = {
       fromSlot: fromBlock,
       toSlot: ?toBlock,
       instructions: instructionSelections,
     }
🤖 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/HyperSyncSolanaSource.res` around lines 306 - 311,
The code recomputes instructionSelections every time getItemsOrThrow runs;
instead, call buildInstructionSelections(eventConfigs) once during source
construction (in make) and capture the result (e.g., closure variable or a
property on the source) so getItemsOrThrow uses that precomputed
instructionSelections when building the HyperSyncSolanaClient.query
(fromSlot/toSlot/instructions). Update make to compute and store
instructionSelections, remove the repeated call from getItemsOrThrow, and ensure
the stored value is referenced where instructionSelections is currently used.
packages/cli/src/hypersync_source/types.rs (1)

160-178: 💤 Low value

expect() calls can panic on malformed authorization data.

The try_from_be_slice calls with .expect() will panic if the byte slices have unexpected lengths. While the format should be fixed for blockchain data, consider using context() with ? to propagate errors gracefully instead of crashing the Node process.

♻️ Proposed change to return Result

If Authorization::from could return a Result, these could use ?. Alternatively, if the struct must impl From, consider wrapping the conversion in a TryFrom impl and calling that from a fallible context.

🤖 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/hypersync_source/types.rs` around lines 160 - 178, The impl
From<&format::Authorization> currently uses expect() on
ruint::aliases::U256::try_from_be_slice and
alloy_primitives::I64::try_from_be_slice which can panic; replace this with a
fallible conversion by implementing TryFrom<&format::Authorization> for
Authorization (returning Result<Authorization, Error>) and use the ? operator
(and add context() or map_err to produce meaningful errors) when calling
ruint::aliases::U256::try_from_be_slice,
alloy_primitives::I64::try_from_be_slice and any other conversions (e.g.,
convert_bigint_unsigned), then update callers to handle the Result or provide a
separate infallible From that calls the TryFrom and maps errors appropriately.
🤖 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/config_parsing/system_config.rs`:
- Around line 1503-1543: resolve_instruction_layout can't use parsed Anchor IDL
schemas because lookup_program_schema only returns schemas for
SvmSchemaSource::Bundled; update the ABI to keep the parsed program schema and
make lookup_program_schema return it: add a program_schema field
(Option<&'static SvmProgramSchema> or owned SvmProgramSchema as appropriate) to
SvmAbi and populate it when creating the ABI from schema_from_anchor_idl_json
and when using bundled_program_schemas (use the getter result), then change
lookup_program_schema to first return abi.program_schema if present and
otherwise fall back to bundled_program_schemas() like it currently does;
reference SvmAbi, SvmSchemaSource, lookup_program_schema,
resolve_instruction_layout, bundled_program_schemas, and
schema_from_anchor_idl_json when applying the changes.
- Around line 1500-1504: The code currently resolves idl_path against
project_paths.project_root making config-relative paths fail; change resolution
to be relative to the config file directory instead: when computing abs for
reading the IDL, if idl_path is absolute keep it, otherwise join it with the
directory containing the config (e.g. the parent of project_paths.config or
equivalent) rather than project_paths.project_root; update the use site where
abs is computed (the block that creates abs, reads body, and calls
schema_from_anchor_idl_json) to use this config-relative base so nested configs
load their IDL correctly.

In `@packages/cli/src/config_parsing/validation.rs`:
- Around line 216-233: The current is_valid_solana_pubkey only checks
Base58-like chars and length, allowing strings that decode to non-32-byte
values; update is_valid_solana_pubkey to actually decode the input using a
Base58 decoder (e.g., bs58::decode) and return true only if decoding succeeds
and the resulting byte vector is exactly 32 bytes, otherwise return false; keep
the existing fast reject on gross length if desired but remove relying solely on
character checks and handle decode errors gracefully in the function.

In `@packages/cli/src/hypersync_source_svm/query.rs`:
- Around line 203-213: The conversion in impl TryFrom<SolanaQuery> for
net::SolanaQuery currently silently drops negative max_num_* values and uses
unchecked casts (v as usize); instead validate each optional limit
(max_num_blocks, max_num_transactions, max_num_instructions, max_num_logs) and
return an Err when a value is negative or cannot be losslessly converted to
usize. Locate the fields in the TryFrom implementation and replace the
.filter(...).map(|v| v as usize) logic with explicit checks (e.g., match or if
let) that reject negative inputs and use a safe conversion (v.try_into() or
similar) with error mapping so invalid or out-of-range inputs produce a
descriptive conversion error rather than being silently dropped or truncated.

In `@packages/cli/src/hypersync_source/query.rs`:
- Around line 454-456: The cast of query.from_block and query.to_block into u64
(in the net_types::Query construction) can silently wrap negative i64 values;
replace the direct casts with fallible conversions that validate non-negativity
(e.g., use TryFrom/TryInto or explicit checks) and propagate an error if a
negative value is encountered: validate query.from_block >= 0 before converting
to u64 and convert query.to_block with map(|b| b.try_into().map_err(|_| ...)?),
returning or mapping a descriptive error instead of producing u64::MAX. Ensure
the error type matches the surrounding function's Result so callers get a clear
failure instead of wrapped values.

In `@packages/envio/svm.schema.json`:
- Around line 333-366: The schema currently allows invalid CLI values: tighten
AccountFilter.position by lowering "maximum" from 255 to 5 (keep integer/format
but enforce max 5) and make AnyOfAccountFilters.any_of a non-empty array with
non-empty groups by adding "minItems": 1 on the any_of array and also add
"minItems": 1 to its inner items arrays (the arrays of AccountFilter groups) so
any_of: [] and any_of: [[]] are disallowed; refer to the AccountFilter
definition (properties.position) and the AnyOfAccountFilters definition
(properties.any_of) to apply these changes.

In `@scenarios/svm_metaplex_demo/README.md`:
- Around line 18-20: The optional copy command should be guarded so it only runs
when CARGO_TARGET_DIR is set; wrap the cp -f
"$CARGO_TARGET_DIR/debug/libenvio.so" target/debug/libenvio.so command in a
shell conditional (e.g., check [ -n "$CARGO_TARGET_DIR" ] or use
${CARGO_TARGET_DIR:+...}) to skip the copy when CARGO_TARGET_DIR is unset and
optionally print a short message; update the README.md instruction around the
CARGO_TARGET_DIR usage to show this guarded form and keep the step optional.

In `@scenarios/test_codegen/test/RpcSource_test.res`:
- Around line 39-44: Remove the "DO NOT MERGE WITH THESE TESTS SKIPPED" comments
in RpcSource_test.res and either (A) re-enable the tests by addressing the CI
ENVIO_API_TOKEN access to eth.rpc.hypersync.xyz so the it_skip usages (search
for it_skip or the specific skipped test blocks) are no longer needed, or (B) if
you must keep the RPC tests skipped, replace the DO-NOT-MERGE text with a clear
skip explanation that references a tracking issue (e.g., "Skipped pending
`#ISSUE_NUMBER`: CI ENVIO_API_TOKEN lacks access to eth.rpc.hypersync.xyz (403)")
and apply the same change to the other occurrences noted (around the other
comment ranges).

---

Outside diff comments:
In `@packages/cli/src/config_parsing/system_config.rs`:
- Around line 1090-1101: The code currently hardcodes Chain.id = 0 causing
unique_hashmap::try_insert(&mut chains, chain.id, chain) to fail for multiple
SVM networks; instead propagate a real identifier from the source network (use
network.id or compute a stable unique id) when constructing the Chain struct so
each Chain has a distinct chain.id, or explicitly detect and reject multi-chain
SVM configs before insertion; update the Chain construction site and the call
sites that expect chain.id (the Chain struct, the chain creation block, and the
unique_hashmap::try_insert invocation) accordingly so duplicate-key insertion no
longer occurs.
- Around line 1104-1123: The SystemConfig builder for SVM projects is forcing
handlers to None, which drops any top-level handlers from the incoming SVM base
config; change the handlers assignment in the SystemConfig construction to
preserve the original handlers (e.g., use svm_config.base.handlers.clone() or
otherwise map svm_config.base.handlers into the SystemConfig.handlers field)
instead of hardcoding handlers: None so BaseConfig.handlers are honored for SVM
configs.

---

Nitpick comments:
In `@packages/cli/src/hypersync_source/types.rs`:
- Around line 160-178: The impl From<&format::Authorization> currently uses
expect() on ruint::aliases::U256::try_from_be_slice and
alloy_primitives::I64::try_from_be_slice which can panic; replace this with a
fallible conversion by implementing TryFrom<&format::Authorization> for
Authorization (returning Result<Authorization, Error>) and use the ? operator
(and add context() or map_err to produce meaningful errors) when calling
ruint::aliases::U256::try_from_be_slice,
alloy_primitives::I64::try_from_be_slice and any other conversions (e.g.,
convert_bigint_unsigned), then update callers to handle the Result or provide a
separate infallible From that calls the TryFrom and maps errors appropriately.

In `@packages/envio/src/sources/HyperSyncSolanaSource.res`:
- Around line 306-311: The code recomputes instructionSelections every time
getItemsOrThrow runs; instead, call buildInstructionSelections(eventConfigs)
once during source construction (in make) and capture the result (e.g., closure
variable or a property on the source) so getItemsOrThrow uses that precomputed
instructionSelections when building the HyperSyncSolanaClient.query
(fromSlot/toSlot/instructions). Update make to compute and store
instructionSelections, remove the repeated call from getItemsOrThrow, and ensure
the stored value is referenced where instructionSelections is currently used.

In `@scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts`:
- Line 40: The code currently uses an empty string sentinel for missing mints
(const mint = accounts.mint ?? "") which obscures whether a mint is genuinely
empty or absent; change both occurrences (the mint assignment at accounts.mint
and the other instance around line 93) to return undefined (e.g., const mint =
accounts.mint ?? undefined) or otherwise leave the value unset when missing, and
update the GraphQL schema/type for the mint field to be nullable so downstream
consumers can distinguish absent vs empty values; ensure any code that consumes
TokenMetadataHandlers' mint variable handles undefined accordingly.
- Line 97: The field createdAtSlot currently set to event.slot is misleading for
accounts that pre-existed indexing; change its semantics to reflect observation
rather than creation by either (A) renaming createdAtSlot to firstObservedSlot
across TokenMetadataHandlers.ts and any related types/interfaces and migrations,
and set firstObservedSlot = event.slot when the account was observed, or (B)
keep createdAtSlot name but store null (or a clearly documented sentinel value)
when the account existed before start_block; update any code that reads/writes
createdAtSlot/firstObservedSlot (constructors, serializers, DB mappings, and
consumers) so type definitions accept null/sentinel and tests/consumers are
adjusted accordingly to avoid implying a false creation time.

In `@scenarios/svm_metaplex_demo/src/indexer.test.ts`:
- Around line 32-42: Replace the unsafe any[] usage and unguarded index access:
define proper types for the entries returned in result.changes (use the exported
types for TokenMetadataAccount and ProgramStats from the envio package) and type
the arrays tokenChanges and statsChanges accordingly; in the loop that reads
(change as any).TokenMetadataAccount / ProgramStats, cast to those concrete
types instead of any, extract sets into tokenChanges and statsChanges with
correct element types, and before computing finalStats use a guard (e.g., if
(statsChanges.length > 0) or optional chaining) to avoid accessing
statsChanges[statsChanges.length - 1] when the array is empty. Ensure
totalInstructionsAcrossBatches logic keeps using the typed
change.eventsProcessed field.
🪄 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: 38c79c00-6c4a-405b-a99e-e395cdb1f775

📥 Commits

Reviewing files that changed from the base of the PR and between d600268 and ab6ba21.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__indexer_code_generates_correct_types_and_values.snap is excluded by !**/*.snap
  • packages/cli/src/hbs_templating/snapshots/envio__hbs_templating__codegen_templates__test__indexer_code_multiple_chains.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • STAGE_7B_DECISIONS.md
  • packages/cli/Cargo.toml
  • packages/cli/CommandLineHelp.md
  • packages/cli/src/cli_args/init_config.rs
  • packages/cli/src/config_parsing/human_config.rs
  • packages/cli/src/config_parsing/public_config.rs
  • packages/cli/src/config_parsing/system_config.rs
  • packages/cli/src/config_parsing/validation.rs
  • packages/cli/src/hbs_templating/codegen_templates.rs
  • packages/cli/src/hbs_templating/contract_import_templates.rs
  • packages/cli/src/hypersync_source/config.rs
  • packages/cli/src/hypersync_source/decode.rs
  • packages/cli/src/hypersync_source/mod.rs
  • packages/cli/src/hypersync_source/query.rs
  • packages/cli/src/hypersync_source/types.rs
  • packages/cli/src/hypersync_source_svm/borsh_decoder.rs
  • packages/cli/src/hypersync_source_svm/config.rs
  • packages/cli/src/hypersync_source_svm/mod.rs
  • packages/cli/src/hypersync_source_svm/query.rs
  • packages/cli/src/hypersync_source_svm/types.rs
  • packages/cli/src/lib.rs
  • packages/cli/src/template_dirs.rs
  • packages/cli/templates/dynamic/init_templates/shared/package.json.hbs
  • packages/cli/templates/static/shared/pnpm-workspace.yaml
  • packages/cli/templates/static/svm_metaplex_template/typescript/.env.example
  • packages/cli/templates/static/svm_metaplex_template/typescript/.gitignore
  • packages/cli/templates/static/svm_metaplex_template/typescript/README.md
  • packages/cli/templates/static/svm_metaplex_template/typescript/config.yaml
  • packages/cli/templates/static/svm_metaplex_template/typescript/schema.graphql
  • packages/cli/templates/static/svm_metaplex_template/typescript/src/handlers/TokenMetadataHandlers.ts
  • packages/cli/templates/static/svm_metaplex_template/typescript/tsconfig.json
  • packages/cli/test/configs/svm-metaplex-config.yaml
  • packages/envio/evm.schema.json
  • packages/envio/fuel.schema.json
  • packages/envio/index.d.ts
  • packages/envio/package.json
  • packages/envio/src/ChainFetcher.res
  • packages/envio/src/Config.res
  • packages/envio/src/Core.res
  • packages/envio/src/Envio.res
  • packages/envio/src/EventConfigBuilder.res
  • packages/envio/src/HandlerLoader.res
  • packages/envio/src/Hasura.res
  • packages/envio/src/Internal.res
  • packages/envio/src/Main.res
  • packages/envio/src/SimulateItems.res
  • packages/envio/src/SvmTypes.res
  • packages/envio/src/sources/EventRouter.res
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/HyperSyncSolanaClient.res
  • packages/envio/src/sources/HyperSyncSolanaSource.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/Svm.res
  • packages/envio/src/tui/Tui.res
  • packages/envio/src/tui/components/TuiData.res
  • packages/envio/svm.schema.json
  • scenarios/svm_metaplex_demo/.envio/.gitignore
  • scenarios/svm_metaplex_demo/README.md
  • scenarios/svm_metaplex_demo/config.test.yaml
  • scenarios/svm_metaplex_demo/config.yaml
  • scenarios/svm_metaplex_demo/envio-env.d.ts
  • scenarios/svm_metaplex_demo/package.json
  • scenarios/svm_metaplex_demo/schema.graphql
  • scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts
  • scenarios/svm_metaplex_demo/src/indexer.test.ts
  • scenarios/svm_metaplex_demo/tsconfig.json
  • scenarios/svm_metaplex_demo/vitest.config.ts
  • scenarios/test_codegen/test/EventRouter_svm_test.res
  • scenarios/test_codegen/test/HyperSyncSolanaClient_test.res
  • scenarios/test_codegen/test/RpcSource_test.res
💤 Files with no reviewable changes (1)
  • packages/envio/package.json

Comment on lines +1500 to +1504
let abs = project_paths.project_root.join(idl_path);
let body = fs::read_to_string(&abs)
.with_context(|| format!("reading IDL at '{}'", abs.display()))?;
let schema = schema_from_anchor_idl_json(&body)
.with_context(|| format!("parsing IDL at '{}'", abs.display()))?;

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

Resolve program.idl relative to config.yaml.

idl is documented as config-relative, but project_root.join(idl_path) makes it repo-root-relative instead. Nested config locations will fail to load their IDL.

Suggested fix
-        let abs = project_paths.project_root.join(idl_path);
+        let abs = path_utils::get_config_path_relative_to_root(
+            project_paths,
+            PathBuf::from(idl_path),
+        )?;
         let body = fs::read_to_string(&abs)
             .with_context(|| format!("reading IDL at '{}'", abs.display()))?;
🤖 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/config_parsing/system_config.rs` around lines 1500 - 1504,
The code currently resolves idl_path against project_paths.project_root making
config-relative paths fail; change resolution to be relative to the config file
directory instead: when computing abs for reading the IDL, if idl_path is
absolute keep it, otherwise join it with the directory containing the config
(e.g. the parent of project_paths.config or equivalent) rather than
project_paths.project_root; update the use site where abs is computed (the block
that creates abs, reads body, and calls schema_from_anchor_idl_json) to use this
config-relative base so nested configs load their IDL correctly.

Comment on lines +1503 to +1543
let schema = schema_from_anchor_idl_json(&body)
.with_context(|| format!("parsing IDL at '{}'", abs.display()))?;
return Ok(SvmAbi {
program_id: program.program_id.clone(),
defined_types: schema.defined_types,
source: SvmSchemaSource::AnchorIdl {
path: idl_path.to_string(),
},
});
}

if !any_instruction_carries_schema {
if let Some((_, name, getter)) = bundled_program_schemas()
.into_iter()
.find(|(pid, _, _)| *pid == program.program_id.as_str())
{
let schema = getter();
return Ok(SvmAbi {
program_id: program.program_id.clone(),
defined_types: schema.defined_types.clone(),
source: SvmSchemaSource::Bundled { name },
});
}
}

Ok(SvmAbi {
program_id: program.program_id.clone(),
defined_types: BTreeMap::new(),
source: SvmSchemaSource::Inline,
})
}

fn lookup_program_schema(abi: &SvmAbi) -> Option<&'static SvmProgramSchema> {
match abi.source {
SvmSchemaSource::Bundled { .. } => bundled_program_schemas()
.into_iter()
.find(|(pid, _, _)| *pid == abi.program_id.as_str())
.map(|(_, _, getter)| getter()),
_ => None,
}
}

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 | 🏗️ Heavy lift

Keep the parsed SVM program schema around for layout resolution.

resolve_instruction_layout only infers accounts/args when program_schema is Some, but this code only reconstructs a schema for bundled programs and only when no instruction has inline schema. That means Anchor IDLs always fall back to empty layouts, and one inline override on a bundled program disables schema fallback for every other instruction.

Also applies to: 1572-1584

🤖 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/config_parsing/system_config.rs` around lines 1503 - 1543,
resolve_instruction_layout can't use parsed Anchor IDL schemas because
lookup_program_schema only returns schemas for SvmSchemaSource::Bundled; update
the ABI to keep the parsed program schema and make lookup_program_schema return
it: add a program_schema field (Option<&'static SvmProgramSchema> or owned
SvmProgramSchema as appropriate) to SvmAbi and populate it when creating the ABI
from schema_from_anchor_idl_json and when using bundled_program_schemas (use the
getter result), then change lookup_program_schema to first return
abi.program_schema if present and otherwise fall back to
bundled_program_schemas() like it currently does; reference SvmAbi,
SvmSchemaSource, lookup_program_schema, resolve_instruction_layout,
bundled_program_schemas, and schema_from_anchor_idl_json when applying the
changes.

Comment on lines +216 to +233
pub fn is_valid_solana_pubkey(s: &str) -> bool {
// Base58 alphabet: 1-9, A-H, J-N, P-Z, a-k, m-z (no 0, O, I, l).
// Encoded 32-byte values are 32-44 chars (typically 43-44).
let len = s.len();
if !(32..=44).contains(&len) {
return false;
}
s.bytes().all(|b| {
matches!(b,
b'1'..=b'9'
| b'A'..=b'H'
| b'J'..=b'N'
| b'P'..=b'Z'
| b'a'..=b'k'
| b'm'..=b'z'
)
})
}

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

Decode pubkeys instead of only checking alphabet and length.

This accepts any 32-44 char Base58-looking string, including values that decode to something other than 32 bytes. Invalid program_ids and account-filter pubkeys can therefore pass validation and fail much later at runtime.

🤖 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/config_parsing/validation.rs` around lines 216 - 233, The
current is_valid_solana_pubkey only checks Base58-like chars and length,
allowing strings that decode to non-32-byte values; update
is_valid_solana_pubkey to actually decode the input using a Base58 decoder
(e.g., bs58::decode) and return true only if decoding succeeds and the resulting
byte vector is exactly 32 bytes, otherwise return false; keep the existing fast
reject on gross length if desired but remove relying solely on character checks
and handle decode errors gracefully in the function.

Comment on lines +203 to +213
max_num_blocks: q.max_num_blocks.filter(|v| *v >= 0).map(|v| v as usize),
max_num_transactions: q
.max_num_transactions
.filter(|v| *v >= 0)
.map(|v| v as usize),
max_num_instructions: q
.max_num_instructions
.filter(|v| *v >= 0)
.map(|v| v as usize),
max_num_logs: q.max_num_logs.filter(|v| *v >= 0).map(|v| v as usize),
})

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify unchecked casts and silent-negative filtering in this file.
rg -n -C2 'max_num_(blocks|transactions|instructions|logs).*as usize|filter\(\|v\| \*v >= 0\)' packages/cli/src/hypersync_source_svm/query.rs

# Verify whether CI/build config includes any 32-bit targets where wrapping is possible.
fd -HI 'Cargo.toml|config.toml|*.yml|*.yaml' | xargs rg -n -C1 'i686|armv7|x86|32|target'

Repository: enviodev/hyperindex

Length of output: 50732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/src/hypersync_source_svm/query.rs"

echo "=== max_num_* block in context ==="
sed -n '150,230p' "$FILE"

echo "=== occurrences of max_num_* in query.rs ==="
rg -n "max_num_(blocks|transactions|instructions|logs)" "$FILE"

echo "=== type of q and relevant struct definitions (near occurrences) ==="
# Show a bit more around the first occurrence to capture the surrounding struct/impl.
FIRST_LINE=$(rg -n "max_num_blocks" "$FILE" | head -n 1 | cut -d: -f1)
START=$((FIRST_LINE-60))
END=$((FIRST_LINE+40))
sed -n "${START},${END}p" "$FILE"

echo "=== CI/workflow / config scan for 32-bit Rust targets ==="
# GitHub Actions workflows
if [ -d .github/workflows ]; then
  rg -n --hidden --glob '.github/workflows/*.yml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
  rg -n --hidden --glob '.github/workflows/*.yaml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
else
  echo "(no .github/workflows directory)"
fi

# Cargo/rust config locations (if present)
if [ -d .cargo ]; then
  rg -n --hidden '(i686|i386|armv7|--target\b|target\s*=)' .cargo || true
fi

# Any explicit target triples in repo config files
fd -HI --type f --exclude 'node_modules' 'Cargo.toml' 'Cargo.lock' 'rust-toolchain.toml' 'rust-toolchain' '*.yml' '*.yaml' '*.toml' 2>/dev/null \
  | xargs -r rg -n '(i686|i386|armv7|--target\b|target\s*=|wasm32)' || true

Repository: enviodev/hyperindex

Length of output: 4716


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/src/hypersync_source_svm/query.rs"

echo "=== max_num_* block in context ==="
sed -n '150,230p' "$FILE"

echo "=== occurrences of max_num_* in query.rs ==="
rg -n "max_num_(blocks|transactions|instructions|logs)" "$FILE"

echo "=== type context near first max_num_blocks ==="
FIRST_LINE=$(rg -n "max_num_blocks" "$FILE" | head -n 1 | cut -d: -f1)
START=$((FIRST_LINE-80))
END=$((FIRST_LINE+60))
sed -n "${START},${END}p" "$FILE"

echo "=== CI/workflow / config scan for 32-bit Rust targets ==="
if [ -d .github/workflows ]; then
  rg -n --hidden --glob '.github/workflows/*.yml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
  rg -n --hidden --glob '.github/workflows/*.yaml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
else
  echo "(no .github/workflows directory)"
fi

if [ -d .cargo ]; then
  rg -n --hidden '(i686|i386|armv7|--target\b|target\s*=)' .cargo || true
fi

# Limit search to likely config files to avoid huge outputs
fd -HI --type f \
  -g 'Cargo.toml' -g 'rust-toolchain*' \
  -g '*.yml' -g '*.yaml' -g '*.toml' \
  -g 'Dockerfile*' -g 'Makefile*' -g 'justfile' \
  | xargs -r rg -n '(i686|i386|armv7|--target\b|target\s*=|wasm32)' || true

Repository: enviodev/hyperindex

Length of output: 4694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/src/hypersync_source_svm/query.rs"

echo "=== max_num_* block in context ==="
sed -n '150,230p' "$FILE"

echo "=== occurrences of max_num_* in query.rs ==="
rg -n "max_num_(blocks|transactions|instructions|logs)" "$FILE"

echo "=== type context near first max_num_blocks ==="
FIRST_LINE=$(rg -n "max_num_blocks" "$FILE" | head -n 1 | cut -d: -f1)
START=$((FIRST_LINE-80))
END=$((FIRST_LINE+60))
sed -n "${START},${END}p" "$FILE"

echo "=== CI/workflow / config scan for 32-bit Rust targets ==="
if [ -d .github/workflows ]; then
  rg -n --hidden --glob '.github/workflows/*.yml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
  rg -n --hidden --glob '.github/workflows/*.yaml' '(i686|i386|armv7|--target\b|target\s*=)' .github/workflows || true
else
  echo "(no .github/workflows directory)"
fi

if [ -d .cargo ]; then
  rg -n --hidden '(i686|i386|armv7|--target\b|target\s*=)' .cargo || true
fi

# Limit search to likely config files to avoid huge outputs
fd -HI --type f \
  -g 'Cargo.toml' -g 'rust-toolchain*' \
  -g '*.yml' -g '*.yaml' -g '*.toml' \
  -g 'Dockerfile*' -g 'Makefile*' -g 'justfile' \
  | xargs -r rg -n '(i686|i386|armv7|--target\b|target\s*=|wasm32)' || true

Repository: enviodev/hyperindex

Length of output: 4694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/cli/src/hypersync_source_svm/query.rs"

echo "=== imports/top of file ==="
sed -n '1,120p' "$FILE"

echo "=== definition of SolanaQuery (where max_num_* are declared) ==="
rg -n "struct SolanaQuery|pub max_num_blocks|pub max_num_transactions|pub max_num_instructions|pub max_num_logs" "$FILE"
# Print around the struct block that contains the max_num_* fields
FIRST=$(rg -n "pub max_num_blocks" "$FILE" | head -n 1 | cut -d: -f1)
START=$((FIRST-40)); END=$((FIRST+40))
sed -n "${START},${END}p" "$FILE"

echo "=== SolanaQuery -> net::SolanaQuery mapping function signature and any existing anyhow imports/uses ==="
rg -n "impl TryFrom<SolanaQuery> for net::SolanaQuery|type Error|anyhow::ensure!|transpose\\(\\)\\?" "$FILE"
FIRST_TRY=$(rg -n "impl TryFrom<SolanaQuery> for net::SolanaQuery" "$FILE" | head -n 1 | cut -d: -f1)
START2=$((FIRST_TRY-30)); END2=$((FIRST_TRY+80))
sed -n "${START2},${END2}p" "$FILE"

Repository: enviodev/hyperindex

Length of output: 6212


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/cli/src/hypersync_source_svm/query.rs"
rg -n "anyhow::\\{[^}]*Context|use anyhow::\\{[^}]*Context|use anyhow::Context|\\bContext\\b" "$FILE" || true
rg -n "\\.context\\(" "$FILE" || true

Repository: enviodev/hyperindex

Length of output: 45


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the actual net::SolanaQuery definition in the workspace/deps (likely in hypersync_solana_net_types).
rg -n "struct SolanaQuery" -S . --glob '*.rs' | head -n 50

# Look specifically for max_num_blocks field type usages near the net SolanaQuery.
rg -n "max_num_blocks\s*:" -S . --glob '*.rs' | head -n 50

Repository: enviodev/hyperindex

Length of output: 690


Reject invalid max_num_* limits instead of silently dropping/casting

In impl TryFrom<SolanaQuery> for net::SolanaQuery, max_num_blocks, max_num_transactions, max_num_instructions, and max_num_logs currently drop negative values via .filter(|v| *v >= 0) and then cast with v as usize without overflow checking, which can silently produce incorrect limits.

💡 Proposed fix
-use anyhow::Result;
+use anyhow::{Context, Result};
@@
-            max_num_blocks: q.max_num_blocks.filter(|v| *v >= 0).map(|v| v as usize),
+            max_num_blocks: q
+                .max_num_blocks
+                .map(|v| {
+                    anyhow::ensure!(v >= 0, "max_num_blocks must be non-negative");
+                    usize::try_from(v).context("max_num_blocks overflows usize")
+                })
+                .transpose()?,
@@
-            max_num_transactions: q
-                .max_num_transactions
-                .filter(|v| *v >= 0)
-                .map(|v| v as usize),
+            max_num_transactions: q
+                .max_num_transactions
+                .map(|v| {
+                    anyhow::ensure!(v >= 0, "max_num_transactions must be non-negative");
+                    usize::try_from(v).context("max_num_transactions overflows usize")
+                })
+                .transpose()?,
@@
-            max_num_instructions: q
-                .max_num_instructions
-                .filter(|v| *v >= 0)
-                .map(|v| v as usize),
+            max_num_instructions: q
+                .max_num_instructions
+                .map(|v| {
+                    anyhow::ensure!(v >= 0, "max_num_instructions must be non-negative");
+                    usize::try_from(v).context("max_num_instructions overflows usize")
+                })
+                .transpose()?,
@@
-            max_num_logs: q.max_num_logs.filter(|v| *v >= 0).map(|v| v as usize),
+            max_num_logs: q
+                .max_num_logs
+                .map(|v| {
+                    anyhow::ensure!(v >= 0, "max_num_logs must be non-negative");
+                    usize::try_from(v).context("max_num_logs overflows usize")
+                })
+                .transpose()?,
🤖 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/hypersync_source_svm/query.rs` around lines 203 - 213, The
conversion in impl TryFrom<SolanaQuery> for net::SolanaQuery currently silently
drops negative max_num_* values and uses unchecked casts (v as usize); instead
validate each optional limit (max_num_blocks, max_num_transactions,
max_num_instructions, max_num_logs) and return an Err when a value is negative
or cannot be losslessly converted to usize. Locate the fields in the TryFrom
implementation and replace the .filter(...).map(|v| v as usize) logic with
explicit checks (e.g., match or if let) that reject negative inputs and use a
safe conversion (v.try_into() or similar) with error mapping so invalid or
out-of-range inputs produce a descriptive conversion error rather than being
silently dropped or truncated.

Comment on lines +454 to +456
Ok(net_types::Query {
from_block: query.from_block as u64,
to_block: query.to_block.map(|b| b as u64),

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 | 🟡 Minor | ⚡ Quick win

Negative block numbers silently wrap to large values.

from_block and to_block are cast from i64 to u64 without validation. A negative value (e.g., -1) would wrap to u64::MAX, causing unexpected query behavior. Consider validating or using TryFrom with an error.

🛡️ Proposed fix
 Ok(net_types::Query {
-    from_block: query.from_block as u64,
-    to_block: query.to_block.map(|b| b as u64),
+    from_block: query.from_block.try_into().context("from_block must be non-negative")?,
+    to_block: query.to_block.map(|b| b.try_into()).transpose().context("to_block must be non-negative")?,
📝 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
Ok(net_types::Query {
from_block: query.from_block as u64,
to_block: query.to_block.map(|b| b as u64),
Ok(net_types::Query {
from_block: query.from_block.try_into().context("from_block must be non-negative")?,
to_block: query.to_block.map(|b| b.try_into()).transpose().context("to_block must be non-negative")?,
🤖 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/hypersync_source/query.rs` around lines 454 - 456, The cast
of query.from_block and query.to_block into u64 (in the net_types::Query
construction) can silently wrap negative i64 values; replace the direct casts
with fallible conversions that validate non-negativity (e.g., use
TryFrom/TryInto or explicit checks) and propagate an error if a negative value
is encountered: validate query.from_block >= 0 before converting to u64 and
convert query.to_block with map(|b| b.try_into().map_err(|_| ...)?), returning
or mapping a descriptive error instead of producing u64::MAX. Ensure the error
type matches the surrounding function's Result so callers get a clear failure
instead of wrapped values.

Comment on lines +333 to +366
"position": {
"description": "Account position within the instruction (0..=5).",
"type": "integer",
"format": "uint8",
"minimum": 0,
"maximum": 255
},
"values": {
"description": "Allowed base58 pubkeys for this account position.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": [
"position",
"values"
]
},
"AnyOfAccountFilters": {
"type": "object",
"properties": {
"any_of": {
"description": "A non-empty list of AND-groups. Each group is itself a non-empty list of `{position, values}` entries that must all match the same instruction. An instruction matches `any_of` when any one group matches.",
"type": "array",
"items": {
"type": "array",
"items": {
"$ref": "#/$defs/AccountFilter"
}
}
}

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 | 🟡 Minor | ⚡ Quick win

Tighten the schema to match the CLI's SVM filter validation.

The CLI now rejects positions above 5, empty any_of, and empty groups, but the schema still allows position: 255, any_of: [], and any_of: [[]]. That means editor/schema validation will bless configs the CLI later rejects.

Suggested fix
         "position": {
           "description": "Account position within the instruction (0..=5).",
           "type": "integer",
           "format": "uint8",
           "minimum": 0,
-          "maximum": 255
+          "maximum": 5
         },
@@
         "any_of": {
           "description": "A non-empty list of AND-groups. Each group is itself a non-empty list of `{position, values}` entries that must all match the same instruction. An instruction matches `any_of` when any one group matches.",
           "type": "array",
+          "minItems": 1,
           "items": {
             "type": "array",
+            "minItems": 1,
             "items": {
               "$ref": "`#/`$defs/AccountFilter"
             }
           }
         }
🤖 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/svm.schema.json` around lines 333 - 366, The schema currently
allows invalid CLI values: tighten AccountFilter.position by lowering "maximum"
from 255 to 5 (keep integer/format but enforce max 5) and make
AnyOfAccountFilters.any_of a non-empty array with non-empty groups by adding
"minItems": 1 on the any_of array and also add "minItems": 1 to its inner items
arrays (the arrays of AccountFilter groups) so any_of: [] and any_of: [[]] are
disallowed; refer to the AccountFilter definition (properties.position) and the
AnyOfAccountFilters definition (properties.any_of) to apply these changes.

Comment on lines +18 to +20
# 2. (Optional) If you have CARGO_TARGET_DIR set, sync the artifact:
cp -f "$CARGO_TARGET_DIR/debug/libenvio.so" target/debug/libenvio.so

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 | 🟡 Minor | ⚡ Quick win

Guard the optional artifact copy command.

Line 19 can fail when CARGO_TARGET_DIR is unset, even though this step is marked optional.

Proposed doc fix
 # 2. (Optional) If you have CARGO_TARGET_DIR set, sync the artifact:
-cp -f "$CARGO_TARGET_DIR/debug/libenvio.so" target/debug/libenvio.so
+if [ -n "${CARGO_TARGET_DIR:-}" ]; then
+  cp -f "$CARGO_TARGET_DIR/debug/libenvio.so" target/debug/libenvio.so
+fi
🤖 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 `@scenarios/svm_metaplex_demo/README.md` around lines 18 - 20, The optional
copy command should be guarded so it only runs when CARGO_TARGET_DIR is set;
wrap the cp -f "$CARGO_TARGET_DIR/debug/libenvio.so" target/debug/libenvio.so
command in a shell conditional (e.g., check [ -n "$CARGO_TARGET_DIR" ] or use
${CARGO_TARGET_DIR:+...}) to skip the copy when CARGO_TARGET_DIR is unset and
optionally print a short message; update the README.md instruction around the
CARGO_TARGET_DIR usage to show this guarded form and keep the step optional.

Comment on lines +39 to +44
// DO NOT MERGE WITH THESE TESTS SKIPPED.
// TEMP: skipped on the svm-hyperindex-demo branch to unblock the
// experimental release. CI's `ENVIO_API_TOKEN` lacks product access to
// `eth.rpc.hypersync.xyz` (returns 403 "Your token does not have access
// to this product"). Restore by reverting this `it_skip` once the token's
// RPC subscription is sorted.

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 | 🏗️ Heavy lift

Remove "DO NOT MERGE" comments before merging to main.

Three tests are marked with "DO NOT MERGE WITH THESE TESTS SKIPPED" comments. Merging code with explicit DO-NOT-MERGE markers is a critical process violation. These comments indicate unfinished work and create confusion about the PR's merge readiness.

According to the comments, the root cause is an infrastructure issue: CI's ENVIO_API_TOKEN lacks access to eth.rpc.hypersync.xyz (HTTP 403). Before merging:

  1. Preferred: Resolve the token access issue and restore the tests, or
  2. Alternative: If the RPC tests must remain skipped, remove the "DO NOT MERGE" comments and replace them with a clear explanation + tracking issue reference (e.g., "Skipped pending #ISSUE_NUMBER: CI token RPC access").

Also applies to: 143-144, 649-650

🤖 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 `@scenarios/test_codegen/test/RpcSource_test.res` around lines 39 - 44, Remove
the "DO NOT MERGE WITH THESE TESTS SKIPPED" comments in RpcSource_test.res and
either (A) re-enable the tests by addressing the CI ENVIO_API_TOKEN access to
eth.rpc.hypersync.xyz so the it_skip usages (search for it_skip or the specific
skipped test blocks) are no longer needed, or (B) if you must keep the RPC tests
skipped, replace the DO-NOT-MERGE text with a clear skip explanation that
references a tracking issue (e.g., "Skipped pending `#ISSUE_NUMBER`: CI
ENVIO_API_TOKEN lacks access to eth.rpc.hypersync.xyz (403)") and apply the same
change to the other occurrences noted (around the other comment ranges).

@JasoonS JasoonS closed this Jun 4, 2026
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