Skip to content

Solana instruction indexing foundation#1241

Open
JasoonS wants to merge 25 commits into
mainfrom
solana-instruction-basics
Open

Solana instruction indexing foundation#1241
JasoonS wants to merge 25 commits into
mainfrom
solana-instruction-basics

Conversation

@JasoonS
Copy link
Copy Markdown
Contributor

@JasoonS JasoonS commented May 27, 2026

Summary by CodeRabbit

  • New Features

    • Added Solana (SVM) instruction indexing via HyperSync for streaming and decoding program instructions with Borsh schema support.
    • Introduced Metaplex Token Metadata template for indexing NFT metadata creation and updates.
  • Documentation

    • Added template guides and quick-start instructions for SVM indexing.

Review Change Stack

@JasoonS JasoonS requested a review from DZakh May 27, 2026 11:40
claude and others added 15 commits May 27, 2026 13:42
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.
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.

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

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

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

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

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

- `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'`

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This PR implements end-to-end Solana SVM instruction indexing. It adds YAML schemas for configuring Solana programs and instructions, integrates Borsh decoding via Rust/N-API, extends HyperSync to query Solana instruction data, generates typed ReScript/TypeScript handler code with decoded instruction arguments, and provides a complete Metaplex Token Metadata demo with tests.

Changes

SVM Instruction Indexing Support

