Skip to content

Subgraph mode: compatibility fixes from real subgraphs (wildcard data sources, register-pass scope, struct params, encode, …) - #1732

Closed
moose-code wants to merge 49 commits into
claude/hyperindex-subgraph-mode-bs67pqfrom
moose/subgraph-compat-fixes
Closed

moose-code wants to merge 49 commits into
claude/hyperindex-subgraph-mode-bs67pqfrom
moose/subgraph-compat-fixes

Conversation

@moose-code

@moose-code moose-code commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Subgraph-mode compatibility fixes, found by running real public subgraphs on envio@3.14.0-subgraph. This targets #1626's branch. It is a draft for review and not for merge during the hackathon.

There are 42 commits, one per fix, and each has a test under packages/envio-tests/test/ (or cargo test). Three commits are fixer2's patches from #1735, with their authorship kept. Checks at 787e30446: cargo fmt, clippy -D warnings, cargo test --lib (1008), pnpm check-graph-ts, all 63 test/Subgraph* files (175 tests) and the replay stress test.

Silent wrong data

Commit Fix
59888d95c A data source without source.address is a wildcard, as on graph-node. Before this, ENS Resolver's 11 handlers ran 0 times
672686641 save() merges onto the stored entity, as on graph-node: an unset field keeps its value and an explicit null clears it. Before this it replaced the entity
df61e8e29 A new entity's unset fields are written as null
6d3e2d8a7 Bytes.toString() is UTF-8. This fixes Aave's "POOL" id and bytes32 symbols
e25774fc4 Transaction fields are selected for mappings that live outside the project dir. Before this, thales had a null tx.hash
856ca7417 Loading a field derived from a list field is refused instead of silently returning empty
15169dbda event.transactionLogIndex = log index, as on graph-node

Register pass (dataSource.create) and RPC

