envio
Version:
A latency and sync speed optimized, developer friendly blockchain data indexer.
1,240 lines (1,126 loc) • 73.8 kB
TypeScript
import * as Sury from "rescript-schema";
import type { default as BigDecimalT } from "bignumber.js";
export { default as BigDecimal } from "bignumber.js";
// Runtime value stubs used by the `S.*` namespace declarations further down
// so the exported `S.bigDecimal` / `S.bigint` consts pick up typed schemas.
// The implementations live in `index.js`, sourced from `.res.mjs` compiled
// output.
declare const bigDecimalSchema: Sury.Schema<BigDecimalT>;
declare const bigintSchema: Sury.Schema<bigint>;
/** Ethereum address — a 20-byte hex string prefixed with `0x`. */
export type Address = `0x${string}`;
/** Structured logger bound to an event or handler context. Messages are
* displayed in the console and the Envio Hosted Service. */
export type Logger = {
readonly debug: (
message: string,
params?: Record<string, unknown> | Error
) => void;
readonly info: (
message: string,
params?: Record<string, unknown> | Error
) => void;
readonly warn: (
message: string,
params?: Record<string, unknown> | Error
) => void;
readonly error: (
message: string,
params?: Record<string, unknown> | Error
) => void;
};
/** Handle for an external-call effect created via {@link createEffect}.
* Effects provide automatic deduplication, error handling, and caching. */
export declare abstract class Effect<I, O> {
protected opaque: I | O;
}
/** Calls an {@link Effect} with the given input and returns its output. */
export type EffectCaller = <I, O>(
effect: Effect<I, O>,
// This is a hack to make the call complain on undefined
// when it's not needed, instead of extending the input type.
// Might be not needed if I misunderstand something in TS.
input: I extends undefined ? undefined : I
) => Promise<O>;
/** Context passed to an Effect's handler function. */
export type EffectContext = {
/** Access the logger instance with the event as context. */
readonly log: Logger;
/** Call another Effect from inside this one. */
readonly effect: EffectCaller;
/** Whether to cache this call's result. Defaults to the effect's `cache`
* option; set to `false` to skip caching for this specific invocation. */
cache: boolean;
};
/** Rate-limit window for an {@link Effect}. Strings resolve to common
* durations; a plain `number` is treated as milliseconds. */
export type RateLimitDuration = "second" | "minute" | number;
/** Rate-limit configuration for an {@link Effect}. `false` disables rate
* limiting; otherwise a `{calls, per}` pair caps invocations per duration. */
export type RateLimit =
| false
| { readonly calls: number; readonly per: RateLimitDuration };
/** Options accepted by {@link createEffect}. */
export type EffectOptions<Input, Output> = {
/** The name of the effect. Used for logging and debugging. */
readonly name: string;
/** The input schema of the effect. */
readonly input: Sury.Schema<Input>;
/** The output schema of the effect. */
readonly output: Sury.Schema<Output>;
/** Rate limit for the effect. Set to `false` to disable or provide
* `{calls, per: "second" | "minute"}` to enable. */
readonly rateLimit: RateLimit;
/** Whether the effect should be cached. */
readonly cache?: boolean;
};
/** Arguments passed to the handler function of an {@link Effect}. */
export type EffectArgs<Input> = {
readonly input: Input;
readonly context: EffectContext;
};
export const TestHelpers: {
Addresses: {
readonly mockAddresses: readonly [
Address, Address, Address, Address, Address,
Address, Address, Address, Address, Address,
Address, Address, Address, Address, Address,
Address, Address, Address, Address, Address,
];
readonly defaultAddress: Address;
};
};
/** Utility type to expand/flatten complex types for better IDE display. */
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
/**
* Operator for filtering entity fields in getWhere queries.
* Only `id` and fields with `@index` in the schema can be queried at runtime.
*/
export type GetWhereOperator<T> = {
/** Matches entities where the field equals the given value. */
readonly _eq?: T;
/** Matches entities where the field is greater than the given value. */
readonly _gt?: T;
/** Matches entities where the field is less than the given value. */
readonly _lt?: T;
/** Matches entities where the field is greater than or equal to the given value. */
readonly _gte?: T;
/** Matches entities where the field is less than or equal to the given value. */
readonly _lte?: T;
/** Matches entities where the field equals any of the given values. */
readonly _in?: readonly T[];
};
/**
* Constructs a getWhere filter type from an entity type.
* Each field can be filtered using {@link GetWhereOperator} (`_eq`, `_gt`, `_lt`, `_gte`, `_lte`, `_in`).
*
* Note: only `id` and fields with `@index` in the schema can be queried at runtime.
* Attempting to filter on a non-indexed field will throw a descriptive error.
*/
export type GetWhereFilter<E> = {
[K in keyof E]?: GetWhereOperator<E[K]>;
};
type UnknownToOutput<T> = T extends Sury.Schema<unknown>
? Sury.Output<T>
: T extends (...args: any[]) => any
? T
: T extends unknown[]
? { [K in keyof T]: UnknownToOutput<T[K]> }
: T extends { [k in keyof T]: unknown }
? Flatten<
{
[k in keyof T as HasUndefined<UnknownToOutput<T[k]>> extends true
? k
: never]?: UnknownToOutput<T[k]>;
} & {
[k in keyof T as HasUndefined<UnknownToOutput<T[k]>> extends true
? never
: k]: UnknownToOutput<T[k]>;
}
>
: T;
type HasUndefined<T> = [T] extends [undefined]
? true
: undefined extends T
? true
: false;
// Utility to flatten the type into a single object
type Flatten<T> = T extends object
? { [K in keyof T as T[K] extends never ? never : K]: T[K] }
: T;
// All the type gymnastics with generics to be able to
// define schema without an additional `S.schema` call in TS:
// createEffect({
// input: undefined,
// })
// Instead of:
// createEffect({
// input: S.schema(undefined),
// })
// Or for objects:
// createEffect({
// input: {
// foo: S.string,
// },
// })
// Instead of:
// createEffect({
// input: S.schema({
// foo: S.string,
// }),
// })
// The behaviour is inspired by Sury code:
// https://github.com/DZakh/sury/blob/551f8ee32c1af95320936d00c086e5fb337f59fa/packages/sury/src/S.d.ts#L344C1-L355C50
export function createEffect<
IS,
OS,
I = UnknownToOutput<IS>,
O = UnknownToOutput<OS>,
// A hack to enforce that the inferred return type
// matches the output schema type
R extends O = O
>(
options: {
/** The name of the effect. Used for logging and debugging. */
readonly name: string;
/** The input schema of the effect. */
readonly input: IS;
/** The output schema of the effect. */
readonly output: OS;
/** Rate limit for the effect. Set to false to disable or provide {calls: number, per: "second" | "minute"} to enable. */
readonly rateLimit: RateLimit;
/** Whether the effect should be cached. */
readonly cache?: boolean;
},
handler: (args: EffectArgs<I>) => Promise<R>
): Effect<I, O>;
// Important! Should match the index.js file
export declare namespace S {
export type Output<T> = Sury.Output<T>;
export type Infer<T> = Sury.Output<T>;
export type Input<T> = Sury.Input<T>;
export type Schema<Output, Input = unknown> = Sury.Schema<Output, Input>;
export const string: typeof Sury.string;
export const address: Sury.Schema<Address, Address>;
// export const evmChainId: Sury.Schema<EvmChainId, EvmChainId>;
// export const fuelChainId: Sury.Schema<FuelChainId, FuelChainId>;
// export const svmChainId: Sury.Schema<SvmChainId, SvmChainId>;
export const jsonString: typeof Sury.jsonString;
export const boolean: typeof Sury.boolean;
export const int32: typeof Sury.int32;
export const number: typeof Sury.number;
export const bigint: typeof bigintSchema;
export const never: typeof Sury.never;
export const union: typeof Sury.union;
export const object: typeof Sury.object;
// Might change in a near future
// export const custom: typeof Sury.custom;
// Don't expose recursive for now, since it's too advanced
// export const recursive: typeof Sury.recursive;
export const transform: typeof Sury.transform;
export const shape: typeof Sury.shape;
export const refine: typeof Sury.refine;
export const schema: typeof Sury.schema;
export const record: typeof Sury.record;
export const array: typeof Sury.array;
export const tuple: typeof Sury.tuple;
export const merge: typeof Sury.merge;
export const optional: typeof Sury.optional;
export function nullable<Output, Input>(
schema: Sury.Schema<Output, Input>
): Sury.Schema<Output | null, Input | null>;
export const bigDecimal: typeof bigDecimalSchema;
export const unknown: typeof Sury.unknown;
// Nullish type will change in "sury@10"
// export const nullish: typeof Sury.nullish;
export const assertOrThrow: typeof Sury.assertOrThrow;
export const parseOrThrow: typeof Sury.parseOrThrow;
}
// ============== Indexer Config (Module Augmentation) ==============
/**
* Internal augmentation surface populated by `.envio/types.d.ts` (via
* codegen) so the project-bound aliases below resolve to concrete chain /
* contract / entity / enum types.
*
* Do not augment manually. If a project-bound type like {@link Indexer},
* {@link Entities}, or {@link EvmChainName} resolves to an error string,
* run `envio codegen` (or your package manager's `codegen` script, e.g.
* `pnpm codegen`) to regenerate `.envio/types.d.ts`.
*/
export interface Global {}
/** Lookup table extracted from {@link Global} — empty when not yet augmented. */
type GlobalConfig = Global extends { config: infer C extends IndexerConfigTypes }
? C
: {};
/** Error-message string returned by project-bound aliases when codegen has
* not run yet. Resolves to `string` so handler signatures stay assignable.
* Wording is package-manager-neutral — `envio init` lets users pick pnpm,
* npm, yarn, or bun, so the hint refers to the codegen invocation rather
* than a specific PM. */
type NotConfigured<TName extends string, THint extends string> =
`${TName} is not available. ${THint} in config.yaml and run 'envio codegen'`;
type IsEmptyObject<T> = keyof T extends never ? true : false;
/**
* Shape of the indexer configuration used internally for defineConfig.
* This models only the subset of fields defineConfig consumes; the JSON
* emitted by `envio config view` is a superset (enums, entities, per-chain
* sources, event metadata, EVM global field selections, and other
* serializer-only fields are intentionally omitted here).
*/
type IndexerConfig = {
/** The indexer name. */
name: string;
/** The indexer description. */
description?: string;
/** Path to handlers directory for auto-loading (default: "src/handlers"). */
handlers?: string;
/** Target batch size for event processing (default: 5000). */
fullBatchSize?: number;
/** Whether to rollback on chain reorg (default: true). */
rollbackOnReorg?: boolean;
/** Whether to save full entity history (default: false). */
saveFullHistory?: boolean;
/** Whether raw events are enabled (default: false). */
rawEvents?: boolean;
/** EVM ecosystem configuration. */
evm?: {
/** Chain configurations keyed by chain name. */
chains: Record<string, EvmChainConfig>;
/** Contract configurations keyed by contract name. */
contracts?: Record<string, EvmContractConfig>;
/** Address format (default: "checksum"). */
addressFormat?: "lowercase" | "checksum";
};
/** Fuel ecosystem configuration. */
fuel?: {
/** Chain configurations keyed by chain name. */
chains: Record<string, FuelChainConfig>;
/** Contract configurations keyed by contract name. */
contracts?: Record<string, FuelContractConfig>;
};
/** SVM ecosystem configuration. */
svm?: {
/** Chain configurations keyed by chain name. */
chains: Record<string, SvmChainConfig>;
};
};
// ============== Contract Types ==============
/** EVM contract configuration. */
type EvmContractConfig = {
/** The contract ABI. */
readonly abi: unknown;
};
/** Fuel contract configuration. */
type FuelContractConfig = {
/** The contract ABI. */
readonly abi: unknown;
};
// ============== EVM Types ==============
/** EVM chain configuration (for IndexerConfig). */
type EvmChainConfig<Id extends number = number> = {
/** The chain ID. */
readonly id: Id;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock?: number;
/** Number of blocks to keep for reorg handling (default: 200). */
readonly maxReorgDepth?: number;
/** Number of blocks behind the chain head to lag (default: 0). */
readonly blockLag?: number;
};
/** EVM chain value (for runtime Indexer). */
type EvmChain<
Id extends number = number,
ContractName extends string = never
> = {
/** The chain ID. */
readonly id: Id;
/** The chain name. */
readonly name: string;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock: number | undefined;
/** Whether all chains have entered real-time indexing mode (caught up to head,
* or reached their configured endBlock for finite-range indexers). */
readonly isRealtime: boolean;
} & {
readonly [K in ContractName]: EvmContract<K>;
};
/** EVM contract (for runtime Indexer). */
type EvmContract<Name extends string = string> = {
/** The contract name. */
readonly name: Name;
/** The contract ABI. */
readonly abi: readonly unknown[];
/** The contract addresses. */
readonly addresses: readonly Address[];
};
/** Fuel contract (for runtime Indexer). */
type FuelContract<Name extends string = string> = {
/** The contract name. */
readonly name: Name;
/** The contract ABI. */
readonly abi: unknown;
/** The contract addresses. */
readonly addresses: readonly Address[];
};
// ============== Fuel Types ==============
/** Fuel chain configuration (for IndexerConfig). */
type FuelChainConfig<Id extends number = number> = {
/** The chain ID. */
readonly id: Id;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock?: number;
/** Number of blocks to keep for reorg handling (default: 200). */
readonly maxReorgDepth?: number;
/** Number of blocks behind the chain head to lag (default: 0). */
readonly blockLag?: number;
};
/** Fuel chain value (for runtime Indexer). */
type FuelChain<
Id extends number = number,
ContractName extends string = never
> = {
/** The chain ID. */
readonly id: Id;
/** The chain name. */
readonly name: string;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock: number | undefined;
/** Whether all chains have entered real-time indexing mode (caught up to head,
* or reached their configured endBlock for finite-range indexers). */
readonly isRealtime: boolean;
} & {
readonly [K in ContractName]: FuelContract<K>;
};
// ============== SVM (Solana) Types ==============
/** SVM chain configuration (for IndexerConfig). */
type SvmChainConfig<Id extends number = number> = {
/** The chain ID. */
readonly id: Id;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock?: number;
/** Number of blocks to keep for reorg handling (default: 200). */
readonly maxReorgDepth?: number;
/** Number of blocks behind the chain head to lag (default: 0). */
readonly blockLag?: number;
};
/** SVM chain value (for runtime Indexer). */
type SvmChain<Id extends number = number> = {
/** The chain ID. */
readonly id: Id;
/** The chain name. */
readonly name: string;
/** The block number indexing starts from. */
readonly startBlock: number;
/** The block number indexing stops at (if configured). */
readonly endBlock: number | undefined;
/** Whether all chains have entered real-time indexing mode (caught up to head,
* or reached their configured endBlock for finite-range indexers). */
readonly isRealtime: boolean;
};
// ============== Indexer Type ==============
/** Minimal type constraint for IndexerFromConfig to allow usage without full IndexerConfig. */
type IndexerConfigTypes = {
evm?: {
chains: Record<string, { id: number }>;
contracts?: Record<string, Record<string, { eventName: string }>>;
eventFilters?: Record<string, Record<string, { readonly params: object }>>;
};
fuel?: {
chains: Record<string, { id: number }>;
contracts?: Record<string, Record<string, { eventName: string }>>;
eventFilters?: Record<string, Record<string, { readonly params: object }>>;
};
svm?: { chains: Record<string, { id: number }> };
entities?: Record<string, object>;
enums?: Record<string, string>;
};
// ============== onEvent / contractRegister Types ==============
// Extract contracts type from config
type EvmContracts<Config extends IndexerConfigTypes = GlobalConfig> =
Config["evm"] extends { contracts: infer C extends Record<string, Record<string, any>> }
? C : {};
type FuelContracts<Config extends IndexerConfigTypes = GlobalConfig> =
Config["fuel"] extends { contracts: infer C extends Record<string, Record<string, any>> }
? C : {};
// Extract eventFilters type from config — a sibling lookup table that maps
// contract+event to the `where` filter shape `{ params: { ... } }`. Split
// out from `EvmContracts` so per-event entries stay focused on the event
// payload and keep the two lookup tables independently composable.
type EvmEventFilters<Config extends IndexerConfigTypes = GlobalConfig> =
Config["evm"] extends { eventFilters: infer F extends Record<string, Record<string, any>> }
? F : {};
type FuelEventFilters<Config extends IndexerConfigTypes = GlobalConfig> =
Config["fuel"] extends { eventFilters: infer F extends Record<string, Record<string, any>> }
? F : {};
// Extract contract names for contract registration
type EvmContractNames<Config extends IndexerConfigTypes = GlobalConfig> =
Config["evm"] extends { contracts: Record<infer N, any> } ? N & string : never;
type FuelContractNames<Config extends IndexerConfigTypes = GlobalConfig> =
Config["fuel"] extends { contracts: Record<infer N, any> } ? N & string : never;
/** Event identity for onEvent/contractRegister calls. */
type EventIdentity<
Contracts extends Record<string, Record<string, any>>,
C extends keyof Contracts = keyof Contracts,
E extends keyof Contracts[C] & string = keyof Contracts[C] & string
> = {
/** The contract name as defined in config.yaml. */
readonly contract: C;
/** The event name as defined in the contract ABI. */
readonly event: E;
/** Whether to process all events (wildcard mode). */
readonly wildcard?: boolean;
};
/**
* Shared shape for handler contexts across ecosystems — logger, effect
* caller, preload flag, chain state, and the entity operations map derived
* from the project schema.
*/
type BaseHandlerContext<Config extends IndexerConfigTypes = GlobalConfig, ChainId = unknown> = {
/** Access the logger instance. */
readonly log: Logger;
/** Call an Effect with the given input. */
readonly effect: EffectCaller;
/** True when running in preload mode (parallel pre-run for cache population). */
readonly isPreload: boolean;
/** Chain state for the current event's chain. */
readonly chain: {
readonly id: ChainId;
readonly isRealtime: boolean;
};
} & {
readonly [K in keyof ConfigEntities<Config>]: EntityOperations<ConfigEntities<Config>[K]>;
};
/** Context for onEvent handlers. Includes entity operations, logging, and chain info. */
export type EvmOnEventContext<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<
BaseHandlerContext<Config, EvmChainIds<Config>>
>;
/** Context for onEvent handlers in Fuel ecosystem. */
export type FuelOnEventContext<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<
BaseHandlerContext<Config, FuelChainIds<Config>>
>;
/** Context for `indexer.onSlot` handlers in SVM ecosystem. */
export type SvmOnSlotContext<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<
BaseHandlerContext<Config, SvmChainIds<Config>>
>;
/** Entity operations available in handler contexts. */
type EntityOperations<Entity> = {
readonly get: (id: string) => Promise<Entity | undefined>;
readonly getOrThrow: (id: string, message?: string) => Promise<Entity>;
readonly getWhere: (filter: GetWhereFilter<Entity>) => Promise<Entity[]>;
readonly getOrCreate: (entity: Entity) => Promise<Entity>;
readonly set: (entity: Entity) => void;
readonly deleteUnsafe: (id: string) => void;
};
/** Contract registration handle. */
type ContractRegistration = {
/** Register a new contract address for dynamic indexing. */
readonly add: (address: Address) => void;
};
/** Context for contractRegister handlers. Chain object includes contract registration methods.
* `isRealtime` is intentionally absent: contract registration runs during historical sync,
* so the "realtime" distinction isn't meaningful and the runtime does not expose it. */
export type EvmContractRegisterContext<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<{
readonly log: Logger;
readonly chain: {
readonly id: EvmChainIds<Config>;
} & {
readonly [K in EvmContractNames<Config>]: ContractRegistration;
};
}>;
/** Context for contractRegister handlers in Fuel ecosystem. `isRealtime` is intentionally
* absent — see EvmContractRegisterContext. */
export type FuelContractRegisterContext<Config extends IndexerConfigTypes = GlobalConfig> = Prettify<{
readonly log: Logger;
readonly chain: {
readonly id: FuelChainIds<Config>;
} & {
readonly [K in FuelContractNames<Config>]: ContractRegistration;
};
}>;
// ============== onEvent / contractRegister Named Types ==============
/** Constraint: any event must have literal contractName and eventName fields. */
type EventLike = { readonly contractName: string; readonly eventName: string };
/** Scalar value or array of values — used by event filter fields to accept either a
* single topic or multiple alternatives (OR semantics). */
export type SingleOrMultiple<T> = T | readonly T[];
/** EVM event type resolved by contract and event name. Union of all events when no generics provided.
* The mapped form distributes `K in C` so disjoint event sets across contracts survive — using
* `EvmContracts<Config>[C][E]` directly would collapse to keys common to *all* contracts (often `never`). */
export type EvmOnEvent<
Config extends IndexerConfigTypes = GlobalConfig,
C extends keyof EvmContracts<Config> = keyof EvmContracts<Config>,
E extends string = string
> = {
[K in C]: EvmContracts<Config>[K][E & keyof EvmContracts<Config>[K]];
}[C];
/** The chain object passed into the EVM dynamic `where` callback form. Exposes
* the chain `id` and the event's own contract under its capitalized name,
* with `addresses` listing the indexed contract addresses on this chain.
*
* Only the event's own contract is exposed — multi-contract address
* filtering is not supported in this iteration. */
export type EvmOnEventWhereChain<ContractName extends string> = {
readonly id: number;
} & {
readonly [K in ContractName]: { readonly addresses: readonly Address[] };
};
/** Arguments passed to the EVM dynamic `where` callback form. Return an
* `EvmOnEventWhereFilter` to apply a filter, or `true` / `false` to keep / skip
* all events for that invocation. */
export type EvmOnEventWhereArgs<ContractName extends string> = {
readonly chain: EvmOnEventWhereChain<ContractName>;
};
/** A single EVM `where` filter condition. `params` accepts either a single
* AND-conjunction of indexed-parameter narrowings, or an array of them (OR
* semantics). `block.number._gte` promotes to the event's startBlock and
* overrides the contract-level `start_block` — use it to restrict per-event
* processing without touching `config.yaml`. Only `_gte` is supported on
* event filters; use `indexer.onBlock` for `_lte` / `_every`. */
export type EvmOnEventWhereFilter<Params> = {
readonly params?: Params | readonly Params[];
readonly block?: {
readonly number?: {
readonly _gte?: number;
};
};
};
/** The `where` option value of `indexer.onEvent` / `indexer.contractRegister`
* in the EVM ecosystem.
*
* TypeScript accepts either a static filter object or a dynamic callback.
* The dynamic callback may return a boolean to keep (`true`) or skip (`false`)
* all events on that invocation, or an `EvmOnEventWhereFilter` for narrowing.
*
* The ReScript surface only exposes the callback form — multi-condition OR
* semantics are always expressed via an array on `params`, not at the top
* level of `where`. */
export type EvmOnEventWhere<Params, ContractName extends string> =
| EvmOnEventWhereFilter<Params>
| ((args: EvmOnEventWhereArgs<ContractName>) => EvmOnEventWhereFilter<Params> | boolean);
/** The chain object passed into the Fuel dynamic `where` callback form. */
export type FuelOnEventWhereChain<ContractName extends string> = EvmOnEventWhereChain<ContractName>;
/** Arguments passed to the Fuel dynamic `where` callback form. */
export type FuelOnEventWhereArgs<ContractName extends string> = EvmOnEventWhereArgs<ContractName>;
/** A single Fuel `where` filter condition. Keyed on `block.height` instead
* of `block.number`. */
export type FuelOnEventWhereFilter<Params> = {
readonly params?: Params | readonly Params[];
readonly block?: {
readonly height?: {
readonly _gte?: number;
};
};
};
/** The `where` option value of `indexer.onEvent` / `indexer.contractRegister` in the Fuel ecosystem. */
export type FuelOnEventWhere<Params, ContractName extends string> =
| FuelOnEventWhereFilter<Params>
| ((args: FuelOnEventWhereArgs<ContractName>) => FuelOnEventWhereFilter<Params> | boolean);
// When the matching ecosystem isn't configured, `EvmEvent` / `FuelEvent` resolve
// to the error-message string. Filter back to a structural `EventLike` so the
// default-bound option / handler aliases stay usable in non-configured cases.
type _ProjectEvmEvent = EvmEvent extends EventLike ? EvmEvent : never;
type _ProjectFuelEvent = FuelEvent extends EventLike ? FuelEvent : never;
/** Options for registering an EVM onEvent handler. Contract and event literal names are derived from the Event type.
* The conditional `Event extends EventLike` distributes over union members so that each member's
* contractName/eventName pair is constrained together — preventing invalid cross-member pairings.
* The `Params` generic carries the indexed-parameter shape (looked up via `EvmEventFilters[C][E]["params"]`
* by callers) so the `where` option enforces the same per-event narrowing as the inline handler signature. */
export type EvmOnEventOptions<Event extends EventLike = _ProjectEvmEvent, Params = {}> = Event extends EventLike
? {
readonly contract: Event["contractName"];
readonly event: Event["eventName"];
readonly wildcard?: boolean;
readonly where?: EvmOnEventWhere<Params, Event["contractName"] & string>;
}
: never;
/** Handler function for an EVM onEvent registration. Context is provided as a separate generic so the project alias can bind it. */
export type EvmOnEventHandler<
Event extends EventLike = _ProjectEvmEvent,
Context = EvmOnEventContext,
> = (args: { event: Event; context: Context }) => Promise<void>;
/** Options for registering an EVM contractRegister handler. Same shape as EvmOnEventOptions. */
export type EvmContractRegisterOptions<
Event extends EventLike = _ProjectEvmEvent,
Params = {},
> = EvmOnEventOptions<Event, Params>;
/** Handler function for an EVM contractRegister registration. */
export type EvmContractRegisterHandler<
Event extends EventLike = _ProjectEvmEvent,
Context = EvmContractRegisterContext,
> = EvmOnEventHandler<Event, Context>;
/** Fuel event type resolved by contract and event name. Same distributive-mapped pattern as `EvmOnEvent`. */
export type FuelOnEvent<
Config extends IndexerConfigTypes = GlobalConfig,
C extends keyof FuelContracts<Config> = keyof FuelContracts<Config>,
E extends string = string
> = {
[K in C]: FuelContracts<Config>[K][E & keyof FuelContracts<Config>[K]];
}[C];
/** Options for registering a Fuel onEvent handler. Mirrors `EvmOnEventOptions`
* but binds the `where` filter to `FuelOnEventWhere` so block ranges read
* `block.height` (Fuel) instead of `block.number` (EVM). */
export type FuelOnEventOptions<
Event extends EventLike = _ProjectFuelEvent,
Params = {},
> = Event extends EventLike
? {
readonly contract: Event["contractName"];
readonly event: Event["eventName"];
readonly wildcard?: boolean;
readonly where?: FuelOnEventWhere<Params, Event["contractName"] & string>;
}
: never;
/** Handler function for a Fuel onEvent registration. */
export type FuelOnEventHandler<
Event extends EventLike = _ProjectFuelEvent,
Context = FuelOnEventContext,
> = EvmOnEventHandler<Event, Context>;
/** Options for registering a Fuel contractRegister handler. Same shape as
* `FuelOnEventOptions` so the `where` filter uses `block.height`. */
export type FuelContractRegisterOptions<
Event extends EventLike = _ProjectFuelEvent,
Params = {},
> = FuelOnEventOptions<Event, Params>;
/** Handler function for a Fuel contractRegister registration. */
export type FuelContractRegisterHandler<
Event extends EventLike = _ProjectFuelEvent,
Context = FuelContractRegisterContext,
> = EvmOnEventHandler<Event, Context>;
// ============== EVM onBlock types ==============
/**
* Structured filter object returned by an EVM `indexer.onBlock` `where`
* predicate. `_every` alignment is relative to `_gte` (or the chain's
* configured `startBlock` when `_gte` is omitted), preserving
* `(blockNumber - startBlock) % _every === 0`.
*/
export type EvmOnBlockFilter = {
readonly block?: {
readonly number?: {
/** Matches blocks whose number is greater than or equal to the given value. */
readonly _gte?: number;
/** Matches blocks whose number is less than or equal to the given value. */
readonly _lte?: number;
/** Match every Nth block. Alignment is relative to `_gte`. */
readonly _every?: number;
};
};
};
/**
* Return type of an EVM `indexer.onBlock` `where` predicate. The predicate
* must explicitly return — an implicit `undefined` is not accepted.
* - `false` → skip this chain entirely.
* - `true` → register on the chain with no extra filter.
* - {@link EvmOnBlockFilter} → register with the given range/stride.
*/
export type EvmOnBlockWhereResult = boolean | EvmOnBlockFilter;
/** Argument passed to an EVM `indexer.onBlock` `where` predicate. */
export type EvmOnBlockWhereArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Configured chain being evaluated. Use `chain.id` to branch per chain. */
readonly chain: EvmChain<EvmChainIds<Config>, EvmContractNames<Config>>;
};
/** Context for EVM `indexer.onBlock` handlers. Alias of {@link EvmOnEventContext}. */
export type EvmOnBlockContext<Config extends IndexerConfigTypes = GlobalConfig> = EvmOnEventContext<Config>;
/** Arguments passed to an EVM block handler. */
export type EvmOnBlockHandlerArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Block being processed. Contains the block number; extended fields are
opt-in via `field_selection` in config.yaml. */
readonly block: { readonly number: number };
/** Handler context: entity operations, logger, effect caller, chain state. */
readonly context: EvmOnBlockContext<Config>;
};
/** Handler function for an EVM `indexer.onBlock` registration. */
export type EvmOnBlockHandler<Config extends IndexerConfigTypes = GlobalConfig> = (
args: EvmOnBlockHandlerArgs<Config>,
) => Promise<void>;
/** Options for an EVM `indexer.onBlock` registration. */
export type EvmOnBlockOptions<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Unique name for this block handler. Used as the key in error messages
and in persisted progress tracking. */
readonly name: string;
/** Optional predicate evaluated once per configured chain at registration
time. Return `false` to skip a chain, `true` to match every block, or
a filter object to restrict by block number range and stride. */
readonly where?: (args: EvmOnBlockWhereArgs<Config>) => EvmOnBlockWhereResult;
};
// ============== Fuel onBlock types ==============
/**
* Structured filter object returned by a Fuel `indexer.onBlock` `where`
* predicate. `_every` alignment is relative to `_gte` (or the chain's
* configured `startBlock` when `_gte` is omitted), preserving
* `(blockNumber - startBlock) % _every === 0`.
*/
export type FuelOnBlockFilter = {
readonly block?: {
readonly height?: {
/** Matches blocks whose height is greater than or equal to the given value. */
readonly _gte?: number;
/** Matches blocks whose height is less than or equal to the given value. */
readonly _lte?: number;
/** Match every Nth block. Alignment is relative to `_gte`. */
readonly _every?: number;
};
};
};
/**
* Return type of a Fuel `indexer.onBlock` `where` predicate. The predicate
* must explicitly return — an implicit `undefined` is not accepted.
* - `false` → skip this chain.
* - `true` → register on the chain with no extra filter.
* - {@link FuelOnBlockFilter} → register with the given range/stride.
*/
export type FuelOnBlockWhereResult = boolean | FuelOnBlockFilter;
/** Argument passed to a Fuel `indexer.onBlock` `where` predicate. */
export type FuelOnBlockWhereArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Configured chain being evaluated. Use `chain.id` to branch per chain. */
readonly chain: FuelChain<FuelChainIds<Config>, FuelContractNames<Config>>;
};
/** Context for Fuel `indexer.onBlock` handlers. Alias of {@link FuelOnEventContext}. */
export type FuelOnBlockContext<Config extends IndexerConfigTypes = GlobalConfig> = FuelOnEventContext<Config>;
/** Arguments passed to a Fuel block handler. */
export type FuelOnBlockHandlerArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Block being processed. Contains the block height; extended fields are
opt-in via `field_selection` in config.yaml. */
readonly block: { readonly height: number };
/** Handler context: entity operations, logger, effect caller, chain state. */
readonly context: FuelOnBlockContext<Config>;
};
/** Handler function for a Fuel `indexer.onBlock` registration. */
export type FuelOnBlockHandler<Config extends IndexerConfigTypes = GlobalConfig> = (
args: FuelOnBlockHandlerArgs<Config>,
) => Promise<void>;
/** Options for a Fuel `indexer.onBlock` registration. */
export type FuelOnBlockOptions<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Unique name for this block handler. Used as the key in error messages
and in persisted progress tracking. */
readonly name: string;
/** Optional predicate evaluated once per configured chain at registration
time. Return `false` to skip a chain, `true` to match every block, or
a filter object to restrict by block height range and stride. */
readonly where?: (args: FuelOnBlockWhereArgs<Config>) => FuelOnBlockWhereResult;
};
// ============== SVM onSlot types ==============
/**
* Structured filter object returned by an SVM `indexer.onSlot` `where`
* predicate. `_every` alignment is relative to `_gte` (or the chain's
* configured `startBlock` when `_gte` is omitted), preserving
* `(slot - startBlock) % _every === 0`.
*/
export type SvmOnSlotFilter = {
readonly slot?: {
/** Matches slots whose number is greater than or equal to the given value. */
readonly _gte?: number;
/** Matches slots whose number is less than or equal to the given value. */
readonly _lte?: number;
/** Match every Nth slot. Alignment is relative to `_gte`. */
readonly _every?: number;
};
};
/**
* Return type of an SVM `indexer.onSlot` `where` predicate. The predicate
* must explicitly return — an implicit `undefined` is not accepted.
* - `false` → skip this chain.
* - `true` → register on the chain with no extra filter.
* - {@link SvmOnSlotFilter} → register with the given range/stride.
*/
export type SvmOnSlotWhereResult = boolean | SvmOnSlotFilter;
/** Argument passed to an SVM `indexer.onSlot` `where` predicate. */
export type SvmOnSlotWhereArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Configured chain being evaluated. Use `chain.id` to branch per chain. */
readonly chain: SvmChain<SvmChainIds<Config>>;
};
/** Arguments passed to an SVM slot handler. */
export type SvmOnSlotHandlerArgs<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Slot number being processed. */
readonly slot: number;
/** Handler context: entity operations, logger, effect caller, chain state. */
readonly context: SvmOnSlotContext<Config>;
};
/** Handler function for an SVM `indexer.onSlot` registration. */
export type SvmOnSlotHandler<Config extends IndexerConfigTypes = GlobalConfig> = (
args: SvmOnSlotHandlerArgs<Config>,
) => Promise<void>;
/** Options for an SVM `indexer.onSlot` registration. */
export type SvmOnSlotOptions<Config extends IndexerConfigTypes = GlobalConfig> = {
/** Unique name for this slot handler. Used as the key in error messages
and in persisted progress tracking. */
readonly name: string;
/** Optional predicate evaluated once per configured chain at registration
time. Return `false` to skip a chain, `true` to match every slot, or
a filter object to restrict by slot range and stride. */
readonly where?: (args: SvmOnSlotWhereArgs<Config>) => SvmOnSlotWhereResult;
};
// ============== SVM onInstruction types ==============
/** Borsh-decoded params view of an instruction. Present whenever a
* `ProgramSchema` was attached to the program (bundled, Anchor IDL, or
* hand-written `accounts`/`args` in YAML). Absent when no schema applies or
* the discriminator didn't match any registered instruction. */
export type SvmInstructionParams = {
/** Schema-declared instruction name. */
readonly name: string;
/** Borsh-decoded args object. POC types this as `unknown`; narrow with a
* locally-declared type until the typed-args codegen lands. */
readonly args: unknown;
/** Named accounts in schema order. Keys are exactly the schema-declared
* names; values are base58 pubkeys. */
readonly accounts: Readonly<Record<string, string>>;
/** Accounts beyond the schema's named list (Anchor `remaining_accounts`,
* IDL drift). Empty when counts match the schema. */
readonly extraAccounts: readonly string[];
};
/** Block context for a matched instruction. */
export type SvmInstructionBlock = {
/** Slot this instruction's block was matched in. */
readonly slot: number;
readonly time: number;
/** Always empty for now — reserved for the future reorg-guard route. */
readonly hash: string;
};
export type SvmTokenBalance = {
readonly account?: string;
readonly mint?: string;
readonly owner?: string;
/** u64 decimal string. Cast with BigInt(...) for arithmetic. */
readonly preAmount?: string;
readonly postAmount?: string;
};
/** Parent transaction surfaced when an instruction's
* `include_transaction` flag is `true`. */
export type SvmTransaction = {
readonly signatures: readonly string[];
readonly feePayer?: string;
readonly success?: boolean;
readonly err?: string;
/** Lamports. */
readonly fee?: bigint;
readonly computeUnitsConsumed?: bigint;
readonly accountKeys: readonly string[];
readonly recentBlockhash?: string;
readonly version?: string;
/** SPL Token / Token-2022 balance snapshots for this transaction.
* Present when `include_token_balances` is `true`. */
readonly tokenBalances?: readonly SvmTokenBalance[];
};
export type SvmLog = {
readonly kind: string;
readonly message: string;
};
/** A single Solana instruction delivered to an `onInstruction` handler.
*
* Carries the matched instruction's own fields (`programId`, `data`,
* `accounts`, discriminator prefixes, `params`) plus the program/instruction
* names, parent transaction, scoped logs, and block context. Parameterised
* over `Params` so the per-(program, instruction) overload of
* `onInstruction` can narrow `instruction.params` to the codegen-generated
* `{ args, accounts }` shape.
*
* `data` and discriminator prefixes are `0x`-prefixed hex strings; accounts
* are base58 strings. */
export type SvmInstruction<
Params extends SvmInstructionParams = SvmInstructionParams,
> = {
/** Program name as declared under `programs[].name` in `config.yaml`. */
readonly programName: string;
/** Instruction name as declared under `instructions[].name` in
* `config.yaml`. */
readonly instructionName: string;
readonly programId: string;
readonly data: string;
readonly accounts: readonly string[];
readonly instructionAddress: readonly number[];
readonly isInner: boolean;
readonly d1?: string;
readonly d2?: string;
readonly d4?: string;
readonly d8?: string;
/** Borsh-decoded params. Present when a schema is configured and matched. */
readonly params?: Params;
/** Present when the instruction's `include_transaction` is `true`. */
readonly transaction?: SvmTransaction;
/** Present when the instruction's `include_logs` is `true`; only logs
* scoped to this exact instruction (matching `instruction_address`). */
readonly logs?: readonly SvmLog[];
readonly block: SvmInstructionBlock;
};
/** Arguments passed to handlers registered via `indexer.onInstruction`. */
export type SvmOnInstructionHandlerArgs<
Config extends IndexerConfigTypes = GlobalConfig,
Instr extends SvmInstruction = SvmInstruction,
> = {
readonly instruction: Instr;
readonly context: SvmOnSlotContext<Config>;
};
/** Shape extracted from `Global.config.svm.programs[P][I]`. The codegen
* emits `{ args: ...; accounts: ... }` per (program, instruction); this
* helper turns that into a `SvmInstructionParams`-compatible record. */
type SvmParamsFromProgramTable<TInstr> = TInstr extends {
args: infer A;
accounts: infer Acc extends Readonly<Record<string, string>>;
}
? {
readonly name: string;
readonly args: A;
readonly accounts: Acc;
readonly extraAccounts: readonly string[];
}
: SvmInstructionParams;
/** Options for an SVM `indexer.onInstruction` registration. */
export type SvmOnInstructionOptions<P extends string = string, I extends string = string> = {
/** Program name as declared under `chains[].programs[].name` in
* `config.yaml`. */
readonly program: P;
/** Instruction name as declared under
* `chains[].programs[].instructions[].name` in `config.yaml`. */
readonly instruction: I;
};
/** Handler function for an SVM `indexer.onInstruction` registration. */
export type SvmOnInstructionHandler<
Config extends IndexerConfigTypes = GlobalConfig,
> = (args: SvmOnInstructionHandlerArgs<Config>) => Promise<void>;
// ============== Indexer Types ==============
// Helper: Check if an ecosystem is configured. Single-ecosystem indexers only
// — see `SingleEcosystemChains` for how the result is consumed.
type HasEvm<Config> = "evm" extends keyof Config ? true : false;
type HasFuel<Config> = "fuel" extends keyof Config ? true : false;
type HasSvm<Config> = "svm" extends keyof Config ? true : false;
// EVM ecosystem type — includes chains plus handler registration methods.
// NOTE: options use inline { contract: C; event: E } shape for TypeScript inference.
// Using EvmOnEventOptions<Contracts[C][E]> would break inference since C/E can't be
// derived from indexed access types. The named EvmOnEventOptions type is for end-user
// reference; the inline shape here is structurally identical.
type EvmEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
"evm" extends keyof Config
? Config["evm"] extends {
chains: infer Chains;
contracts?: Record<infer ContractName, any>;
}
? Chains extends Record<string, { id: number }>
? {
/** Array of all EVM chain IDs. */
readonly chainIds: readonly Chains[keyof Chains]["id"][];
/** Per-chain configuration keyed by chain name or ID. */
readonly chains: {
readonly [K in Chains[keyof Chains]["id"]]: EvmChain<
K,
ContractName extends string ? ContractName : never
>;
} & {
readonly [K in keyof Chains]: EvmChain<
Chains[K]["id"],
ContractName extends string ? ContractName : never
>;
};
/**
* Register a block handler. `where` is evaluated once per configured
* chain at registration time; return `false` to skip a chain, `true`
* to match every block, or an {@link EvmOnBlockFilter} describing a
* block-number range and stride. Always available regardless of
* whether `contracts` are configured — block handlers don't need
* any contract context.
*/
readonly onBlock: (
options: EvmOnBlockOptions<Config>,
handler: EvmOnBlockHandler<Config>,
) => void;
} & (Config["evm"] extends {
contracts: infer Contracts extends Record<string, Record<string, any>>;
}
? {
/** Register an event handler. */
readonly onEvent: <
C extends keyof Contracts & string,
E extends keyof Contracts[C] & string
>(
options: {
readonly contract: C;
readonly event: E;
readonly wildcard?: boolean;
readonly where?: EvmOnEventWhere<
EvmEventFilters<Config>[C] extends Record<string, any>
? EvmEventFilters<Config>[C][E & keyof EvmEventFilters<Config>[C]] extends { readonly params: infer P }
? P
: {}
: {},
C
>;
},
handler: EvmOnEventHandler<Contracts[C][E], EvmOnEventContext<Config>>
) => void;
/** Register a contract register handler for dynamic contract indexing. */
readonly contractRegister: <
C extends keyof Contracts & string,
E extends keyof Contracts[C] & string
>(
options: {
readonly contract: C;
readonly event: E;
readonly wildcard?: boolean;
readonly where?: EvmOnEventWhere<
EvmEventFilters<Config>[C] extends Record<string, any>
? EvmEventFilters<Config>[C][E & keyof EvmEventFilters<Config>[C]] extends { readonly params: infer P }
? P
: {}
: {},
C
>;
},
handler: EvmContractRegisterHandler<Contracts[C][E], EvmContractRegisterContext<Config>>
) => void;
}
: {})
: never
: never
: never;
// Fuel ecosystem type — chains plus handler registration methods.
type FuelEcosystem<Config extends IndexerConfigTypes = GlobalConfig> =
"fuel" extends keyof Config
? Config["fuel"] extends {
chains: infer Chains;
contracts?: Record<infer ContractName, any>;
}
? Chains extends Record<string, { id: number }>
? {
/** Array of all Fuel chain IDs. */
readonly chainIds: readonly Chains[keyof Chains]["id"][];
/** Per-chain configuration keyed by chain name or ID. */
readonly chains: {
readonly [K in Chains[keyof Chains]["id"]]: FuelChain<
K,
ContractName extends string ? ContractName : never
>;
} & {
readonly [K in keyof Chains]: FuelChain<
Chains[K]["id"],
ContractName extends string ? ContractName : never
>;
};
/** Register a Fuel block handler. See `EvmEcosystem.onBlock` for
* `where` semantics; Fuel filters on `block.height`. Always
* available regardless of whether `contracts` are configured. */
readonly onBlock: (
options: FuelOnBlockOptions<Config>,
handler: FuelOnBlockHandler<Config>,
) => void;
} & (Config["fuel"] extends {
contracts: infer Contracts extends Record<string, Record<string, any>>;
}
? {
/** Register an event handler. */
readonly onEvent: <
C extends keyof Contracts & string,
E extends keyof Contracts[C] & string
>(
options: {
readonly contract: C;
readonly event: E;
readonly wildcard?: boolean;
read