Layer / File(s) Summary
Design decisions, SVM schema types, and CLI surface
STAGE_7B_DECISIONS.md, packages/cli/Cargo.toml, packages/cli/CommandLineHelp.md, packages/cli/src/cli_args/init_config.rs, packages/cli/src/cli_args/interactive_init/mod.rs, packages/cli/src/config_parsing/human_config.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
Documents schema-lookup design (opaque handles), bundled schema auto-attachment, typed codegen for args/accounts, YAML validation rules, and envio init --idl behavior. Introduces HypersyncConfig type, optional programs_experimental list, and Borsh argument grammars (ArgDef, ArgType, ArgPrimitive, ArgComposite, ArgEnumVariant, filters). Updates SVM CLI template selection to interactively choose between Metaplex and feature templates. Adds pnpm build configuration.
YAML validation and system config translation
packages/cli/src/config_parsing/validation.rs, packages/cli/src/config_parsing/system_config.rs, packages/cli/src/config_parsing/public_config.rs, packages/envio/svm.schema.json
Validates SVM YAML (base58 pubkeys, hex discriminators, account filter constraints). Resolves program schemas from IDL paths, bundled entries (Metaplex Token Metadata), or inline definitions. Builds DataSource::Svm with optional HyperSync URL, derives EventKind::Svm per instruction with discriminator bytes and (accounts, args) layouts. Emits Abi::Svm(SvmAbi) with schema source and defined types. Generates public SvmConfig JSON with programs map and SvmEventItem descriptors.
Trace query support and HyperSync N-API initialization
packages/cli/src/hypersync_source/mod.rs, packages/cli/src/hypersync_source/query.rs, packages/envio/src/sources/HyperSyncClient.res
Extends HyperSync client API with optional trace field selections (d1/d2/d4/d8 discriminators, a0-a9 accounts, addresses, call types, sighashes) and max_num_traces limits. Adds set_log_level N-API function and HypersyncClient::new constructor delegating to new_with_agent with auto-set user-agent.
Rust/N-API Borsh decoder, Solana queries, and client
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/envio/src/sources/HyperSyncSolanaClient.res
Implements process-global schema registry via register_program_schema (returns stable u32 handle) and decode_instruction (returns optional decoded args/named accounts/extra accounts). Defines SolanaQuery and nested InstructionSelection/TransactionSelection/LogSelection/FieldSelection with i64 slot/count inputs. Exports HypersyncSolanaClient wrapping hypersync_client_solana::Client, providing get_height and get async methods. Defines response types: Block, Transaction, Instruction (with optional d1-d8 discriminator prefixes, a0-a5 account filters, decoded payload), Log, TokenBalance, Reward, QueryResponse.
Runtime types, addon interface, and event config builders
packages/envio/src/Core.res, packages/envio/src/Envio.res, packages/envio/src/Internal.res, packages/envio/src/EventConfigBuilder.res
Adds ReScript types for decoded instructions (svmDecodedInstruction), raw instructions with optional decoded data (svmInstruction), parent transactions and token balances (svmTransaction, svmTokenBalance), per-instruction logs (svmLog), and event payloads (svmInstructionEvent). Introduces svmInstructionEventConfig with program id, discriminator/byte-length, inclusion toggles, account filters, and positional args/defined types. Exposes addon methods: setLogLevel, registerProgramSchema, decodeInstruction. Implements buildSvmInstructionEventConfig builder deriving parameter schema and wiring identity/flags.
Code generation for SVM events and handler types
packages/cli/src/hbs_templating/codegen_templates.rs, packages/cli/src/hbs_templating/contract_import_templates.rs, packages/envio/index.d.ts
Generates per-instruction ReScript event modules via EventTemplate::from_config_event(EventKind::Svm(_)) (minimal svmInstruction params, no-op where filter). Adds SVM indexer onInstruction registration type and onInstructionOptions callback signature. Emits TypeScript svm_programs table under Global.config.svm.programs with per-instruction typed args (recursively mapped from Solana Borsh schemas with cycle detection) and string-keyed accounts shapes. Adds public TypeScript types: SvmDecodedInstruction, SvmInstruction, SvmTokenBalance, SvmTransaction, SvmLog, SvmInstructionEvent<Decoded>, handler args/options/signatures.
Config parsing, event routing, and source wiring
packages/envio/src/Config.res, packages/envio/src/sources/EventRouter.res, packages/envio/src/sources/Svm.res, packages/envio/src/ChainFetcher.res
Parses SVM program/event/ABI from public config. buildContractEvents derives single programId from chain addresses, converts account filter pubkeys, builds instruction event configs via buildSvmInstructionEventConfig. Event router adds getSvmEventId (programId + discriminator/none suffix) and fromSvmEventConfigsOrThrow (precomputes per-program discriminator byte-length descending ordering for longest-first routing). makeRPCSource now accepts configurable sourceFor parameter. ChainFetcher conditionally creates HyperSyncSolanaSource (if hypersync URL present) with fallback to RPC oracle source.
HyperSync Solana source orchestration
packages/envio/src/sources/HyperSyncSolanaSource.res
Builds instruction selections with account filter grouping and discriminator byte-length ordering from event configs. Groups configs by programId, registers schemas once at startup, precomputes schema handles and whether token balances are needed. Fetches blocks/instructions/logs/transactions via HyperSync client. Indexes fetched data by (slot, txIndex) and (slot, txIndex, instructionAddress) keys. Routes each instruction using discriminator-keyed lookup (longest-first probe with program-wide fallback), decodes payload, synthesizes deterministic logIndex, attaches transaction/logs/token-balances conditionally, materializes svmInstructionEvent payloads. Exposes getBlockHashes (throws unsupported) and getHeightOrThrow with Prometheus metrics.
Handler registration, metadata reload, and UI updates
packages/envio/src/Main.res, packages/envio/src/HandlerLoader.res, packages/envio/src/Hasura.res, packages/envio/src/SimulateItems.res, packages/envio/src/SvmTypes.res, packages/envio/src/tui/Tui.res, packages/envio/src/tui/components/TuiData.res
Adds onInstruction handler registration parsing SVM identity ({program, instruction} or ReScript GADT), wiring to HandlerRegister.setHandler. Replaces prior SVM error path with SVM instruction event config construction. Adds reloadHasuraMetadata REST call after schema clearing to force re-introspection. Updates patchConfig to apply startBlock/endBlock overrides even when no simulate items present. Introduces SvmTypes.Pubkey module with schema/externals for pubkey/array conversions. Updates TUI to compute per-chain blockUnit ("Slots" for SVM, "Blocks" for EVM/Fuel) and render dynamic progress labels.
Metaplex metadata indexer template and scenario
packages/cli/templates/static/svm_metaplex_template/*, scenarios/svm_metaplex_demo/*, scenarios/test_codegen/test/*_test.res, packages/cli/test/configs/svm-metaplex-config.yaml
Provides Metaplex Token Metadata CLI template: .env.example (mainnet RPC), .gitignore, README (quick-start), config.yaml (TokenMetadata program, CreateMetadataAccountV3/UpdateMetadataAccountV2 with discriminators), schema.graphql (TokenMetadataAccount, ProgramStats types), handlers/TokenMetadataHandlers.ts (extract accounts, upsert metadata, bump stats). Live scenario with config (pinned slots), E2E test querying HyperSync window and asserting metadata writes/stats consistency. Router and client unit tests exercise getSvmEventId fallback, discriminator byte-length ordering precomputation. CLI test config uses same program/instructions with deterministic block range.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • enviodev/hyperindex#890: Updates SVM interactive init flow to route through template prompting, directly overlapping the CLI template selection refactor in this PR.
  • enviodev/hyperindex#1212: Adds HyperSync N-API surface (set_log_level, HypersyncClient::new) in the same addon wrapper layer as this PR's trace support extension.
  • enviodev/hyperindex#1071: Modifies codegen event plumbing in codegen_templates.rs, complementary to this PR's SVM instruction event code generation.

Suggested reviewers

  • DZakh
  • JonoPrest
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

JasoonS and others added 7 commits May 27, 2026 13:44
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.
Plumb SPL Token / Token-2022 pre/post balance snapshots through the
full pipeline so handlers can access them via
event.transaction.tokenBalances when include_token_balances is true
in config.yaml. include_token_balances implies include_transaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signals that the SVM config surface may change in future releases.
Internal Rust types (Program, Vec<Program>) keep their names; only
the serde-facing YAML key changes via #[serde(rename)].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime auto-discovers handlers from src/handlers/. The field is
still valid in the schema for users who need explicit paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidate include_transaction, include_logs, include_token_balances
into a field_selection block that accepts true (all fields) or a list
of field names (per-field selection, not yet supported). This aligns
with the EVM field_selection pattern and is forward-compatible.

When field_selection is absent, no extra data is fetched. The
downstream pipeline (public_config, Config.res, EventConfigBuilder,
HyperSyncSolanaSource) remains unchanged — the conversion happens
in system_config.rs when building SvmEventKind.

https://claude.ai/code/session_01YV1sASgBTS5Sx5ktDTD42P
@JasoonS JasoonS force-pushed the solana-instruction-basics branch from 00cfabb to 958972d Compare May 27, 2026 11:44
The Trace struct was removed by the Rust decoder refactor on main,
but the impl block survived conflict resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Config.res: rename eventItem["event"] to eventItem["sighash"] to
  match main's field rename in the contract event item schema
- system_config.rs: replace .map_or(false, ...) with .is_some_and(...)
  to satisfy the clippy unnecessary_map_or lint

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- RpcSource_test.res: use allEventParams (main's field name) instead
  of allEventSignatures (stale name from pre-rebase branch)
- interactive_init/mod.rs: delegate to svm_prompts::prompt_template_init_flow
  instead of hardcoding FeatureBlockHandler, so the Select prompt
  shows all available SVM templates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JasoonS
Copy link
Copy Markdown
Contributor Author

JasoonS commented May 27, 2026

@coderabbitai review

@enviodev enviodev deleted a comment from coderabbitai Bot May 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (2)
scenarios/test_codegen/test/HyperSyncSolanaClient_test.res (1)

16-16: ⚡ Quick win

Replace deprecated Pervasives.max with Int.max.

The Pervasives module is deprecated in ReScript 12. Use Int.max instead.

♻️ Proposed fix
-      fromSlot: Pervasives.max(0, height - 10_000),
+      fromSlot: Int.max(0, height - 10_000),

As per coding guidelines: Always use ReScript 12 documentation. Never suggest ReasonML syntax

🤖 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/HyperSyncSolanaClient_test.res` at line 16, The
code uses the deprecated Pervasives.max when computing fromSlot; replace
Pervasives.max with Int.max (i.e., change the call used in the expression that
sets fromSlot) so the expression becomes Int.max(0, height - 10_000), ensuring
you use ReScript 12 Int module functions rather than Pervasives.
packages/envio/src/sources/HyperSyncSolanaSource.res (1)

319-336: ⚡ Quick win

Precompute instruction selections once per source instance.

buildInstructionSelections(eventConfigs) is request-invariant, but it’s rebuilt on every fetch loop. Move it to make scope to reduce hot-path overhead.

♻️ Proposed refactor
 let make = ({chain, endpointUrl, apiToken, eventConfigs, clientMaxRetries, clientTimeoutMillis}: options): t => {
@@
   let schemaHandlesByProgram = buildSchemaHandles(eventConfigs)
 
   let needsTokenBalances = eventConfigs->Belt.Array.some(cfg => cfg.includeTokenBalances)
+  let instructionSelections = buildInstructionSelections(eventConfigs)
@@
   let getItemsOrThrow = async (
@@
-    let instructionSelections = buildInstructionSelections(eventConfigs)
     let fields = if needsTokenBalances {
🤖 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 319 - 336,
buildInstructionSelections(eventConfigs) is being recomputed on every fetch;
compute it once in the source's make scope and reuse it in the fetch loop to
avoid hot-path overhead. Move the call to
buildInstructionSelections(eventConfigs) into the make function (store result in
a field/property on the source instance, e.g., instructionSelectionsOnInit),
remove the per-fetch call that defines instructionSelections in the fetch loop,
and reference that stored instructionSelections when building the
HyperSyncSolanaClient.query (the query construction that uses instructions:
instructionSelections). Ensure the stored value is used wherever the current
per-request variable instructionSelections would be used.
🤖 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/cli_args/interactive_init/mod.rs`:
- Around line 162-164: The InitFlow SVM CLI path currently hardcodes
FeatureBlockHandler when InitFlow::Svm { init_flow: None } is used, bypassing
the interactive template selection; update the logic that constructs
InitFlow::Svm so that when init_flow is None it calls
svm_prompts::prompt_template_init_flow (the same function used in the
interactive branch) and uses its returned TemplateArgs to populate the SVM
init_flow instead of defaulting to FeatureBlockHandler, ensuring
MetaplexTokenMetadata becomes selectable; locate the code creating InitFlow::Svm
and replace the hardcoded TemplateArgs/FeatureBlockHandler fallback with a call
to svm_prompts::prompt_template_init_flow (or propagate the TemplateArgs there)
and handle errors similarly to the interactive branch.

In `@packages/cli/src/config_parsing/system_config.rs`:
- Around line 1101-1102: The Chain struct is being created with a hardcoded id =
0 which causes collisions for multiple SVM networks; change the creation of
Chain in the block that builds SVM networks to use a unique identifier (e.g.,
use the existing network.id or a composed unique key like (network.name,
network.id) or a generated UUID) instead of 0 and ensure the same identifier is
used as the key when inserting into the chains HashMap so each SVM chain
(mainnet, devnet, etc.) is indexed uniquely.

In `@packages/cli/src/hypersync_source_svm/config.rs`:
- Around line 28-32: The current conversion for max_num_retries uses a lossy
cast (.map(|v| v as u32)) which silently truncates values > u32::MAX; change the
mapping to perform a fallible conversion and only accept values that fit into
u32 (e.g., use u32::try_from or v.try_into() and .ok() to produce an
Option<u32>), keeping the existing filter(|v| *v >= 0) and falling back to
default.max_num_retries when the conversion fails; update the expression
referencing c.max_num_retries / max_num_retries to use this safe conversion.

In
`@packages/cli/templates/static/svm_metaplex_template/typescript/src/handlers/TokenMetadataHandlers.ts`:
- Line 48: The code in TokenMetadataHandlers.ts sets const mint = accounts[1] ??
"" which allows an empty string to be persisted; instead, guard on a missing
mint by checking accounts[1] (or the enclosing handler function that processes
accounts) and return/skip persisting or throw early if it's absent—do not
fallback to "". Locate the mint assignment and replace the unconditional
empty-string fallback with a presence check (e.g., if (!accounts[1]) { /*
skip/return */ }) so downstream code does not write placeholder identity data.