Commit Fix
053d129d9 Runs only for handlers that can reach a create (oxc call graph), and identical in-flight calls are shared
131420ef8, 0fc590756, 0b2a98a54, fc9f88741, e7b3d2dec The pass has its own versioned LRU store that keeps registrations made before a throw (Aave's 0 aTokens). once handlers are primed into it (Venus). A pass waits for an earlier creator of the same entity. Call answers are reused in the handler pass. ipfs.map counts as a create
966672bae, e81bd98cd, 6d57bc840, a90d060c6 ENVIO_SUBGRAPH_RPC accepts bare URL strings. Rate-limit codes are treated as transient. The deadline starts once a request holds a gate slot, so a queued backlog no longer double-sends. Progress is logged while a batch waits on calls
cced3554e, fc167ee6f Polling timestamp ranges are capped and fetched in parallel. Each data source stops at its own endBlock
c8992bbd5, efc31ef91 ENSRainbow: ENVIO_SUBGRAPH_ENS_RAINBOW_URL, or off. A dead service answers null with one warning

AssemblyScript / graph-ts runtime

c6b9563e4 and 2a6d95fb3 (struct params and tuple arrays); 3af3e1645 (ethereum.encode); e8f121b7f (public ethereum.* constructors); 665879aef (i64 arithmetic and Date); 2c144715e (DataView u64 and matchstick-as imports); 0dba8aa91 (re-exported handlers); 9477d8566 (TS enum, and getters through as casts); ec9fb0dfc (a sibling as cast reads a copy); 3c058090e (the AS pass runs on mapping libraries in node_modules); 787e30446 (BigInt.toU32/toU64 of negatives); bfd4b0ba1 (no log.* lines from the register pass).

Codegen and Envio Cloud

cc38dab80 (hoisted and pre-oclif graph-cli); a1fb8b671 (the generated-dir lookup follows re-exports); fe13904a8 (codegen runs on the manifest given with --config); 9e34722a2 (envio codegen refuses like start when it can't build); 9918a53e2 (a startup line says where to query); eb468ab82: the Cloud operator ships an empty generated/, which the runtime now rebuilds, and it warns about a stale committed one.

Before → after

Unmodified repos, run in a block window.

  • ENS: died at 3.33M before. Now it syncs, and AddrChanged 2,212 equals the network deployment's 2,212, with identical ids.
  • Uniswap v3: hung at 100% CPU before. It now syncs in 22 s, with counts identical to the patched run.
  • Aave v3: needed 4 patches plus a mapping edit before. Unmodified, it now registers Pool, PoolConfigurator and aTokens.
  • Nouns, Notional, ssv, p2pme, autonolas, uma, algebra, ekubo, thales, uniswap-v4: each crashed before and now runs.
  • Uniswap v2: an exact match with the official network deployment (15/15 types, 412,334 ids).

Not fixed

  • BSC wrong transaction: upstream HyperSync data. Tx indices are off by one in blocks ~30.5M–31.8M, and that range needs a reindex.
  • The handler-pass preload still makes speculative calls.
  • Entities written only by polling block handlers are invisible to the register store.
  • Timestamp loses microseconds.
  • Translator and storage items are in Subgraph mode: translator and storage compatibility fixes #1735.

🤖 Generated with Claude Code

moose-code and others added 7 commits October 14, 2026 19:23
graph-node matches a data source with no source.address against every
contract's logs; ENS's Resolver is one data source for every resolver
anyone deploys. It was translated to an address-less contract that
nothing ever registers an address for, so all of its handlers silently
ran zero times. Its handlers (and their register pass) now register as
wildcard; a log matching an addressed data source too runs both, as on
graph-node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The register pass reran every handler of every source at fetch time
whenever the manifest had templates, against a store that reads null,
and with it every contract call the handler makes. A pool's swap handler
creates nothing, so the rerun only cost RPC: Venus and Aave spent most of
their budget there.

The addon now follows each handler through the mapping's module graph
(top-level functions, classes, variables, relative imports and
re-exports, into generated/templates.ts) and keeps the pass only for the
ones that can reach DataSourceTemplate.create. Whatever it can't see
counts as a create, so a wrong answer costs a pass, never a missed
registration. ENVIO_SUBGRAPH_REGISTER_ALL_HANDLERS=1 restores the old
behaviour.

Concurrent register passes now share one in-flight request per op, so a
block's factory events read the factory once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
envio's decoder keys a tuple by its components' names when the ABI names
them (Nouns' NounCreated seed, SSV's ValidatorAdded cluster); the shim
read it by position, so every field was undefined and, every parameter
being converted together, even the struct's siblings threw. A keyed
tuple is now read in ABI order, for a struct and for a struct array.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs promise `[<url>|{...}]`, a JSON array mixing both, but the array
deserialized straight into RpcSelection::List(Vec<Rpc>), so
`["https://a","https://b"]` was refused before indexing started (Nouns,
CryptoPunks, Notional, Venus all hit it). A string in the array is now
an entry with only its url, the same as a bare URL on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was a refusal, though the compat doc lists it as supported, and Nouns
hashes every proposal's content with it (calcEncodedProposalHash), linking
candidates to proposals through that hash. It is graph-node's
ethabi::encode(&[token]), Solidity's abi.encode of one value: the ABI
type is derived from the value's kinds and encoded with viem. A token
carries no integer width and every integer takes a full word, so 256 bits
encodes the same bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s' constructors

A mapping can build these itself: Notional's polling block handler
synthesizes an event with new ethereum.Event(address, 0, 0, null, block,
new ethereum.Transaction(...), [], null). The shim's Event constructor was
its own internal (raw, kind), so the event came back empty, and Block and
Transaction had no constructor at all. They now take graph-ts' public
arguments; the runtime builds its events through makeEvent, and its
blank chain objects without arguments. Event fields are settable, as
graph-ts' are.

conformance.ts dropped construct signatures with the rest of a class's
statics, so it never saw this. It now compares every public constructor's
arity with graph-ts' and lists the four only the shim constructs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The register pass reruns a mapping against a store that reads null, only
to collect dataSource.create() calls, but its log.* calls went to the
real logger. Notional printed 190+ 'Token not found' errors from it that
graph-node never would. The compat doc already says writes and logs are
no-ops there; now logs are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

moose-code and others added 2 commits October 14, 2026 19:44
ens.nameByHash asked a hardcoded api.ensrainbow.io, which is gone (its
Railway app answers 404 'Application not found'), as is every other
hosted instance, so the ENS subgraph could not run at all without
patching envio.

- ENVIO_SUBGRAPH_ENS_RAINBOW_URL names the heal endpoint; 'off' skips
  lookups.
- A 404 is a miss only when it is ENSRainbow's own 'Label not found'.
- A service that keeps failing (5 attempts, or a foreign 404) answers
  null, as graph-node does without a rainbow table, for 5 minutes, with
  one warning, and those nulls are not cached.
- At most 32 lookups are in flight: a registry backfill asks a million.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shim returned hex, so Aave's event.params.id.toString() == 'POOL'
never held and its Pool and PoolConfigurator were silently never
created, and a bytes32 ERC20 symbol fallback (MKR) was stored as
0x4d4b52... in Uniswap. It now decodes UTF-8 lossily and trims the
trailing NULs of a fixed-size buffer, as graph-node does.
Value.displayData() keeps hex for bytes, as graph-ts' does.

The IPFS test asserted the old hex for ipfs.cat(...).toString(); it now
expects the file's text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code and others added 17 commits October 14, 2026 19:50
The register pass ran creating handlers at fetch time against a store
that read null for everything, which lost Aave's tokens two ways:
handleReserveInitialized loads an entity an earlier event saved before it
calls ATokenContract.create(), so the load was null, the mapping threw
first, and no aToken address was ever registered (every aToken event
silently skipped); and handleProxyCreated creates, then loads and throws
on null, which was fatal because something was already registered.

Register-pass writes now go to a store of the pass's own, versioned by
the writing event's position; a read sees the latest write from a
strictly earlier event on its chain (200k entities, then dropped back to
null). A round's writes are kept only once it completes, so replays
don't leak them. A mapping that throws in the pass keeps what it
registered before the throw; the handler pass surfaces real errors.

The same store is what lets Venus's getOrCreateMarket and Uniswap's
Token.load find what an earlier factory event saved, instead of making
every contract call again at every event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subgraph in a yarn or npm workspace has its graph-cli hoisted to the
workspace root. envio looked only in the package's own node_modules and
said graph-cli wasn't installed (Polymarket, Venus, PancakeSwap, and in
the sweep Multyr, Algebra, Autonolas, UMA). It now walks up the way
Node resolution does, and puts every ancestor's node_modules/.bin on the
codegen script's PATH.

graph-cli 0.2x predates oclif and has no dist/commands/ to run
in-process (GMX, PancakeSwap); its gluegun bin is spawned instead, with
the same arguments and heap.

fromSubgraph gained projectDir, to put a project inside a workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two things asc accepts that Node didn't, from the sweep:
- AssemblyScript's DataView has setUint64/getUint64 (and the Int64
  pair) over u64/i64; JavaScript's only setBigUint64 and friends. Ekubo
  builds every pool id that way and failed on its first event. They are
  now aliased onto DataView.prototype when missing.
