diff --git a/packages/envio-tests/test/EvmHandlersApi_test.res b/packages/envio-tests/test/EvmHandlersApi_test.res new file mode 100644 index 000000000..0d5e3619e --- /dev/null +++ b/packages/envio-tests/test/EvmHandlersApi_test.res @@ -0,0 +1,628 @@ +open Vitest + +let configYaml = ` +name: ts-api-types +field_selection: + transaction_fields: + - transactionIndex + - hash +contracts: + - name: Token + events: + - event: Transfer(address indexed from, address indexed to, uint256 value) + - event: Approval(address indexed owner, address indexed spender, uint256 value) + field_selection: + block_fields: + - parentHash + transaction_fields: + - to + - from + - hash + - event: "Synced()" + - name: Factory + events: + - event: PoolCreated(address indexed pool) +chains: + - id: 1 + start_block: 0 + contracts: + - name: Token + address: "0x0000000000000000000000000000000000000001" + - name: Factory + address: "0x0000000000000000000000000000000000000002" + - id: 137 + start_block: 0 + contracts: + - name: Token + address: "0x0000000000000000000000000000000000000003" + - name: Factory + address: "0x0000000000000000000000000000000000000004" +` + +let check = handlers => InternalTestIndexer.fromUserApi(~schema=ApiTypesFixtures.schema, ~handlers, ~configYaml)->ignore + +describe("EVM API types", () => { + it("resolves config-bound chain/contract name and id unions", _ => + check(` +import type { + Address, + EvmChainId, + EvmChainName, + EvmContractName, + FuelChainId, + SvmChainId, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); + +// @ts-expect-error - "NotAContract" is not configured +const _bad: EvmContractName = "NotAContract"; + +// Non-configured ecosystems resolve to the codegen hint string, not a union. +expectType< + TypeEqual< + FuelChainId, + "FuelChainId is not available. Configure Fuel chains in config.yaml and run 'envio codegen'" + > +>(true); +expectType< + TypeEqual< + SvmChainId, + "SvmChainId is not available. Configure SVM chains in config.yaml and run 'envio codegen'" + > +>(true); +`) + ) + + it("looks up EvmEvent by contract and event name", _ => + check(` +import type { + Address, + EvmChainId, + EvmEvent, + EvmOnEvent, + EvmOnEventWhereChain, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +// Without generics, the discriminant spans every configured event. +type AllEvents = EvmEvent; +expectType>(true); + +// EvmOnEvent is the config-generic form behind the EvmEvent alias. +expectType>(true); + +// The where-callback chain object exposes the event's own contract addresses. +expectType< + TypeEqual["Token"]["addresses"], readonly Address[]> +>(true); + +type TokenEvent = EvmEvent<"Token">; +expectType>(true); + +type TransferEvent = EvmEvent<"Token", "Transfer">; +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType< + TypeEqual< + TransferEvent["params"], + { + readonly from: Address; + readonly to: Address; + readonly value: bigint; + } + > +>(true); +expectType>(true); +expectType>(true); +expectType>(true); +`) + ) + + it("shapes onEvent / contractRegister options, handlers and contexts", _ => + check(` +import type { + Account, + Address, + EvmContractRegisterContext, + EvmContractRegisterHandler, + EvmContractRegisterOptions, + EvmEvent, + EvmOnEventContext, + EvmOnEventHandler, + EvmOnEventOptions, + EvmOnEventWhere, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +// Synced() has no indexed params, so the eventFilters lookup resolves to {}. +type SyncedOpts = EvmOnEventOptions>; +expectType< + TypeEqual< + SyncedOpts, + { + readonly contract: "Token"; + readonly event: "Synced"; + readonly wildcard?: boolean; + readonly where?: EvmOnEventWhere<{}, "Token">; + } + > +>(true); + +type PoolCreated = EvmEvent<"Factory", "PoolCreated">; +expectType< + TypeEqual, EvmOnEventOptions> +>(true); + +expectType< + TypeEqual< + EvmOnEventHandler, + (args: { event: PoolCreated; context: EvmOnEventContext }) => Promise + > +>(true); +expectType< + TypeEqual< + EvmContractRegisterHandler, + (args: { + event: PoolCreated; + context: EvmContractRegisterContext; + }) => Promise + > +>(true); + +// Without generics the handler accepts the union of every EVM event. +type DefaultArgs = Parameters[0]; +expectType>(true); +expectType>(true); + +expectType>(true); +expectType>(true); +expectType>(true); +expectType< + TypeEqual< + EvmOnEventContext["Account"]["get"], + (id: string) => Promise + > +>(true); +expectType< + TypeEqual void> +>(true); + +expectType>(true); +expectType< + TypeEqual< + EvmContractRegisterContext["chain"]["Token"]["add"], + (address: Address) => void + > +>(true); +expectType< + TypeEqual< + EvmContractRegisterContext["chain"]["Factory"]["add"], + (address: Address) => void + > +>(true); + +// contractRegister context exposes no entity operations. +// @ts-expect-error - Account ops are not on the contractRegister context +type _accountOnCr = EvmContractRegisterContext["Account"]; + +// EvmOnEventOptions rejects an Event that isn't EventLike. +// @ts-expect-error - missing contractName/eventName +type _bad = EvmOnEventOptions<{ foo: "bar" }>; + +// Union events keep contract/event paired; mismatches are rejected. +type UnionEvent = EvmEvent<"Token", "Transfer"> | EvmEvent<"Factory", "PoolCreated">; +type UnionOpts = EvmOnEventOptions; +const _a: UnionOpts = { contract: "Token", event: "Transfer" }; +const _b: UnionOpts = { contract: "Factory", event: "PoolCreated" }; +// @ts-expect-error - "PoolCreated" is not an event of "Token" +const _bad1: UnionOpts = { contract: "Token", event: "PoolCreated" }; +// @ts-expect-error - "Transfer" is not an event of "Factory" +const _bad2: UnionOpts = { contract: "Factory", event: "Transfer" }; +`) + ) + + it("narrows event block/transaction fields by field_selection", _ => + check(` +import type { EvmEvent } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +// Unselected fields carry the branded FieldNotSelected sentinel so reading +// them is a compile error instead of silently passing. +type IsNotSelected = T extends { readonly __fieldNotSelected: string } + ? true + : false; + +// Approval declares custom block_fields [parentHash] and transaction_fields +// [to, from, hash]; defaults (number/timestamp/hash) stay included. +type ApprovalEvent = EvmEvent<"Token", "Approval">; +expectType>(true); +expectType>(true); +expectType>(true); +expectType< + TypeEqual +>(true); +expectType< + TypeEqual +>(true); +expectType>(true); +expectType>(true); + +// Transfer inherits the global selection: transaction [transactionIndex, hash]. +type TransferEvent = EvmEvent<"Token", "Transfer">; +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +`) + ) + + it("binds Entity / EntityName / Enum / EnumName to the schema", _ => + check(` +import type { Account, Entity, EntityName, Enum, EnumName } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +const _account: EntityName = "Account"; +const _delegation: EntityName = "Delegation"; +// @ts-expect-error - "NotAnEntity" is not in the schema +const _badEntity: EntityName = "NotAnEntity"; + +expectType, Account>>(true); +expectType["id"], string>>(true); +expectType["balance"], bigint>>(true); +expectType["accountType"], "ADMIN" | "USER">>(true); +expectType["delegate_id"], string | undefined>>(true); +// @ts-expect-error - "NotAnEntity" is not assignable to EntityName +type _badEntityLookup = Entity<"NotAnEntity">; + +const _accountType: EnumName = "AccountType"; +const _size: EnumName = "GravatarSize"; +// @ts-expect-error - "NotAnEnum" is not in the schema +const _badEnum: EnumName = "NotAnEnum"; + +expectType, "ADMIN" | "USER">>(true); +expectType, "SMALL" | "MEDIUM" | "LARGE">>(true); +// @ts-expect-error - "NotAnEnum" is not assignable to EnumName +type _badEnumLookup = Enum<"NotAnEnum">; +`) + ) + + it("shapes the onBlock surface", _ => + check(` +import type { + EvmChainId, + EvmOnBlockContext, + EvmOnBlockFilter, + EvmOnBlockHandler, + EvmOnBlockHandlerArgs, + EvmOnBlockOptions, + EvmOnBlockWhereArgs, + EvmOnBlockWhereResult, + EvmOnEventContext, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +const _blockOpts: EvmOnBlockOptions = { + name: "b", + where: ({ chain }) => (chain.id === 1 ? true : false), +}; +expectType(_blockOpts); +expectType< + TypeEqual +>(true); +expectType>(true); +expectType< + TypeEqual Promise> +>(true); + +expectType>(true); +expectType>(true); +expectType>(true); +expectType< + TypeEqual +>(true); + +// The predicate result excludes void/undefined, so an implicit return fails. +expectType>(true); +type _Predicate = (args: { + readonly chain: { readonly id: number }; +}) => EvmOnBlockWhereResult; +// @ts-expect-error - implicit undefined return is not assignable +const _missingReturn: _Predicate = ({ chain }) => { + if (chain.id === 1) return true; +}; + +const _ok: EvmOnBlockFilter = { + block: { number: { _gte: 1, _lte: 10, _every: 2 } }, +}; +const _empty: EvmOnBlockFilter = {}; +expectType(_ok); +expectType(_empty); +`) + ) + + it("guards the indexer.onEvent / contractRegister registration surface", _ => + check(` +import { indexer } from "envio"; +import type { Address } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +// Type-only: guarded by \`if (0)\` so tsc validates the surface without +// registering handlers (registration throws once the indexer is built). +if (0) { + indexer.onEvent( + // @ts-expect-error - "BadContract" is not a configured contract + { contract: "BadContract", event: "X" }, + async () => {}, + ); + indexer.onEvent( + // @ts-expect-error - "BadEvent" is not an event of Token + { contract: "Token", event: "BadEvent" }, + async () => {}, + ); + indexer.onEvent( + { contract: "Token", event: "Transfer" }, + async ({ event }) => { + expectType>(true); + expectType>(true); + }, + ); + indexer.contractRegister( + { contract: "Factory", event: "PoolCreated" }, + async ({ event, context }) => { + expectType>(true); + context.chain.Factory.add(event.params.pool); + context.chain.Token.add(event.params.pool); + // @ts-expect-error - UnknownContract is not configured + context.chain.UnknownContract.add(event.params.pool); + }, + ); + + // where.block must stay on the EVM \`number\` filter with only \`_gte\`. + indexer.onEvent( + { + contract: "Token", + event: "Transfer", + wildcard: true, + where: { block: { number: { _gte: 1 } } }, + }, + async () => {}, + ); + indexer.onEvent( + { + contract: "Token", + event: "Transfer", + wildcard: true, + // @ts-expect-error - EVM keys block by \`number\`, not \`height\`. + where: { block: { height: { _gte: 1 } } }, + }, + async () => {}, + ); + indexer.onEvent( + { + contract: "Token", + event: "Transfer", + wildcard: true, + where: { + block: { + number: { + // @ts-expect-error - Only \`_gte\` is supported on event filters. + _lte: 1, + }, + }, + }, + }, + async () => {}, + ); + indexer.onEvent( + { + contract: "Token", + event: "Transfer", + wildcard: true, + where: { + block: { + number: { + // @ts-expect-error - Only \`_gte\` is supported on event filters. + _every: 100, + }, + }, + }, + }, + async () => {}, + ); +} +`) + ) + + it("narrows onEvent where.params by indexed event params", _ => + check(` +import { indexer } from "envio"; +import type { Address, EvmOnEventWhere, SingleOrMultiple } from "envio"; +import { expectType } from "ts-expect"; + +const ZERO: Address = "0x0000000000000000000000000000000000000000"; + +// Transfer's indexed params (from/to) resolve the where.params filter, each +// accepting a single value or an array (OR semantics). +type TransferWhere = EvmOnEventWhere< + { + readonly from?: SingleOrMultiple
; + readonly to?: SingleOrMultiple
; + }, + "Token" +>; +const _single: TransferWhere = { params: { from: ZERO } }; +const _multi: TransferWhere = { params: { from: [ZERO], to: [ZERO] } }; +expectType(_single); +expectType(_multi); + +if (0) { + indexer.onEvent( + { contract: "Token", event: "Transfer", wildcard: true, where: { params: { from: ZERO } } }, + async () => {}, + ); + indexer.onEvent( + { contract: "Token", event: "Transfer", wildcard: true, where: { params: { to: [ZERO] } } }, + async () => {}, + ); + indexer.onEvent( + { + contract: "Token", + event: "Transfer", + wildcard: true, + // @ts-expect-error - value is not an indexed param, so it isn't filterable + where: { params: { value: 1n } }, + }, + async () => {}, + ); +} +`) + ) + + it("binds the Indexer / TestIndexer instances and TestHelpers", _ => + check(` +import { createTestIndexer, indexer, TestHelpers } from "envio"; +import type { + Account, + Indexer, + TestIndexer, + TestIndexerProcessConfig, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +expectType TestIndexer>>(true); +expectType>(true); +expectType>(true); + +// The test indexer exposes entity operations bound to the schema. +expectType< + TypeEqual void> +>(true); + +// process() config is keyed by chain id. +const _proc: TestIndexerProcessConfig = { chains: { 1: { startBlock: 0 } } }; +expectType(_proc); + +expectType< + TypeEqual +>(true); +`) + ) +}) + +describe("Effect and utility types", () => { + it("infers createEffect input/output and Effect handles", _ => + check(` +import { createEffect, S } from "envio"; +import type { + Effect, + EffectArgs, + EffectCaller, + EffectChain, + EffectContext, + EffectOptions, + Logger, + RateLimit, + RateLimitDuration, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +const getBalance = createEffect( + { + name: "getBalance", + input: { address: S.address, blockNumber: S.optional(S.bigint) }, + output: S.bigint, + rateLimit: false, + }, + async ({ input, context }) => { + expectType< + TypeEqual< + typeof input, + { address: \`0x\${string}\`; blockNumber?: bigint | undefined } + > + >(true); + expectType>(true); + expectType>(true); + expectType>(true); + // @ts-expect-error - input is required for a non-undefined schema + await context.effect(getBalance, undefined); + return input.blockNumber ?? 0n; + }, +); + +expectType< + TypeEqual< + typeof getBalance, + Effect<{ address: \`0x\${string}\`; blockNumber?: bigint | undefined }, bigint> + > +>(true); + +// Ecosystem-agnostic surface aliases. +expectType>(true); +expectType["input"], number>>(true); +expectType>(true); +const _rlOff: RateLimit = false; +const _rl: RateLimit = { calls: 1, per: "second" }; +const _dur: RateLimitDuration = "minute"; +expectType(_rlOff); +expectType(_rl); +expectType(_dur); +expectType["name"], string>>(true); +expectType["rateLimit"], RateLimit>>(true); +`) + ) + + it("shapes getWhere filters, the dynamic where callback, and misc aliases", _ => + check(` +import type { + Account, + Address, + EvmOnEventWhere, + EvmOnEventWhereArgs, + EvmOnEventWhereFilter, + GetWhereFilter, + GetWhereOperator, + Logger, + SingleOrMultiple, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +// getWhere operators/filters over an entity type. +const _op: GetWhereOperator = { _gte: 1n, _in: [1n, 2n] }; +expectType>(_op); +const _filter: GetWhereFilter = { balance: { _gt: 0n }, id: { _eq: "x" } }; +expectType>(_filter); + +// The dynamic where callback form exposes the event's own contract addresses. +type Args = EvmOnEventWhereArgs<"Token">; +expectType>(true); +expectType< + TypeEqual +>(true); +const _cb: EvmOnEventWhere<{}, "Token"> = ({ chain }) => + chain.id === 1 ? true : { block: { number: { _gte: 1 } } }; +expectType>(_cb); +const _staticFilter: EvmOnEventWhereFilter<{}> = { block: { number: { _gte: 1 } } }; +expectType>(_staticFilter); + +// SingleOrMultiple accepts a value or a readonly array of it. +const _single: SingleOrMultiple
= "0x0"; +const _multi: SingleOrMultiple
= ["0x0", "0x1"]; +expectType>(_single); +expectType>(_multi); + +expectType< + TypeEqual | Error) => void> +>(true); +`) + ) +}) diff --git a/packages/envio-tests/test/FuelHandlersApi_test.res b/packages/envio-tests/test/FuelHandlersApi_test.res new file mode 100644 index 000000000..303d0adb2 --- /dev/null +++ b/packages/envio-tests/test/FuelHandlersApi_test.res @@ -0,0 +1,277 @@ +open Vitest + +let files = Dict.fromArray([("abis/greeter-abi.json", FuelAbiFixtures.greeter)]) + +let configYaml = ` +name: fuel-api-types +ecosystem: fuel +chains: + - id: 0 + start_block: 0 + contracts: + - name: Greeter + address: 0xb9bc445e5696c966dcf7e5d1237bd03c04e3ba6929bdaedfeebc7aae784c3a0b + abi_file_path: abis/greeter-abi.json + events: + - name: NewGreeting + - name: ClearGreeting +` + +let check = handlers => InternalTestIndexer.fromUserApi(~schema=ApiTypesFixtures.schema, ~files, ~handlers, ~configYaml)->ignore + +describe("Fuel API types", () => { + it("resolves config-bound Fuel chain/contract name and id unions", _ => + check(` +import type { FuelChainId, FuelChainName, FuelContractName } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +expectType>(true); +expectType>(true); + +// @ts-expect-error - "NotAContract" is not configured +const _bad: FuelContractName = "NotAContract"; +`) + ) + + it("looks up FuelEvent and its Fuel-specific block/transaction", _ => + check(` +import type { FuelChainId, FuelEvent } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +type AllEvents = FuelEvent; +expectType>(true); + +type NewGreeting = FuelEvent<"Greeter", "NewGreeting">; +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); + +// Fuel blocks are keyed by height (not number) and carry an id + time. +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); + +// Log params decode to the ABI's Sway struct shapes. +expectType>(true); +expectType>(true); + +type ClearGreeting = FuelEvent<"Greeter", "ClearGreeting">; +expectType>(true); +expectType>(true); +`) + ) + + it("shapes Fuel onEvent / contractRegister options, handlers and contexts", _ => + check(` +import type { + Account, + Address, + FuelContractRegisterContext, + FuelContractRegisterHandler, + FuelContractRegisterOptions, + FuelEvent, + FuelOnEventContext, + FuelOnEventHandler, + FuelOnEventOptions, + FuelOnEventWhere, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +type NewGreeting = FuelEvent<"Greeter", "NewGreeting">; + +// Fuel has no indexed params, so the eventFilters lookup resolves to {}. +expectType< + TypeEqual< + FuelOnEventOptions, + { + readonly contract: "Greeter"; + readonly event: "NewGreeting"; + readonly wildcard?: boolean; + readonly where?: FuelOnEventWhere<{}, "Greeter">; + } + > +>(true); + +expectType< + TypeEqual< + FuelContractRegisterOptions, + FuelOnEventOptions + > +>(true); + +expectType< + TypeEqual< + FuelOnEventHandler, + (args: { event: NewGreeting; context: FuelOnEventContext }) => Promise + > +>(true); +expectType< + TypeEqual< + FuelContractRegisterHandler, + (args: { + event: NewGreeting; + context: FuelContractRegisterContext; + }) => Promise + > +>(true); + +expectType>(true); +expectType>(true); +expectType< + TypeEqual void> +>(true); + +expectType>(true); +expectType< + TypeEqual< + FuelContractRegisterContext["chain"]["Greeter"]["add"], + (address: Address) => void + > +>(true); + +// contractRegister context exposes no entity operations. +// @ts-expect-error - Account ops are not on the Fuel contractRegister context +type _accountOnCr = FuelContractRegisterContext["Account"]; +`) + ) + + it("keys the Fuel onBlock / where surface on block.height", _ => + check(` +import type { + Address, + FuelChainId, + FuelOnBlockContext, + FuelOnBlockFilter, + FuelOnBlockHandler, + FuelOnBlockHandlerArgs, + FuelOnBlockOptions, + FuelOnBlockWhereArgs, + FuelOnBlockWhereResult, + FuelOnEvent, + FuelOnEventContext, + FuelOnEventWhere, + FuelOnEventWhereArgs, + FuelOnEventWhereChain, + FuelOnEventWhereFilter, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); + +// FuelOnEvent is the config-generic form behind the FuelEvent alias. +expectType>(true); + +const _fBlockOpts: FuelOnBlockOptions = { + name: "b", + where: ({ chain }) => (chain.id === 0 ? true : false), +}; +expectType(_fBlockOpts); + +// The Fuel where-callback surface mirrors EVM but filters on block.height. +expectType["id"], number>>(true); +expectType< + TypeEqual< + FuelOnEventWhereArgs<"Greeter">["chain"]["Greeter"]["addresses"], + readonly Address[] + > +>(true); +const _fFilter: FuelOnEventWhereFilter<{}> = { block: { height: { _gte: 1 } } }; +expectType>(_fFilter); +expectType< + TypeEqual +>(true); +expectType>(true); +expectType< + TypeEqual Promise> +>(true); +expectType>(true); +expectType>(true); + +const _ok: FuelOnBlockFilter = { + block: { height: { _gte: 1, _lte: 10, _every: 2 } }, +}; +expectType(_ok); + +// Fuel event filters narrow block.height with _gte only. +const _where: FuelOnEventWhere<{}, "Greeter"> = { + block: { height: { _gte: 1 } }, +}; +expectType>(_where); + +// The dynamic callback form returns a filter or a boolean. +const _whereCb: FuelOnEventWhere<{}, "Greeter"> = ({ chain }) => + chain.id === 0 ? true : { block: { height: { _gte: 1 } } }; +expectType>(_whereCb); +`) + ) + + it("guards the Fuel indexer registration surface", _ => + check(` +import { indexer } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +if (0) { + indexer.onEvent( + // @ts-expect-error - "BadContract" is not a configured contract + { contract: "BadContract", event: "X" }, + async () => {}, + ); + indexer.onEvent( + // @ts-expect-error - "BadEvent" is not an event of Greeter + { contract: "Greeter", event: "BadEvent" }, + async () => {}, + ); + indexer.onEvent( + { contract: "Greeter", event: "NewGreeting" }, + async ({ event }) => { + expectType>(true); + }, + ); + indexer.contractRegister( + { contract: "Greeter", event: "ClearGreeting" }, + async ({ context }) => { + context.chain.Greeter.add("0x0"); + }, + ); + indexer.onBlock( + { name: "fuelBlock", where: ({ chain }) => (chain.id === 0 ? true : false) }, + async ({ block }) => { + expectType>(true); + }, + ); + indexer.onEvent( + { + contract: "Greeter", + event: "NewGreeting", + wildcard: true, + // @ts-expect-error - Fuel keys block by \`height\`, not \`number\`. + where: { block: { number: { _gte: 1 } } }, + }, + async () => {}, + ); +} +`) + ) + + it("binds schema entities and enums under a Fuel config", _ => + check(` +import type { Account, Entity, EntityName, Enum, EnumName } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +const _entity: EntityName = "Account"; +// @ts-expect-error - "NotAnEntity" is not in the schema +const _badEntity: EntityName = "NotAnEntity"; +expectType, Account>>(true); +expectType["accountType"], "ADMIN" | "USER">>(true); + +const _enum: EnumName = "AccountType"; +// @ts-expect-error - "NotAnEnum" is not in the schema +const _badEnum: EnumName = "NotAnEnum"; +expectType, "SMALL" | "MEDIUM" | "LARGE">>(true); +`) + ) +}) diff --git a/packages/envio-tests/test/SvmHandlersApi_test.res b/packages/envio-tests/test/SvmHandlersApi_test.res new file mode 100644 index 000000000..45c6080a2 --- /dev/null +++ b/packages/envio-tests/test/SvmHandlersApi_test.res @@ -0,0 +1,220 @@ +open Vitest + +let configYaml = ` +name: svm-api-types +ecosystem: svm +chains: + - start_block: 0 + experimental: + hypersync_config: + url: https://solana.hypersync.xyz + programs: + - name: Swapper + program_id: 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 + instructions: + - name: swap + discriminator: "0x09" + args: + - { name: amountIn, type: u64 } + - { name: minAmountOut, type: u64 } + accounts: + - source + - destination + field_selection: + transaction_fields: [signatures] +` + +let check = handlers => InternalTestIndexer.fromUserApi(~schema=ApiTypesFixtures.schema, ~handlers, ~configYaml)->ignore + +describe("SVM API types", () => { + it("resolves config-bound SVM chain name and id unions", _ => + check(` +import type { SvmChainId, SvmChainName } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +expectType>(true); +`) + ) + + it("shapes the onSlot surface", _ => + check(` +import type { + Account, + SvmChainId, + SvmOnSlotContext, + SvmOnSlotFilter, + SvmOnSlotHandler, + SvmOnSlotHandlerArgs, + SvmOnSlotOptions, + SvmOnSlotWhereArgs, + SvmOnSlotWhereResult, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); + +const _slotOpts: SvmOnSlotOptions = { + name: "s", + where: ({ chain }) => (chain.id === 0 ? true : false), +}; +expectType(_slotOpts); +expectType>(true); +expectType< + TypeEqual void> +>(true); +expectType< + TypeEqual< + SvmOnSlotContext["Account"]["get"], + (id: string) => Promise + > +>(true); + +expectType>(true); +expectType>(true); +expectType< + TypeEqual Promise> +>(true); + +expectType>(true); +expectType>(true); + +const _ok: SvmOnSlotFilter = { slot: { _gte: 1, _lte: 10, _every: 2 } }; +const _empty: SvmOnSlotFilter = {}; +expectType(_ok); +expectType(_empty); +`) + ) + + it("shapes the config-independent instruction named types", _ => + check(` +import type { + SvmInstruction, + SvmInstructionBlock, + SvmInstructionParams, + SvmLog, + SvmTokenBalance, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); + +expectType>(true); +expectType>(true); +expectType< + TypeEqual>> +>(true); +expectType< + TypeEqual +>(true); + +expectType>(true); +expectType>(true); +expectType>(true); + +expectType>(true); +expectType>(true); +`) + ) + + it("shapes onInstruction options / handler and narrows params from config", _ => + check(` +import type { + SvmOnInstructionHandler, + SvmOnInstructionHandlerArgs, + SvmOnInstructionOptions, + SvmOnSlotContext, + SvmTransaction, +} from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +expectType< + TypeEqual< + SvmOnInstructionOptions<"Swapper", "swap">, + { readonly program: "Swapper"; readonly instruction: "swap" } + > +>(true); +expectType< + TypeEqual +>(true); +expectType< + TypeEqual< + SvmOnInstructionHandler, + (args: SvmOnInstructionHandlerArgs) => Promise + > +>(true); + +// The configured instruction selects the signatures transaction field; +// unselected fields carry the FieldNotSelected sentinel. +type IsNotSelected = T extends { readonly __fieldNotSelected: string } + ? true + : false; +expectType>(true); +expectType>(true); +`) + ) + + it("guards the SVM indexer registration surface", _ => + check(` +import { indexer } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +if (0) { + indexer.onSlot( + { name: "everySlot", where: ({ chain }) => (chain.id === 0 ? true : false) }, + async ({ slot }) => { + expectType>(true); + }, + ); + indexer.onInstruction( + // @ts-expect-error - "BadProgram" is not a configured program + { program: "BadProgram", instruction: "swap" }, + async () => {}, + ); + indexer.onInstruction( + // @ts-expect-error - "badInstr" is not an instruction of Swapper + { program: "Swapper", instruction: "badInstr" }, + async () => {}, + ); + indexer.onInstruction( + { program: "Swapper", instruction: "swap" }, + async ({ instruction }) => { + expectType>(true); + if (instruction.params) { + expectType>(true); + expectType>(true); + } + }, + ); +} +`) + ) + + it("binds schema entities and enums under an SVM config", _ => + check(` +import type { Account, Entity, EntityName, Enum, EnumName } from "envio"; +import { expectType, type TypeEqual } from "ts-expect"; + +const _entity: EntityName = "Account"; +// @ts-expect-error - "NotAnEntity" is not in the schema +const _badEntity: EntityName = "NotAnEntity"; +expectType, Account>>(true); +expectType["accountType"], "ADMIN" | "USER">>(true); + +const _enum: EnumName = "AccountType"; +// @ts-expect-error - "NotAnEnum" is not in the schema +const _badEnum: EnumName = "NotAnEnum"; +expectType, "SMALL" | "MEDIUM" | "LARGE">>(true); +`) + ) +}) diff --git a/packages/envio-tests/test/helpers/ApiTypesFixtures.res b/packages/envio-tests/test/helpers/ApiTypesFixtures.res new file mode 100644 index 000000000..af4aae51f --- /dev/null +++ b/packages/envio-tests/test/helpers/ApiTypesFixtures.res @@ -0,0 +1,27 @@ +// Shared GraphQL schema backing the per-ecosystem TypeScript API type tests. +// Entities and enums here let each ecosystem's generated context expose the +// same entity operations and schema-bound aliases (Entity/Enum/…). +let schema = ` +enum AccountType { + ADMIN + USER +} + +enum GravatarSize { + SMALL + MEDIUM + LARGE +} + +type Account { + id: ID! + balance: BigInt! + accountType: AccountType! + delegate: Account +} + +type Delegation { + id: ID! + amount: BigInt! +} +` diff --git a/packages/envio-tests/test/helpers/FuelAbiFixtures.res b/packages/envio-tests/test/helpers/FuelAbiFixtures.res new file mode 100644 index 000000000..c38e03da1 --- /dev/null +++ b/packages/envio-tests/test/helpers/FuelAbiFixtures.res @@ -0,0 +1,189 @@ +let greeter = `{ + "programType": "contract", + "specVersion": "1", + "encodingVersion": "1", + "concreteTypes": [ + { + "type": "()", + "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "type": "enum Error", + "concreteTypeId": "4ed298ed0be3fc65895c5d5263885191731caa9f79ff63e56d98b449e5ba4b3f", + "metadataTypeId": 1 + }, + { + "type": "enum std::option::Option", + "concreteTypeId": "6131325e56e0a740b11c011fd2f7e67e7104a223cad4ddd4c4033d9fe1fe9768", + "metadataTypeId": 2, + "typeArguments": [ + "270fc75adb9a9c47c2092f42f632ea79d1f5e558793ff90fcde067ff3a899ccf" + ] + }, + { + "type": "str[8]", + "concreteTypeId": "fd59cdd2c531ab3564f4fcc13eb876860d4d7cd4d12aa1f042a5aeceacde951f" + }, + { + "type": "struct ClearGreeting", + "concreteTypeId": "3e3ffd83393027a4af2a81a6fd031daba84882b4117a89be43a798f807322e98", + "metadataTypeId": 4 + }, + { + "type": "struct Greeting", + "concreteTypeId": "270fc75adb9a9c47c2092f42f632ea79d1f5e558793ff90fcde067ff3a899ccf", + "metadataTypeId": 5 + }, + { + "type": "struct NewGreeting", + "concreteTypeId": "4d8812009a01dfce9ead2049933d9eb736fadce2d22525e8f451c3f5c00fc4fe", + "metadataTypeId": 6 + } + ], + "metadataTypes": [ + { + "type": "b256", + "metadataTypeId": 0 + }, + { + "type": "enum Error", + "metadataTypeId": 1, + "components": [ + { + "name": "InvalidContractSender", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "name": "ToThrow", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + } + ] + }, + { + "type": "enum std::option::Option", + "metadataTypeId": 2, + "components": [ + { + "name": "None", + "typeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" + }, + { + "name": "Some", + "typeId": 3 + } + ], + "typeParameters": [3] + }, + { + "type": "generic T", + "metadataTypeId": 3 + }, + { + "type": "struct ClearGreeting", + "metadataTypeId": 4, + "components": [ + { + "name": "user", + "typeId": 7 + } + ] + }, + { + "type": "struct Greeting", + "metadataTypeId": 5, + "components": [ + { + "name": "value", + "typeId": "fd59cdd2c531ab3564f4fcc13eb876860d4d7cd4d12aa1f042a5aeceacde951f" + } + ] + }, + { + "type": "struct NewGreeting", + "metadataTypeId": 6, + "components": [ + { + "name": "user", + "typeId": 7 + }, + { + "name": "greeting", + "typeId": 5 + } + ] + }, + { + "type": "struct std::address::Address", + "metadataTypeId": 7, + "components": [ + { + "name": "bits", + "typeId": 0 + } + ] + } + ], + "functions": [ + { + "inputs": [], + "name": "clear_greeting", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": [ + { + "name": "storage", + "arguments": ["write"] + } + ] + }, + { + "inputs": [], + "name": "current_greeting", + "output": "6131325e56e0a740b11c011fd2f7e67e7104a223cad4ddd4c4033d9fe1fe9768", + "attributes": [ + { + "name": "storage", + "arguments": ["read"] + } + ] + }, + { + "inputs": [ + { + "name": "greeting", + "concreteTypeId": "fd59cdd2c531ab3564f4fcc13eb876860d4d7cd4d12aa1f042a5aeceacde951f" + } + ], + "name": "set_greeting", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": [ + { + "name": "storage", + "arguments": ["write"] + } + ] + }, + { + "inputs": [], + "name": "throw_error", + "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", + "attributes": null + } + ], + "loggedTypes": [ + { + "logId": "4485582493926303652", + "concreteTypeId": "3e3ffd83393027a4af2a81a6fd031daba84882b4117a89be43a798f807322e98" + }, + { + "logId": "5679770223941778533", + "concreteTypeId": "4ed298ed0be3fc65895c5d5263885191731caa9f79ff63e56d98b449e5ba4b3f" + }, + { + "logId": "5586735131546214350", + "concreteTypeId": "4d8812009a01dfce9ead2049933d9eb736fadce2d22525e8f451c3f5c00fc4fe" + } + ], + "messagesTypes": [], + "configurables": [] +} +` diff --git a/scenarios/test_codegen/src/handlers/EventHandlers.ts b/scenarios/test_codegen/src/handlers/EventHandlers.ts index ebfe77f7e..f8ba2eb49 100644 --- a/scenarios/test_codegen/src/handlers/EventHandlers.ts +++ b/scenarios/test_codegen/src/handlers/EventHandlers.ts @@ -129,40 +129,6 @@ expectType< > >(true); -// Type-only surface checks for the registration API. Guarded by `if (0)` so -// tsc validates them but they never execute: invalid contract/event combos must -// stay compile errors, and these must not register real handlers. -if (0) { - indexer.onEvent( - // @ts-expect-error - "BadContract" is not a configured contract - { contract: "BadContract", event: "X" }, - async () => {}, - ); - indexer.onEvent( - // @ts-expect-error - "BadEvent" is not an event of Gravatar - { contract: "Gravatar", event: "BadEvent" }, - async () => {}, - ); - indexer.onEvent( - { contract: "Gravatar", event: "NewGravatar" }, - async ({ event }) => { - expectType>(true); - }, - ); - indexer.contractRegister( - { contract: "NftFactory", event: "SimpleNftCreated" }, - async ({ event, context }) => { - expectType< - TypeEqual - >(true); - context.chain.SimpleNft.add(event.params.contractAddress); - context.chain.NftFactory.add(event.params.contractAddress); - // @ts-expect-error - UnknownContract is not configured - context.chain.UnknownContract.add(event.params.contractAddress); - }, - ); -} - const zeroAddress = "0x0000000000000000000000000000000000000000"; indexer.onEvent({ contract: "Gravatar", event: "CustomSelection" }, async ({ event, context }) => { @@ -188,7 +154,8 @@ indexer.onEvent({ contract: "Gravatar", event: "CustomSelection" }, async ({ eve S.assertOrThrow(event.block, blockSchema)!; deepEqual(context.chain.id, event.chainId); - // Type checking for custom field selection is done in CustomSelection.test.ts + // Type checking for custom field selection lives in + // packages/envio-tests/test/TypeScriptApiTypes_test.res // Test chain field accessibility in TypeScript expectType< @@ -1000,69 +967,3 @@ indexer.onBlock( { name: "test_onblock_skip_all", where: () => false }, async () => {}, ); - -// Type-level regression guards for `where.block` on EVM. Declared as an -// unreached function so `tsc --noEmit` checks the types without runtime -// re-registering events. The `@ts-expect-error` assertions catch the -// class of bug where `EvmOnEventWhere` might get wired through the wrong -// filter shape (e.g. Fuel's `block.height`) — a regression would flip -// the directive from "expected" to "unused", failing the build. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function _typeCheckEvmWhereBlockShape() { - indexer.onEvent( - { - contract: "EventFiltersTest", - event: "Transfer", - wildcard: true, - where: { block: { number: { _gte: 1 } } }, - }, - async () => {}, - ); - indexer.onEvent( - { - contract: "EventFiltersTest", - event: "Transfer", - wildcard: true, - where: { - block: { - // @ts-expect-error EVM keys block by `number`, not `height`. - height: { _gte: 1 }, - }, - }, - }, - async () => {}, - ); - indexer.onEvent( - { - contract: "EventFiltersTest", - event: "Transfer", - wildcard: true, - where: { - block: { - number: { - // @ts-expect-error Only `_gte` is supported on event filters. - _lte: 1, - }, - }, - }, - }, - async () => {}, - ); - indexer.onEvent( - { - contract: "EventFiltersTest", - event: "Transfer", - wildcard: true, - where: { - block: { - number: { - // @ts-expect-error Only `_gte` is supported on event filters. - _every: 100, - }, - }, - }, - }, - async () => {}, - ); -} - diff --git a/scenarios/test_codegen/test/CustomSelection.test.ts b/scenarios/test_codegen/test/CustomSelection.test.ts index 10eb968b2..b2540dc7d 100644 --- a/scenarios/test_codegen/test/CustomSelection.test.ts +++ b/scenarios/test_codegen/test/CustomSelection.test.ts @@ -1,50 +1,6 @@ -import { expectType, type TypeEqual } from "ts-expect"; import assert from "assert"; import { it } from "vitest"; -import { createTestIndexer, type EvmEvent } from "envio"; - -type CustomSelectionEvent = EvmEvent<"Gravatar", "CustomSelection">; -type EmptyEventEvent = EvmEvent<"Gravatar", "EmptyEvent">; - -// Unselected fields are typed as the branded `FieldNotSelected<...>` (not `never`) -// so reading them is a type error instead of silently passing as `never`. -type IsNotSelected = T extends { readonly __fieldNotSelected: string } - ? true - : false; - -// Compile-time type assertions for custom field selection -// CustomSelection event has custom block_fields: [parentHash] -// Default fields (number, timestamp, hash) are always included -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -// Unselected block fields are not selected -expectType>(true); -expectType>(true); - -// CustomSelection event has custom transaction_fields: [to, from, hash] -expectType>(true); -expectType>(true); -expectType>(true); -// Unselected transaction fields are not selected -expectType>(true); -expectType>(true); - -// Events without custom field selection should use the global one -// Global has transactionIndex + hash -expectType>(true); -expectType>(true); -// Fields not in global selection are not selected -expectType>(true); -expectType>(true); - -// Global block has defaults only (number, timestamp, hash) -expectType>(true); -expectType>(true); -expectType>(true); -// parentHash not in global selection — not selected -expectType>(true); +import { createTestIndexer } from "envio"; it("Handles event with a custom field selection (in TS)", async () => { const indexer = createTestIndexer(); diff --git a/scenarios/test_codegen/test/EventHandler.test.ts b/scenarios/test_codegen/test/EventHandler.test.ts index 02919f3df..0ce12e2e3 100644 --- a/scenarios/test_codegen/test/EventHandler.test.ts +++ b/scenarios/test_codegen/test/EventHandler.test.ts @@ -6,34 +6,11 @@ import { type Indexer, type EvmChainId, type EvmChainName, - type EvmContractName, - type EvmEvent, type FuelChainId, - type FuelEvent, type SvmChainId, type TestIndexer, type Token, - type EvmOnEventOptions, - type EvmOnEventHandler, - type EvmContractRegisterOptions, - type EvmContractRegisterHandler, - type EvmOnEventContext, - type EvmContractRegisterContext, - type EvmOnEventWhere, - type Entity, - type EntityName, - type Enum, - type EnumName, -} from "envio"; -import { type Address, - type EvmOnBlockWhereResult, - type EvmOnBlockFilter, - type EvmOnBlockOptions, - type EvmOnBlockContext, - type EvmOnBlockHandler, - type EvmOnBlockHandlerArgs, - type EvmOnBlockWhereArgs, } from "envio"; import { expectType, type TypeEqual } from "ts-expect"; import { createTestIndexer } from "envio"; @@ -1570,373 +1547,4 @@ describe("Use Envio test framework to test event handlers", () => { }], }); }); - - it("EvmEvent type", () => { - // EvmEvent without generics is a union of all events - type AllEvents = EvmEvent; - - // contractName and eventName are discriminant fields with literal types - expectType< - TypeEqual< - AllEvents["contractName"], - | "Gravatar" - | "NftFactory" - | "SimpleNft" - | "TestEvents" - | "Noop" - | "EventFiltersTest" - > - >(true); - - // Narrowing to a specific contract's events - type GravatarEvent = EvmEvent<"Gravatar">; - expectType< - TypeEqual< - GravatarEvent["contractName"], - "Gravatar" - > - >(true); - - // Narrowing to a specific event - type NewGravatarEvent = EvmEvent<"Gravatar", "NewGravatar">; - expectType>(true); - expectType>(true); - expectType>(true); - expectType>(true); - expectType>(true); - expectType< - TypeEqual< - NewGravatarEvent["params"], - { - readonly id: bigint; - readonly owner: `0x${string}`; - readonly displayName: string; - readonly imageUrl: string; - } - > - >(true); - - // Block and transaction have proper types - expectType>(true); - expectType>(true); - expectType>(true); - - // Non-configured ecosystem event types return error string - expectType< - TypeEqual< - FuelEvent, - "FuelEvent is not available. Configure Fuel contracts in config.yaml and run 'envio codegen'" - > - >(true); - }); -}); - -describe("onEvent / contractRegister types", () => { - it("EvmOnEventOptions resolves contract/event literals from Event", () => { - type GravatarNewGravatar = EvmEvent<"Gravatar", "NewGravatar">; - type Opts = EvmOnEventOptions; - - // Gravatar.NewGravatar has no indexed params, so the project-bound - // EvmEventFilters lookup resolves params to {} and `where` is typed - // as `EvmOnEventWhere<{}, "Gravatar">` rather than `unknown`. - expectType< - TypeEqual< - Opts, - { - readonly contract: "Gravatar"; - readonly event: "NewGravatar"; - readonly wildcard?: boolean; - readonly where?: EvmOnEventWhere<{}, "Gravatar">; - } - > - >(true); - }); - - it("EvmContractRegisterOptions has same shape as EvmOnEventOptions", () => { - type Ev = EvmEvent<"NftFactory", "SimpleNftCreated">; - expectType< - TypeEqual, EvmOnEventOptions> - >(true); - }); - - it("EvmOnEventHandler has correct args shape", () => { - type Ev = EvmEvent<"NftFactory", "SimpleNftCreated">; - type Handler = EvmOnEventHandler; - - expectType< - TypeEqual< - Handler, - (args: { event: Ev; context: EvmOnEventContext }) => Promise - > - >(true); - }); - - it("EvmContractRegisterHandler uses EvmContractRegisterContext", () => { - type Ev = EvmEvent<"NftFactory", "SimpleNftCreated">; - type Handler = EvmContractRegisterHandler; - - expectType< - TypeEqual< - Handler, - (args: { - event: Ev; - context: EvmContractRegisterContext; - }) => Promise - > - >(true); - }); - - it("EvmOnEventContext has chain info and entity ops", () => { - expectType>(true); - expectType>(true); - expectType>(true); - - // Entity ops are available on context - expectType< - TypeEqual< - EvmOnEventContext["User"]["get"], - (id: string) => Promise - > - >(true); - expectType< - TypeEqual void> - >(true); - }); - - it("EvmContractRegisterContext has chain.ContractName.add() registration", () => { - expectType< - TypeEqual - >(true); - expectType< - TypeEqual< - EvmContractRegisterContext["chain"]["NftFactory"]["add"], - (address: Address) => void - > - >(true); - expectType< - TypeEqual< - EvmContractRegisterContext["chain"]["SimpleNft"]["add"], - (address: Address) => void - > - >(true); - expectType< - TypeEqual< - EvmContractRegisterContext["chain"]["Gravatar"]["add"], - (address: Address) => void - > - >(true); - }); - - it("EvmContractRegisterContext does not expose entity operations", () => { - // @ts-expect-error - User entity ops should not be on contractRegister context - type _userOnCr = EvmContractRegisterContext["User"]; - }); - - // `indexer.onEvent` / `indexer.contractRegister` type-surface checks live in - // `src/handlers/EventHandlers.ts` (compile-time only): the registration API - // throws once handlers are registered, so they can't run as post-registration - // test cases here. - - it("EvmOnEventHandler defaults to union of all events", () => { - // Without args, the handler accepts the union of all EVM events - type DefaultHandler = EvmOnEventHandler; - type DefaultArgs = Parameters[0]; - - // The event field is the union of all EVM events - expectType>(true); - expectType>(true); - }); - - it("EvmOnEventOptions rejects invalid Event constraint", () => { - // EventLike requires contractName and eventName fields - // @ts-expect-error - missing required fields - type _bad = EvmOnEventOptions<{ foo: "bar" }>; - }); - - it("EvmOnEventOptions preserves contract/event pairing across union members", () => { - // With distributive conditional typing, a union Event type yields a union - // of options where each member's contract/event are paired together. - // Mismatched pairings (e.g. contract: "Gravatar", event: "SimpleNftCreated") - // must be rejected. - type UnionEvent = - | EvmEvent<"Gravatar", "NewGravatar"> - | EvmEvent<"NftFactory", "SimpleNftCreated">; - type UnionOpts = EvmOnEventOptions; - - // Valid pairings compile - const _a: UnionOpts = { contract: "Gravatar", event: "NewGravatar" }; - const _b: UnionOpts = { - contract: "NftFactory", - event: "SimpleNftCreated", - }; - - // @ts-expect-error - "SimpleNftCreated" is not an event of "Gravatar" - const _bad1: UnionOpts = { - contract: "Gravatar", - event: "SimpleNftCreated", - }; - // @ts-expect-error - "NewGravatar" is not an event of "NftFactory" - const _bad2: UnionOpts = { - contract: "NftFactory", - event: "NewGravatar", - }; - }); - - // Type-level surface checks for `indexer.onBlock`. These assertions are - // cheap (no runtime), but they catch regressions where the `where`-return - // type widens back to include `void`/`undefined` — which the runtime no - // longer silently accepts, so the TS type must stay strict. - it("indexer.onBlock exists on the indexer (value-level)", () => { - expectType(indexer.onBlock); - // Smoke-check the options shape by constructing one. If `where` ever - // loses `void` exclusion, the implicit-return negative test below - // catches it; this ensures the `name` + `where` shape still type-checks. - const _opts: Parameters[0] = { - name: "someBlockHandler", - where: ({ chain }) => (chain.id === 1 ? true : false), - }; - expectType[0]>(_opts); - }); - - it("EvmOnBlockWhereResult excludes void/undefined", () => { - // Narrow intent: the result type is exactly `boolean | EvmOnBlockFilter`. - expectType>( - true - ); - // Negative: a function whose body omits a return path is rejected. - // `noImplicitReturns`/the declared return type catches the missing - // return, so the assignment below should fail to type-check. - type _Predicate = (args: { - readonly chain: { readonly id: number }; - }) => EvmOnBlockWhereResult; - // @ts-expect-error - implicit undefined return is not assignable - const _missingReturn: _Predicate = ({ chain }) => { - if (chain.id === 1) return true; - // Falls through with no return — must be a type error. - }; - }); - - it("EvmOnBlockFilter accepts partial/empty shapes (strict-key checks live in the runtime schema)", () => { - // The TS type uses `?:` for every field and doesn't catch typos - // (e.g. `_gt` is fine to TS). The runtime `S.strict` on - // `blockRangeSchema` is what rejects them with a clear error. This - // assertion documents that split: TS validates *shape*, runtime - // validates *keys*. - const _ok: EvmOnBlockFilter = { - block: { number: { _gte: 1, _lte: 10, _every: 2 } }, - }; - const _partial: EvmOnBlockFilter = { block: { number: { _gte: 1 } } }; - const _empty: EvmOnBlockFilter = {}; - expectType(_ok); - expectType(_partial); - expectType(_empty); - }); -}); - -describe("Schema-bound types: Entity / EntityName / Enum / EnumName", () => { - it("EntityName accepts schema entity names and rejects others", () => { - // Positive: known entities are assignable to EntityName. - const _user: EntityName = "User"; - const _gravatar: EntityName = "Gravatar"; - expectType(_user); - expectType(_gravatar); - - // @ts-expect-error - "NotAnEntity" is not in the schema - const _bad: EntityName = "NotAnEntity"; - }); - - it("Entity resolves to the per-entity shape", () => { - // Entity<"User"> is the same shape as the per-entity alias `User`. - expectType, User>>(true); - - // Spot-check a couple of fields on the resolved type. - expectType["id"], string>>(true); - expectType< - TypeEqual["accountType"], "ADMIN" | "USER"> - >(true); - expectType< - TypeEqual["gravatar_id"], string | undefined> - >(true); - - // @ts-expect-error - "NotAnEntity" is not assignable to EntityName - type _bad = Entity<"NotAnEntity">; - }); - - it("EnumName accepts schema enum names and rejects others", () => { - const _account: EnumName = "AccountType"; - const _size: EnumName = "GravatarSize"; - expectType(_account); - expectType(_size); - - // @ts-expect-error - "NotAnEnum" is not in the schema - const _bad: EnumName = "NotAnEnum"; - }); - - it("Enum resolves to the schema enum's value union", () => { - expectType, "ADMIN" | "USER">>(true); - expectType< - TypeEqual, "SMALL" | "MEDIUM" | "LARGE"> - >(true); - - // @ts-expect-error - "NotAnEnum" is not assignable to EnumName - type _bad = Enum<"NotAnEnum">; - }); -}); - -describe("Config-bound types: EvmContractName", () => { - it("EvmContractName is the union of configured EVM contract names", () => { - expectType< - TypeEqual< - EvmContractName, - | "NftFactory" - | "EventFiltersTest" - | "SimpleNft" - | "TestEvents" - | "Gravatar" - | "Noop" - > - >(true); - - // @ts-expect-error - "NotAContract" is not configured - const _bad: EvmContractName = "NotAContract"; - }); -}); - -describe("EvmOnBlock surface: Args / Context / Handler / WhereArgs", () => { - it("EvmOnBlockContext is an alias of EvmOnEventContext", () => { - expectType>(true); - }); - - it("EvmOnBlockHandlerArgs has block.number and the block context", () => { - expectType< - TypeEqual - >(true); - expectType< - TypeEqual - >(true); - }); - - it("EvmOnBlockHandler is an async function from args to void", () => { - expectType< - TypeEqual< - EvmOnBlockHandler, - (args: EvmOnBlockHandlerArgs) => Promise - > - >(true); - }); - - it("EvmOnBlockWhereArgs.chain exposes id and per-contract handles", () => { - // chain.id narrows to the configured EVM chain-id union. - expectType>(true); - expectType>( - true - ); - // Configured contracts are reachable on the chain handle. - expectType< - TypeEqual - >(true); - expectType< - TypeEqual - >(true); - }); });