In `@packages/envio/index.d.ts`:
- Around line 1066-1069: The current constrained infer `accounts: infer Acc
extends Readonly<Record<string, string>>` rejects fixed-key account types (e.g.
`{ readonly authority: string }`) and can trigger the fallback to
`SvmDecodedInstruction`; to fix this, relax the constraint by inferring
`accounts: infer Acc` (remove `extends Readonly<Record<string, string>>`) and
then use a conditional check like `Acc extends Readonly<Record<keyof Acc,
string>>` (or `Acc extends Record<PropertyKey, string> ? ...`) in the
`SvmDecodedFromProgramTable` definition to preserve narrowing for fixed-key
objects and only fall back to `SvmDecodedInstruction` when the inferred `Acc`
truly does not have string-valued properties; update the branches that produce
`decoded.accounts`/`decoded.args` accordingly so they use the validated `Acc`
and avoid unintended fallback.

In `@packages/envio/src/Config.res`:
- Around line 197-219: svmEventDescriptorSchema currently parses accountFilters
as option(array(schema(...))) but downstream expects nested OR-of-AND groups
(array<array<...>>), and includeTokenBalances is not parsed here so should be
omitted; update the accountFilters matcher in svmEventDescriptorSchema to
S.option(S.array(S.array(S.schema(...)))) preserving the inner schema shape
(position:int, values:array(string)) and remove any parsing of
includeTokenBalances from this schema so the structure matches the downstream
SVM event contract used by the mapping logic elsewhere.