- matchstick-as re-exports graph-ts' log, and Beefy's mappings import it
  from there; it now resolves to the shim like graph-ts.

typeConversion.bytesToString trims trailing NULs like Bytes.toString().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilure

The real api.ensrainbow.io fails the TLS handshake (Railway serves its own
certificate for the name), which reads as 'fetch failed' and was retried
like a network blip: an ENS backfill started thousands of lookups at once,
each backing off five times, and logged 15k warnings before the first one
gave up. A certificate or DNS failure now takes the service for down at
once, and lookups already backing off stop as soon as one has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… parallel

Requested blocks closer than 10,000 apart are fetched as one HyperSync
range, which is paged one response at a time. A polling handler every
1,600 blocks merged every invocation of a batch into one range: Notional's
1,978 timestamps were one sequential scan over 3.2M blocks, each awaited
for about 38 s. A merged range now spans at most 20,000 blocks, and up to
8 ranges are fetched at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ler pass

A creating handler's calls ran twice: once in the fetch-time register
pass, whose answers stay in its own memo, and again in the handler pass
through the eth_call effect. Uniswap v3's handlePoolCreated paid every
token call twice. The effect now answers from the register pass's memo
when it holds the same call at the same block, and envio's effect cache
persists it from there as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only JSON-RPC -32005 counted as a rate limit by code. blastapi answers
-32097, sometimes worded 'exceeded its compute units per second
capacity' with no 'rate limit' in it, and that failure stopped the
indexer (Aave, Venus, Uniswap v3 field tests). -32097 and Alchemy's
-32090 now count too, as do 'capacity' and 'limit exceeded' wordings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s register passes

