Problem
CoinType (enssdk) is a branded number — the invariant is that every real coinType fits in Number.MAX_SAFE_INTEGER (bigintToCoinType does Number(v) and throws above 2^53).
But the indexed schema stores coinType as t.bigint(), which Ponder persists as numeric(78) and hydrates as a JS bigint. The result is bigint ⇄ number conversions sprinkled across every read/write boundary:
- Write:
BigInt(coinType) — StandaloneReverseRegistrar handler, resolver-db-helpers, resolution operations.
- Read:
bigintToCoinType(row.coinType) — resolver-records, make-records-response, execute-operations, and Account.nameReferences's resolver (find-name-references-resolver).
These conversions are pure friction and an easy thing to get wrong — coinType should be CoinType the whole way down, including the indexed schema.
Fix
There's a precedent in the same schema file: chainId is the same kind of value (a number that overflows Postgres int4 but fits MAX_SAFE_INTEGER) and is typed correctly:
chainId: t.int8({ mode: "number" }).notNull().$type<ChainId>(),
Type coinType identically:
// instead of: coinType: t.bigint().notNull(),
coinType: t.int8({ mode: "number" }).notNull().$type<CoinType>(),
int4 genuinely overflows (DEFAULT_EVM_COIN_TYPE = 0x80000000 is int4_max + 1 — presumably why bigint was chosen). int8 holds up to 2^63, comfortably above the 2^53 CoinType invariant. numeric(78) is for uint256 amounts — overkill for a coinType.
- Once the columns hydrate as typed
number, every BigInt(coinType) / bigintToCoinType(...) at these boundaries can be dropped.
Scope
- Columns (
packages/ensdb-sdk/src/ensindexer-abstract/):
protocol-acceleration.schema.ts — reverseNameRecord.coinType (L35), resolverAddressRecord.coinType (L204)
subgraph.schema.ts (L523) — legacy Subgraph path; this one serializes BigInt→string on the wire, so either include a wire-compat check or deliberately leave it on t.bigint().
- Write sites:
apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts, apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts (drop BigInt(coinType)).
- Read sites:
apps/ensapi/src/omnigraph-api/schema/resolver-records.ts, apps/ensapi/src/omnigraph-api/lib/find-name-references/find-name-references-resolver.ts, apps/ensapi/src/lib/resolution/make-records-response.ts, apps/ensapi/src/lib/resolution/execute-operations.ts (drop bigintToCoinType).
- Storage-type change (
numeric → int8) ⇒ a re-index / migration.
Semantic nuance
bigintToCoinType throws on values > 2^53 (fail-loud on corrupt data); t.int8({ mode: "number" }) hydrates via Number() (lossless ≤ 2^53, silent truncation only on an impossible/corrupt > 2^53 row). Given the CoinType invariant and int8's 2^63 DB ceiling, real data is always safe — the only change is losing a loud failure on data that cannot legitimately exist.
Context
Surfaced while implementing Account.nameReferences (#1962 / #2292). That PR follows the existing bigint-boundary convention; this issue tracks doing the systemic cleanup so new code (and old) can use CoinType directly.
Problem
CoinType(enssdk) is a brandednumber— the invariant is that every real coinType fits inNumber.MAX_SAFE_INTEGER(bigintToCoinTypedoesNumber(v)and throws above 2^53).But the indexed schema stores
coinTypeast.bigint(), which Ponder persists asnumeric(78)and hydrates as a JSbigint. The result isbigint ⇄ numberconversions sprinkled across every read/write boundary:BigInt(coinType)—StandaloneReverseRegistrarhandler,resolver-db-helpers, resolutionoperations.bigintToCoinType(row.coinType)—resolver-records,make-records-response,execute-operations, andAccount.nameReferences's resolver (find-name-references-resolver).These conversions are pure friction and an easy thing to get wrong —
coinTypeshould beCoinTypethe whole way down, including the indexed schema.Fix
There's a precedent in the same schema file:
chainIdis the same kind of value (a number that overflows Postgresint4but fitsMAX_SAFE_INTEGER) and is typed correctly:Type
coinTypeidentically:int4genuinely overflows (DEFAULT_EVM_COIN_TYPE = 0x80000000isint4_max + 1— presumably whybigintwas chosen).int8holds up to 2^63, comfortably above the 2^53 CoinType invariant.numeric(78)is for uint256 amounts — overkill for a coinType.number, everyBigInt(coinType)/bigintToCoinType(...)at these boundaries can be dropped.Scope
packages/ensdb-sdk/src/ensindexer-abstract/):protocol-acceleration.schema.ts—reverseNameRecord.coinType(L35),resolverAddressRecord.coinType(L204)subgraph.schema.ts(L523) — legacy Subgraph path; this one serializesBigInt→string on the wire, so either include a wire-compat check or deliberately leave it ont.bigint().apps/ensindexer/src/plugins/protocol-acceleration/handlers/StandaloneReverseRegistrar.ts,apps/ensindexer/src/lib/protocol-acceleration/resolver-db-helpers.ts(dropBigInt(coinType)).apps/ensapi/src/omnigraph-api/schema/resolver-records.ts,apps/ensapi/src/omnigraph-api/lib/find-name-references/find-name-references-resolver.ts,apps/ensapi/src/lib/resolution/make-records-response.ts,apps/ensapi/src/lib/resolution/execute-operations.ts(dropbigintToCoinType).numeric→int8) ⇒ a re-index / migration.Semantic nuance
bigintToCoinTypethrows on values > 2^53 (fail-loud on corrupt data);t.int8({ mode: "number" })hydrates viaNumber()(lossless ≤ 2^53, silent truncation only on an impossible/corrupt > 2^53 row). Given the CoinType invariant and int8's 2^63 DB ceiling, real data is always safe — the only change is losing a loud failure on data that cannot legitimately exist.Context
Surfaced while implementing
Account.nameReferences(#1962 / #2292). That PR follows the existing bigint-boundary convention; this issue tracks doing the systemic cleanup so new code (and old) can useCoinTypedirectly.