In `@packages/envio/svm.schema.json`:
- Around line 337-343: The "position" property in the schema (property name
"position" under account_filters) allows up to 255 but must be constrained to
the supported range 0..=5; update the JSON Schema for the "position" field by
changing its "maximum" from 255 to 5 (keep "minimum": 0 and "format": "uint8"
as-is) so invalid configs with positions >5 fail validation.

In `@scenarios/svm_metaplex_demo/src/handlers/TokenMetadataHandlers.ts`:
- Around line 89-99: The code in the pre-existing-metadata branch incorrectly
writes a synthetic createdAtSlot (using event.slot) even though the true
creation slot is unknown; update the TokenMetadataAccount.set call (the branch
handling "Metadata account existed before our `start_block`") to omit or set
createdAtSlot to null/undefined instead of assigning event.slot, and ensure you
do not overwrite any existing createdAtSlot on the record—only update fields you
actually know (id/ updateAuthority/ lastUpdatedSlot/ updateCount/
lastTxSignature) or explicitly preserve the existing createdAtSlot if one exists
in the DB schema for TokenMetadataAccount.

In `@scenarios/svm_metaplex_demo/src/indexer.test.ts`:
- Around line 14-18: The test sets process.env.ENVIO_CONFIG after a static
import of createTestIndexer, so envio may read the env var during its module
initialization; change indexer.test.ts to assign process.env.ENVIO_CONFIG =
"config.test.yaml" before importing envio by replacing the static import of
createTestIndexer with a dynamic import (e.g., set the env var at top-level,
then await import("envio") and extract createTestIndexer), ensuring
createTestIndexer is obtained after the env var is set so envio sees the
override.

In `@scenarios/test_codegen/test/HyperSyncSolanaClient_test.res`:
- Line 22: resp.data.instructions is accessed with Array.getUnsafe(0) (assigned
to first) before confirming the array is non-empty, which can panic; update the
code to check the length or use a safe getter first (e.g., verify
Array.length(resp.data.instructions) > 0 or use Array.get/Belt.Array.get to get
an option) and only assign to first after the presence check (or pattern-match
the option and assert), referencing resp.data.instructions, Array.getUnsafe, and
the first binding.