Venus saves its Comptroller in a once block handler, and getOrCreateMarket
reads it before it creates the VToken template. Block handlers get no
register pass, so the read was null, the mapping threw before the create,
and no market was ever indexed: the bounded field-test window registered
nothing but the Comptroller. A once handler now runs in register mode, at
its block and ahead of every log there, before the first register pass of
its chain, so its writes are in the register store.

Register host ops also get the real chain id now; a block timestamp in
the register pass asked chain 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A manifest's mapping file can re-export handlers written in other files
(SSV's src/mapping.ts does). The generated event class a handler declares
is exported by the module the handler is written in, and the runtime only
looked at the manifest's file, so a re-exported handler got a bare
ethereum.Event: its params lost graph codegen's getters, a struct read
through them was undefined, and saving it failed with 'Cannot convert
undefined to a BigInt'. The classes are now read from every project
module the load hook compiled, keyed by the handler function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es an i64

The shim holds an i64 as a bigint. AssemblyScript widens an integer
literal to i64 next to one, so p2p.me's timestamp.toI64() * 1000 is i64
arithmetic, but JavaScript refuses to mix a bigint with a number and the
first event failed. +, -, * and % now widen an integer to bigint when the
other side is one, and an i64 division with an integer on the other side
stays an i64 division.

AssemblyScript's Date takes and sets its time as an i64, which
JavaScript's refuses as a bigint: the global Date accepts one in subgraph
mode, and gains AssemblyScript's Date.fromString.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d call reuse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated code's directory is read off the mappings' imports of
schema/templates, but only the manifest's own files were read. Uniswap
v4's only re-export handlers written next to them, so envio guessed
generated/, codegen wrote there, and the mappings' ../types/schema never
existed. The scan now follows relative imports breadth first from the
manifest's files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ipfs.map as a create

- AssemblyScript takes a decorator on a top-level function or variable
  (Beefy writes @inline above its helpers), which TypeScript's grammar
  refuses: every Beefy mapping failed to load. Such decorators are
  blanked to spaces before parsing, keeping every source-map offset.
- The register store evicts its least recently used entity past 50k
  (4 versions each) instead of clearing everything at 200k, so a
  factory's own entity stays and memory stays bounded.
- ipfs.map calls back into a mapping function named by a string, which
  the create analysis can't follow, so a handler using ipfs counts as
  one that may create.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On graph-node, Account.tokens @derivedFrom(field: "owners") over
Token.owners: [Account!]! holds every token whose list contains the
account. envio's lookups only match a column equal to a value, so
store.loadRelated silently answered an empty list. It now refuses, naming
the field, rather than answering wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s class

- The transformer needs enum evaluation from the semantic pass to compile
  an enum, and panicked without it — inside the addon, which aborted the
  whole indexer (Uniswap v4's ChainId enum).
- graph-cli 0.2x's tuple getters and old entity loads cast with
  `x as Foo` where newer codegen writes changetype<Foo>(x): the same
  reinterpret, which stripped as a type left a bare tuple without the
  generated getters (GMX's eventData.addressItems was undefined). Such a
  cast now retags onto the class like changetype, except that a value
  already of the class, or a subclass, keeps its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graph codegen reads a struct array as x.toTupleArray<Struct>(), and in
AssemblyScript each element is that Struct, with its getters. The type
argument is erased in JavaScript, so the elements were bare tuples and
every getter on them read undefined: GMX's event data lookups never
matched and its MarketTokenTemplate.create got null. The transform now
wraps such a call (and toTupleMatrix) in a helper that retags each tuple
onto the class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code and others added 5 commits October 14, 2026 20:55
…m the queue

At most 16 RPC requests are in flight and the rest queue in the gate.
untilAnswered wrapped every attempt in the request deadline, queue wait
included, so a backlog timed out before it was ever sent, was asked
again at the back of the queue, and was eventually sent twice: Venus's
register pass logged 46k timeouts for 3.3k requests actually sent, and
made no progress (retest-01). The gate already gives each request the
deadline once it holds a slot, so gated attempts (contract calls,
balances, code, RPC block reads, ENS lookups) no longer get a second one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ph-node does

graph-node returns the log's index in its block for transactionLogIndex
(chain/ethereum/src/trigger.rs, since b95c6953): Geth never set the
per-transaction index, and subgraphs read it only to tell logs apart. Aave v3
reads it at 54 call sites to build entity ids, and was refused on the first.

The pinned unsupported-refusal text moves to event.receipt.logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graph-node runs none of a data source's handlers past its endBlock
(has_expired), while the other data sources on its network and every template
instance keep indexing. envio ends whole chains, so the runtime drops a static
data source's events after its endBlock (event and register-pass handlers) and
bounds its polling block handlers with _lte.

Used by autonolas predict-polymarket (v1 exchange retired at 86750000 beside
open-ended sources) and Beefy (a one-block ConfigBinder source).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The translator now records the manifest's file name (manifestFile) when
--config/ENVIO_CONFIG names one other than subgraph.yaml (Uniswap v3's
v3-subgraph.yaml). graph codegen and the dev type check ran on subgraph.yaml
regardless; they now run on that file, defaulting to subgraph.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… entity

A batch's register passes run concurrently, and a pass's writes reach the
register store only when it completes. So every Comptroller event in
Venus's first batch missed the Market an earlier event of the batch was
still creating, and repeated its ~20 creation calls at its own block:
9.3k distinct (call, block) keys before the first event (retest-02).

A pass that misses an entity an earlier, still-running pass of its chain
also missed now waits for the earliest such pass once, then reads again:
the per-entity order graph-node's sequential processing gives.
Independent events stay concurrent, and a key nobody ends up writing
costs one wait. Waits only go to earlier passes, so they can't cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code added a commit that referenced this pull request Oct 14, 2026
flaunch's BidWall1 data source lists `source.addresses:` and was refused as an
unknown manifest field. graph-node has no such field: its ethereum `Source`
(graph/src/data/subgraph/mod.rs) carries `address` only and doesn't deny
unknown keys, so the list is dropped and the data source has no address, which
graph-node indexes as a wildcard over every emitter. graph-cli would reject the
key ("Unexpected key in map"), but it skips manifest validation entirely when
the manifest has a file data source, as flaunch's does.

The key is now accepted and, like graph-node, not used: the data source gets
no address, so the same events are indexed as on The Graph. (Wildcard data
sources run through #1732's runtime change.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code and others added 2 commits October 14, 2026 21:03
…dules

A mapping library is AssemblyScript too, and the commonest ones don't
load as TypeScript: OpenZeppelin's subgraphs pull in
@amxx/graphprotocol-utils, which declares 'class string extends String'
in a namespace (mento, aavegotchi-core, forta); @protofire/subgraph-toolkit
puts @inline on top-level functions; some files import the same graph-ts
name twice (gammaswap). envio ran its AssemblyScript pass on the project's
own files only, and Node refuses even to strip types under node_modules.

The load hook now passes every .ts module under node_modules except
envio's own package and declaration files through the AssemblyScript
pass, and the pass turns a class named after a built-in type into a
class expression bound to a constant of that name, which TypeScript's
grammar takes and reads the same from outside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port

Envio Cloud drops a project's generated/ and recreates it empty before
the build, and envio skipped graph codegen whenever the directory
existed, so the pod crash-looped on the mappings' ../generated/schema
imports. The runtime now checks the generated files the mappings import
(followed through re-exports), and builds the directory when any is
missing, empty included; the codegen preflight looks for schema.ts, not
the directory. When the generated code is complete but graph-cli can't
be found to rebuild it, startup warns that it runs as committed and may
be stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them

graph-ts reads a BigInt's minimal two's-complement little-endian bytes,
zero-padded, so -1 (the byte 0xff) is 255 and only a value whose bytes
don't fit overflows. The shim refused every negative to toU32 (Clober's
(tick.toU32() << 8) >> 8 failed) and returned toU64 unchanged, negative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moose-code

Copy link
Copy Markdown
Member Author

Pushed 34 more commits (head eb468ab82). The ones that matter most:

  • save() merges onto the stored entity, as on graph-node: an unset field keeps its stored value, and an explicit null clears it (6726866). Before this, save() silently replaced the entity.
  • A new entity's unset fields are stored as null (df61e8e), and an as cast to a sibling class reads a copy (ec9fb0d).
  • event.transactionLogIndex returns the log index, as graph-node does (15169db). Per-source endBlock (fc167ee). Codegen runs on the manifest given with --config (fe13904).
  • Register passes wait for an earlier pass that creates the same entity (0b2a98a). An RPC attempt's deadline is timed from when it gets a gate slot (6d57bc8).
  • The AssemblyScript pass now runs on mapping libraries in node_modules (3c05809). Mapping files outside the project dir get their transaction fields selected (e25774f).
  • Envio Cloud: the operator ships an empty generated/. The runtime now builds generated code when any file the mappings import is missing (eb468ab).

All 59 Subgraph test files and the replay stress test pass. Not for merge during the hackathon. @DZakh, review when you have time.

🤖 Generated with Claude Code

moose-code and others added 4 commits October 14, 2026 21:28
… its topic

envio's decoder hands every bytesN over as the 32-byte word it was
encoded in, topic or body, so ENS InterfaceChanged.interfaceID (indexed
bytes4) was stored as 0x7ba18ba1 padded to 32 bytes. graph-node keeps
the first N bytes. An indexed string, bytes, array or tuple is only its
topic hash, which graph-node hands over as a 32-byte FixedBytes and graph
codegen reads with toBytes(); the shim typed it as the declared type, so
the generated getter of an indexed string threw "Ethereum value is not
bytes".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… after it

Dolomite's AmmPair template was never registered, so every AMM event was
lost without a word. Its admin handler saves each Token after a contract
call; the factory's handleNewPair, at a later block, loads the pair's
Token (`as Token`) before PairTemplate.create(). The register passes of
a batch run concurrently, so the factory's pass read the Token while the
admin's pass still waited on its call, got null, threw before the create,
and the throw is forgiven in the register pass. The wait added for Venus
covers only an earlier pass that has already missed the same entity.

A read that misses while an earlier pass of its chain is still running
now marks the pass provisional: when it ends, thrown or not, it waits
until every earlier pass of its chain has settled and runs again, its
first run's writes dropped (a create it made stands). The import analysis
was right all along: it marks handleNewPair as creating.

Dolomite (arbitrum, 28,220,369 to 28,300,000): 3 AmmPair addresses
registered (was 0); AmmMint 1, AmmTrade 5, AmmLiquidityPosition 1 and the
pairs' reserves equal the network's at 28,300,000 (were 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…istered

Only the register pass registers a template address, at fetch time; the
handler pass's create() is a no-op. When the two disagree — the register
pass threw on an entity that only a handler without a register pass
saved, or there is no register pass at all, as for a polling block
handler — the template's events were lost without a word (Dolomite's
AmmPair). The handler pass now checks its creates against what the
register passes registered and warns once per address, naming the data
source, template, address and block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dolomite generates into src/types, where templates.ts sits beside a
templates/ directory, and imports its template under an alias because a
schema entity has the same name. The analysis follows that import to the
create (Dolomite's lost AmmPair events were a register-pass race, fixed
separately); this pins the shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code and others added 2 commits October 14, 2026 22:43
Uniswap v2 ran 4.3x slower than on envio@3.14.0-subgraph, CPU-bound
(retest-06). A profile put 55% of the time in BigDecimal: normalize()
padded every coefficient to 34 digits and stripped the zeros off again
one bigint division at a time; powers of ten were recomputed with
10n ** n; the bit length came from a binary string; division appended its
100 digits one division each; and every value eagerly built the
BigNumber it carries, from a printed string. exponentToBigDecimal's
times(10) loop alone was 40% of the run.

Same values, by different means: a coefficient of 34 digits or fewer only
loses its own trailing zeros; powers of ten come from a table; a
positive's digit count starts from log10; division is one division of
num * 10 ** extra (an early stop shows as trailing zeros); comparisons
work on the coefficients; the BigNumber is made on first use and carries
its decimal, so an entity read back from memory isn't printed and parsed
again; repeated literals parse once. The second normalization of each
result stays: graph-node normalizes a value again whenever it crosses
back from AssemblyScript into its host (FromAscObj -> BigDecimal::new),
and that pass changes a negative the first left at 35 digits.

The bigdecimal 0.1.2 vector generator now models that round trip and
adds 511 vectors for the shapes a fast path could get wrong (powers of
ten, trailing zeros, 33-36 digit results, negatives at a power of two,
carries, exact and repeating division, zeros): 911 vectors, all equal.
A differential run against the previous code agreed on 1.6M operations.

Uniswap v2 window 10,000,834 to 10,250,000 (unmodified repo, same box
and proxy): 383 ev/s before (150,690 events in 420 s, not finished),
1,654 ev/s after (window of 411,332 events done in 276 s); published
3.14.0-subgraph 1,731 ev/s, 273 s. envio subgraph diff vs the network
(GmSczqdC) at 10,250,000: 15/15 types, 427,478 ids, 1,139 sampled, 0
field mismatches (the published build: 720).

This changes #1626's own graph-node-decimal.ts (F21, 8566fa6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by ==

AssemblyScript's Array#includes, indexOf and lastIndexOf compare each
element with ==, which for BigInt, BigDecimal and Bytes/Address is
graph-ts' overloaded value comparison; JavaScript's compare identity.
ENS's handleMulticoinAddrChanged guards its push with
`if (!coinTypes.includes(coinType))`, so every setAddr on the multicoin
resolver appended another "60": 473 of 3,302 resolvers with coinTypes
held duplicates by block 9,000,000 (network: none). Found by verifying
E1 with envio subgraph diff.

The AssemblyScript pass routes those three calls (one or two arguments,
no spread, not optional or on super) through the operators helper, which
searches an array for a value with an == overload by that overload, with
AssemblyScript's fromIndex rules, and leaves strings, typed arrays and
primitive searches to JavaScript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@moose-code

Copy link
Copy Markdown
Member Author

6 more commits (head 45aff07d1), each with a test:

  • 6da34bfa7: a bytesN event param is N bytes; an indexed dynamic param (string/bytes/array/tuple) is its 32-byte topic.
  • 98a418657: a register pass that misses while an earlier one is still running now waits and re-runs. Dolomite's AmmPair template went from 0 to 3 addresses registered, and AMM entities and reserves equal the network.
  • ceba45e8e: warns once when a handler creates a template that no register pass registered, so those events are no longer lost silently.
  • 1754f67d9: test only (Dolomite's import layout in the create analysis).
  • 0e588a5b8: cheap graph-node BigDecimal rounding, with the same values. @DZakh, this one touches Envio Subgraph (experimental) #1626's own graph-node-decimal.ts. An unmodified Uniswap v2 window goes from 383 to 1,654 ev/s (published 3.14.0: 1,731). The diff at block 10.25M is 427,478 ids with 0 field mismatches (published has 720).
  • 45aff07d1: includes/indexOf/lastIndexOf compare BigInt/BigDecimal/Bytes by value, as AssemblyScript does. This was duplicating ENS coinTypes in 473 of 3,302 resolvers.

Checks: 67 test/Subgraph* files (183 tests) pass, cargo test --lib 1010, plus fmt and lint. Re-runs: Aave v3 exits 0 (44/44 types), Klima exits 0 (9/9). ENS genesis → 9.0M: Resolver diff against the network is 77,237 ids with 0 mismatches.

🤖 Generated with Claude Code

moose-code added a commit that referenced this pull request Oct 14, 2026
…ntegration

The only commit not already merged from fixer3's local branch is 45aff07 (E5 includes/indexOf/lastIndexOf by ==); it sits beside sg-tail-b's as-casts and sg-blocks' pow in assemblyscript.ts/.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rsync

A fresh agent signed in to Envio Cloud can mint its HyperSync token with one
command instead of a second sign-in at envio.dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moose-code added a commit that referenced this pull request Oct 15, 2026
…ntegration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DZakh

DZakh commented Oct 22, 2026

Copy link
Copy Markdown
Member

Merged

@DZakh DZakh closed this Oct 22, 2026

This branch was successfully deployed

1 active deployment
internal — 3dae83df Deployed Oct 15, 2026 by moose-code via authorize #3738
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