In `@scenarios/test_codegen/test/RpcSource_test.res`:
- Around line 42-55: Add an explanatory comment above the Async.it_skip test
(the one calling RpcSource.make and getHeightOrThrow) that documents why the
integration test is skipped: state whether it flakily depends on external RPC
availability, rate limits or test API token usage, mention any known
failures/PRs or environment requirements, and indicate what conditions would
allow re-enabling (e.g., stable mock RPC, recorded fixtures, or CI credentials).
Reference the test identifiers Async.it_skip, RpcSource.make, and
getHeightOrThrow so reviewers can find the test and follow the re-enable
instructions.
- Around line 140-142: Remove or resolve the temporary "DO NOT MERGE" skip
comments around the skipped integration tests: either re-enable the tests by
changing Async.it_skip("Queries transaction fields from raw JSON (with real
RPC)", ...) to Async.it(...) and remove the DO NOT MERGE note, or if they must
remain skipped, replace the comment with a clear TODO that includes a tracking
issue ID and an expected re-enable date; also apply the same fix for the other
skipped block referenced (around the getHeightOrThrow test and lines 648-650).
Ensure the updated comment or TODO references the test identifiers (e.g.,
Async.it_skip for the "Queries transaction fields..." test and the
getHeightOrThrow test) so reviewers can find and validate the change.

---

Nitpick comments:
In `@packages/envio/src/sources/HyperSyncSolanaSource.res`:
- Around line 319-336: buildInstructionSelections(eventConfigs) is being
recomputed on every fetch; compute it once in the source's make scope and reuse
it in the fetch loop to avoid hot-path overhead. Move the call to
buildInstructionSelections(eventConfigs) into the make function (store result in
a field/property on the source instance, e.g., instructionSelectionsOnInit),
remove the per-fetch call that defines instructionSelections in the fetch loop,
and reference that stored instructionSelections when building the
HyperSyncSolanaClient.query (the query construction that uses instructions:
instructionSelections). Ensure the stored value is used wherever the current
per-request variable instructionSelections would be used.

In `@scenarios/test_codegen/test/HyperSyncSolanaClient_test.res`:
- Line 16: The code uses the deprecated Pervasives.max when computing fromSlot;
replace Pervasives.max with Int.max (i.e., change the call used in the
expression that sets fromSlot) so the expression becomes Int.max(0, height -
10_000), ensuring you use ReScript 12 Int module functions rather than
Pervasives.
🪄 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: 1da67b49-1eb7-4b94-b171-b395458190ba

📥 Commits

Reviewing files that changed from the base of the PR and between 64805bc and 42c763b.

⛔ 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 (64)
  • STAGE_7B_DECISIONS.md
  • packages/cli/Cargo.toml
  • packages/cli/CommandLineHelp.md
  • packages/cli/src/cli_args/init_config.rs
  • packages/cli/src/cli_args/interactive_init/mod.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/mod.rs
  • packages/cli/src/hypersync_source/query.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/index.d.ts
  • 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/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

Comment on lines +162 to +164
init_flow: svm_prompts::prompt_template_init_flow(
clap_definitions::svm::TemplateArgs { template: 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 | ⚡ Quick win

SVM template selection is still bypassed on envio init svm (no subcommand).

This interactive branch now prompts correctly, but the CLI path with InitFlow::Svm { init_flow: None } still hardcodes FeatureBlockHandler, so users can't pick MetaplexTokenMetadata there.

Proposed fix
 fn get_svm_ecosystem(init_flow: Option<clap_definitions::svm::InitFlow>) -> Result<Ecosystem> {
     Ok(match init_flow {
         Some(clap_definitions::svm::InitFlow::Template(args)) => Ecosystem::Svm {
             init_flow: svm_prompts::prompt_template_init_flow(args)?,
         },
         None => Ecosystem::Svm {
-            init_flow: crate::init_config::svm::InitFlow::Template(
-                crate::init_config::svm::Template::FeatureBlockHandler,
-            ),
+            init_flow: svm_prompts::prompt_template_init_flow(
+                clap_definitions::svm::TemplateArgs { template: None },
+            )?,
         },
     })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli_args/interactive_init/mod.rs` around lines 162 - 164,
The InitFlow SVM CLI path currently hardcodes FeatureBlockHandler when
InitFlow::Svm { init_flow: None } is used, bypassing the interactive template
selection; update the logic that constructs InitFlow::Svm so that when init_flow
is None it calls svm_prompts::prompt_template_init_flow (the same function used
in the interactive branch) and uses its returned TemplateArgs to populate the
SVM init_flow instead of defaulting to FeatureBlockHandler, ensuring
MetaplexTokenMetadata becomes selectable; locate the code creating InitFlow::Svm
and replace the hardcoded TemplateArgs/FeatureBlockHandler fallback with a call
to svm_prompts::prompt_template_init_flow (or propagate the TemplateArgs there)
and handle errors similarly to the interactive branch.

Comment on lines 1101 to 1102
let chain = Chain {
id: 0, //network.id,
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

Chain ID is hardcoded to 0 for SVM networks.

The chain ID is hardcoded to 0 with a commented-out network.id. This prevents indexing multiple SVM chains (e.g., mainnet + devnet) simultaneously since they would collide in the chains HashMap. Consider using a unique identifier for each SVM chain.

🤖 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 1101 - 1102,
The Chain struct is being created with a hardcoded id = 0 which causes
collisions for multiple SVM networks; change the creation of Chain in the block
that builds SVM networks to use a unique identifier (e.g., use the existing
network.id or a composed unique key like (network.name, network.id) or a
generated UUID) instead of 0 and ensure the same identifier is used as the key
when inserting into the chains HashMap so each SVM chain (mainnet, devnet, etc.)
is indexed uniquely.

Comment on lines +28 to +32
max_num_retries: c
.max_num_retries
.filter(|v| *v >= 0)
.map(|v| v as u32)
.unwrap_or(default.max_num_retries),
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

Avoid lossy i64 -> u32 cast for max_num_retries.

Line 31 uses as u32, which silently truncates values above u32::MAX and can produce an unintended retry count.

Suggested fix
             max_num_retries: c
                 .max_num_retries
                 .filter(|v| *v >= 0)
-                .map(|v| v as u32)
+                .and_then(|v| u32::try_from(v).ok())
                 .unwrap_or(default.max_num_retries),
📝 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
max_num_retries: c
.max_num_retries
.filter(|v| *v >= 0)
.map(|v| v as u32)
.unwrap_or(default.max_num_retries),
max_num_retries: c
.max_num_retries
.filter(|v| *v >= 0)
.and_then(|v| u32::try_from(v).ok())
.unwrap_or(default.max_num_retries),
🤖 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/config.rs` around lines 28 - 32, The
current conversion for max_num_retries uses a lossy cast (.map(|v| v as u32))
which silently truncates values > u32::MAX; change the mapping to perform a
fallible conversion and only accept values that fit into u32 (e.g., use
u32::try_from or v.try_into() and .ok() to produce an Option<u32>), keeping the
existing filter(|v| *v >= 0) and falling back to default.max_num_retries when
the conversion fails; update the expression referencing c.max_num_retries /
max_num_retries to use this safe conversion.

// 4 = update authority
const metadataPda = accounts[0];
if (metadataPda === undefined) return;
const mint = accounts[1] ?? "";
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

Avoid persisting blank mint values when account data is incomplete.

Line 48 falls back to "", which writes placeholder identity data. Guard on missing mint instead of storing an empty value.

💡 Proposed fix
-    const mint = accounts[1] ?? "";
+    const mint = accounts[1];
+    if (mint === undefined) return;
📝 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
const mint = accounts[1] ?? "";
const mint = accounts[1];
if (mint === undefined) return;
🤖 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/templates/static/svm_metaplex_template/typescript/src/handlers/TokenMetadataHandlers.ts`
at line 48, The code in TokenMetadataHandlers.ts sets const mint = accounts[1]
?? "" which allows an empty string to be persisted; instead, guard on a missing
mint by checking accounts[1] (or the enclosing handler function that processes
accounts) and return/skip persisting or throw early if it's absent—do not
fallback to "". Locate the mint assignment and replace the unconditional
empty-string fallback with a presence check (e.g., if (!accounts[1]) { /*
skip/return */ }) so downstream code does not write placeholder identity data.

Comment thread packages/envio/index.d.ts
Comment on lines +1066 to +1069
type SvmDecodedFromProgramTable<TInstr> = TInstr extends {
args: infer A;
accounts: infer Acc extends Readonly<Record<string, string>>;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

❓ Verification inconclusive

🌐 Web query:

In TypeScript, is a fixed-key object type like { readonly authority: string }assignable toReadonly<Record<string, string>>, or does that require an explicit string index signature?

💡 Result:

A fixed-key object type like { readonly authority: string } is generally NOT assignable to Readonly<Record<string, string>> in TypeScript, because Readonly<Record<string, string>> implies a string index signature (i.e., any string key is valid and has a string value), and a fixed-key object type lacks that index signature. This “index signature missing” incompatibility is explicitly seen when assigning an object type without an index signature to Record<string,...>. [1][2] You only avoid the need for an index signature when the target type does not require one (e.g., using a narrower Record key type like Record<'authority','string'>) or when you use an explicit assertion/cast. Regarding readonly: TypeScript’s readonly modifier does not materially change whether an index signature is considered missing; index-signature compatibility is the main issue here. [3][4] Example (what you likely want): - If you mean “object has an authority property, and it’s readonly”, use { readonly authority: string } (or Readonly<{ authority: string }>), not Record<string, string>. [5] - If you truly need “any string key maps to string”, then your type must include a string index signature, e.g. type T = Readonly<{ [k: string]: string }>. (Record<string,string> is defined as an object type with string keys and string values.) [5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Print the relevant portion with a bit of context
sed -n '1045,1095p' packages/envio/index.d.ts | nl -ba | sed -n '1,120p'

Repository: enviodev/hyperindex

Length of output: 1960


Relax Record<string, string> constraint in SvmDecodedFromProgramTable to preserve decoded narrowing.

  • accounts: infer Acc extends Readonly<Record<string, string>> can reject fixed-key account object types (no string index signature); e.g. { readonly authority: string } is not assignable to Readonly<Record<string, string>>, so the constrained infer can fail and typed decoded.accounts/decoded.args narrowing may be lost.
  • Confirm whether this infer failure triggers the SvmDecodedInstruction fallback (and/or any other branch) in the full SvmDecodedFromProgramTable definition.
🤖 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/index.d.ts` around lines 1066 - 1069, The current constrained
infer `accounts: infer Acc extends Readonly<Record<string, string>>` rejects
fixed-key account types (e.g. `{ readonly authority: string }`) and can trigger
the fallback to `SvmDecodedInstruction`; to fix this, relax the constraint by
inferring `accounts: infer Acc` (remove `extends Readonly<Record<string,
string>>`) and then use a conditional check like `Acc extends
Readonly<Record<keyof Acc, string>>` (or `Acc extends Record<PropertyKey,
string> ? ...`) in the `SvmDecodedFromProgramTable` definition to preserve
narrowing for fixed-key objects and only fall back to `SvmDecodedInstruction`
when the inferred `Acc` truly does not have string-valued properties; update the
branches that produce `decoded.accounts`/`decoded.args` accordingly so they use
the validated `Acc` and avoid unintended fallback.

Comment on lines +89 to +99
// Metadata account existed before our `start_block`; record the update
// without claiming a `mint` or `createdAtSlot` we don't actually know.
context.TokenMetadataAccount.set({
id: metadataPda,
mint: "",
updateAuthority,
lastUpdatedSlot: event.slot,
updateCount: 1,
createdAtSlot: event.slot,
lastTxSignature: txSig,
});
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

Avoid writing a synthetic createdAtSlot for pre-existing accounts.

This branch explicitly says the real creation slot is unknown, but still persists createdAtSlot: event.slot. That makes historical data incorrect for accounts created before the indexing window.

🤖 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` around
lines 89 - 99, The code in the pre-existing-metadata branch incorrectly writes a
synthetic createdAtSlot (using event.slot) even though the true creation slot is
unknown; update the TokenMetadataAccount.set call (the branch handling "Metadata
account existed before our `start_block`") to omit or set createdAtSlot to
null/undefined instead of assigning event.slot, and ensure you do not overwrite
any existing createdAtSlot on the record—only update fields you actually know
(id/ updateAuthority/ lastUpdatedSlot/ updateCount/ lastTxSignature) or
explicitly preserve the existing createdAtSlot if one exists in the DB schema
for TokenMetadataAccount.

Comment on lines +14 to +18
process.env.ENVIO_CONFIG = "config.test.yaml";

import { describe, it, expect } from "vitest";
import { createTestIndexer } from "envio";

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

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate ENVIO_CONFIG reads in envio runtime code.
rg -n -C3 'ENVIO_CONFIG' packages/envio

# 2) Locate createTestIndexer definition and nearby initialization behavior.
rg -n -C6 '\bcreateTestIndexer\b' packages/envio

# Expected confirmation:
# - If ENVIO_CONFIG is read at module top-level in the import path of createTestIndexer,
#   the current test ordering is incorrect.

Repository: enviodev/hyperindex

Length of output: 1979


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant test file ordering
echo "=== scenarios/svm_metaplex_demo/src/indexer.test.ts (top) ==="
sed -n '1,120p' scenarios/svm_metaplex_demo/src/indexer.test.ts

# Find where ENVIO_CONFIG is consumed in the repo (focus on packages/envio and envio-related code)
echo "=== Searching for ENVIO_CONFIG usage ==="
rg -n -S "ENVIO_CONFIG" packages scenarios/svm_metaplex_demo || true

# Find createTestIndexer implementation and its import-time behavior
echo "=== Searching for createTestIndexer implementation ==="
rg -n -S "createTestIndexer" packages || true

# If createTestIndexer is in a TypeScript file under packages/envio, print those files' top-level sections
echo "=== Files mentioning createTestIndexer ==="
files=$(rg -l -S "createTestIndexer" packages || true)
echo "$files" | head -n 50

# For each matched file, show a small window around any ENVIO_CONFIG usage (if any) and near module-level initialization
# (Limit to first 20 files to keep output bounded)
i=0
while read -r f; do
  [ -z "$f" ] && continue
  i=$((i+1))
  if [ "$i" -gt 20 ]; then break; fi
  echo "--- $f (ENVIO_CONFIG context) ---"
  rg -n -C3 "ENVIO_CONFIG|process\\.env" "$f" || true
done < <(echo "$files" | head -n 20)

Repository: enviodev/hyperindex

Length of output: 1960


ENVIO_CONFIG may be read before the assignment due to ESM static imports

In scenarios/svm_metaplex_demo/src/indexer.test.ts (around lines 14-18), process.env.ENVIO_CONFIG = "config.test.yaml"; runs before static imports of createTestIndexer from envio. If envio reads process.env.ENVIO_CONFIG during module initialization, this override won’t take effect reliably (ESM static import evaluation happens before the importing module’s top-level code).

💡 Safer pattern
-process.env.ENVIO_CONFIG = "config.test.yaml";
 import { describe, it, expect } from "vitest";
-import { createTestIndexer } from "envio";
 
 ...
     async () => {
+      process.env.ENVIO_CONFIG = "config.test.yaml";
+      const { createTestIndexer } = await import("envio");
       const indexer = createTestIndexer();
       const result = await indexer.process({ chains: { 0: {} } });
🤖 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 14 - 18, The
test sets process.env.ENVIO_CONFIG after a static import of createTestIndexer,
so envio may read the env var during its module initialization; change
indexer.test.ts to assign process.env.ENVIO_CONFIG = "config.test.yaml" before
importing envio by replacing the static import of createTestIndexer with a
dynamic import (e.g., set the env var at top-level, then await import("envio")
and extract createTestIndexer), ensuring createTestIndexer is obtained after the
env var is set so envio sees the override.

maxNumInstructions: 200,
}
let resp = await client.get(~query)
let first = resp.data.instructions->Array.getUnsafe(0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Check array length before unsafe access.

Array.getUnsafe(0) on line 22 will panic if resp.data.instructions is empty, before the assertion on line 26 can verify the array is non-empty. This produces a less helpful error message.

🛡️ Proposed fix to check length first
     let resp = await client.get(~query)
+    t.expect(resp.data.instructions->Array.length > 0).toBe(true)
     let first = resp.data.instructions->Array.getUnsafe(0)
 
     let summary = {
       "heightLooksRecent": height > 300_000_000,
-      "hasInstructions": resp.data.instructions->Array.length > 0,
       "firstProgramId": first.programId,
       "firstDataIsHex": first.data->String.startsWith("0x"),
     }
     t.expect(summary).toEqual({
       "heightLooksRecent": true,
-      "hasInstructions": true,
       "firstProgramId": tokenMetadataProgram,
       "firstDataIsHex": true,
     })
🤖 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/HyperSyncSolanaClient_test.res` at line 22,
resp.data.instructions is accessed with Array.getUnsafe(0) (assigned to first)
before confirming the array is non-empty, which can panic; update the code to
check the length or use a safe getter first (e.g., verify
Array.length(resp.data.instructions) > 0 or use Array.get/Belt.Array.get to get
an option) and only assign to first after the presence check (or pattern-match
the option and assert), referencing resp.data.instructions, Array.getUnsafe, and
the first binding.

Comment on lines +42 to +55
Async.it_skip("Returns the name of the source including sanitized rpc url", async t => {
let source = RpcSource.make({
url: `https://eth.rpc.hypersync.xyz/${testApiToken}`,
chain: MockConfig.chain1337,
eventRouter: EventRouter.empty(),
sourceFor: Sync,
syncConfig: EvmChain.getSyncConfig({}),
allEventParams: [],
lowercaseAddresses: false,
})
let height = await source.getHeightOrThrow()
t.expect(height > 21994218).toBe(true)
t.expect(height < 30000000).toBe(true)
})
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

Document why getHeightOrThrow test is skipped.

Lines 141 and 649 reference "see comment on getHeightOrThrow test above" as the reason for skipping other tests, but there's no explanatory comment on this test explaining the skip rationale.

Add a comment documenting why this integration test is skipped and whether it should be re-enabled.

🤖 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 42 - 55, Add an
explanatory comment above the Async.it_skip test (the one calling RpcSource.make
and getHeightOrThrow) that documents why the integration test is skipped: state
whether it flakily depends on external RPC availability, rate limits or test API
token usage, mention any known failures/PRs or environment requirements, and
indicate what conditions would allow re-enabling (e.g., stable mock RPC,
recorded fixtures, or CI credentials). Reference the test identifiers
Async.it_skip, RpcSource.make, and getHeightOrThrow so reviewers can find the
test and follow the re-enable instructions.

Comment on lines +140 to +142
// DO NOT MERGE WITH THESE TESTS SKIPPED.
// TEMP: skipped - see comment on `getHeightOrThrow` test above.
Async.it_skip("Queries transaction fields from raw JSON (with real RPC)", async t => {
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 or address "DO NOT MERGE" comments before merging.

The explicit "DO NOT MERGE WITH THESE TESTS SKIPPED" comments on lines 140 and 648 indicate these test skips are temporary. Skipping integration tests reduces coverage for critical RPC functionality.

Either re-enable these tests or provide a tracking issue and timeline for re-enabling them.

Also applies to: 648-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 140 - 142,
Remove or resolve the temporary "DO NOT MERGE" skip comments around the skipped
integration tests: either re-enable the tests by changing Async.it_skip("Queries
transaction fields from raw JSON (with real RPC)", ...) to Async.it(...) and
remove the DO NOT MERGE note, or if they must remain skipped, replace the
comment with a clear TODO that includes a tracking issue ID and an expected
re-enable date; also apply the same fix for the other skipped block referenced
(around the getHeightOrThrow test and lines 648-650). Ensure the updated comment
or TODO references the test identifiers (e.g., Async.it_skip for the "Queries
transaction fields..." test and the getHeightOrThrow test) so reviewers can find
and validate the change.

- name: UpdateMetadataAccountV2
discriminator: "0x0f"
field_selection:
transaction_fields: true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Doing this includes all of the transaction fields. In the future if you put an array, you can select specific transaction fields (but this isn't implemented yet - it is all or nothing).

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