Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PonderApp type #412

Merged
merged 4 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions packages/core/src/build/ponderApp.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { AbiEvent, ParseAbi } from "abitype";
import { assertType, test } from "vitest";

import { ExtractAddress, ExtractAllAddresses, PonderApp } from "./ponderApp";

type OneAbi = ParseAbi<
["event Event0(bytes32 indexed arg3)", "event Event1(bytes32 indexed)"]
>;
type TwoAbi = ParseAbi<["event Event(bytes32 indexed)", "event Event()"]>;

test("ExtractAddress", () => {
type a = ExtractAddress<{ address: "0x2" }>;
// ^?
assertType<a>("" as "0x2");

type b = ExtractAddress<{
// ^?
factory: { address: "0x2"; event: AbiEvent; parameter: string };
}>;
assertType<b>("" as never);
});

test("ExtractAllAddress", () => {
type a = ExtractAllAddresses<
// ^?
[
{ name: "optimism"; address: "0x2" },
{
name: "optimism";
factory: { address: "0x2"; event: AbiEvent; parameter: string };
}
]
>[never];
// ^?
assertType<a>("" as "0x2");
});

test("PonderApp non intersecting event names", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [{ name: "One"; network: any; abi: OneAbi }];
}>;

type name = Parameters<p["on"]>[0];
// ^?

assertType<name>("" as "One:Event0" | "One:Event1");
});

test("PonderApp intersecting event names", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [{ name: "Two"; network: any; abi: TwoAbi }];
}>;

type name = Parameters<p["on"]>[0];
// ^?

assertType<name>("" as "Two:Event(bytes32 indexed)" | "Two:Event()");
});

test("PonderApp multiple contracts", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [
{ name: "One"; network: any; abi: OneAbi },
{ name: "Two"; network: any; abi: TwoAbi }
];
}>;

// Events should only correspond to their contract
type name = Exclude<
// ^?
Parameters<p["on"]>[0],
"One:Event0" | "One:Event1" | "Two:Event(bytes32 indexed)" | "Two:Event()"
>;

assertType<never>("" as name);
});

test("PonderApp event type"),
() => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [{ name: "One"; network: any; abi: OneAbi }];
}>;

type name = Parameters<Parameters<p["on"]>[1]>[0]["event"]["name"];
// ^?

assertType<name>("" as "Event0" | "Event1");

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(({}) as p).on("One:Event1", ({ event }) => {});
// ^?
};

test("PonderApp context network type", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [
{
name: "One";
network: [{ name: "mainnet" }, { name: "optimism" }];
abi: OneAbi;
}
];
}>;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(({}) as p).on("One:Event1", ({ context }) => {});
// ^?
});

test("PonderApp context client type", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [
{
name: "One";
network: [{ name: "mainnet" }, { name: "optimism" }];
abi: OneAbi;
}
];
}>;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(({}) as p).on("One:Event1", ({ context: { client } }) => {});
// ^?
});

test("PonderApp context contracts type", () => {
type p = PonderApp<{
// ^?
networks: any;
contracts: readonly [
{
name: "One";
network: [{ name: "mainnet"; address: "0x1" }, { name: "optimism" }];
abi: OneAbi;
address: "0x2";
startBlock: 1;
endBlock: 2;
}
];
}>;

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(({}) as p).on("One:Event1", ({ context: { contracts } }) => {});
// ^?
});
171 changes: 171 additions & 0 deletions packages/core/src/build/ponderApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { AbiEvent } from "abitype";
import { Abi, GetEventArgs } from "viem";

import {
Config,
ContractFilter,
ContractRequired,
FilterAbiEvents,
RecoverAbiEvent,
SafeEventNames,
} from "@/config/config";
import { ReadOnlyClient } from "@/indexing/ponderActions";
import { Block } from "@/types/block";
import { Log } from "@/types/log";
import { Transaction } from "@/types/transaction";

/** "{ContractName}:{EventName}". */
export type Name<TContract extends Config["contracts"][number]> =
`${TContract["name"]}:${SafeEventNames<
FilterAbiEvents<TContract["abi"]>
>[number]}`;

/** All possible names for a list of contracts. */
export type Names<TContracts extends Config["contracts"]> =
TContracts extends readonly [
infer First extends Config["contracts"][number],
...infer Rest extends Config["contracts"]
]
? [Name<First>, ...Names<Rest>]
: [];

/** Recover the `contract` element at the index where {@link TName} is equal to {@link TContracts}[index]. */
export type RecoverContract<
TContracts extends Config["contracts"],
TName extends string
> = TContracts extends readonly [
infer First extends Config["contracts"][number],
...infer Rest extends Config["contracts"]
]
? First["name"] extends TName
? First
: RecoverContract<Rest, TName>
: never;

type ContractNetworkOverrides = ContractRequired<
string,
readonly AbiEvent[],
string
>["network"];

/** Extract the address type from a Contract. */
export type ExtractAddress<
TContract extends
| ContractNetworkOverrides
| ContractFilter<readonly AbiEvent[], string>
> = Extract<TContract, { address: unknown }>["address"];

/** Extract the startBlock type from a Contract. */
export type ExtractStartBlock<
TContract extends
| ContractNetworkOverrides
| ContractFilter<readonly AbiEvent[], string>
> = Extract<TContract, { startBlock: unknown }>["startBlock"];

/** Extract the endBlock type from a Contract. */
export type ExtractEndBlock<
TContract extends
| ContractNetworkOverrides
| ContractFilter<readonly AbiEvent[], string>
> = Extract<TContract, { endBlock: unknown }>["endBlock"];

/** Extract all address from a list of Contracts. */
export type ExtractAllAddresses<TContracts extends ContractNetworkOverrides> =
TContracts extends readonly [
infer First extends ContractNetworkOverrides[number],
...infer Rest extends ContractNetworkOverrides
]
? readonly [ExtractAddress<First>, ...ExtractAllAddresses<Rest>]
: [];

/** Extract all startBlocks from a list of Contracts. */
export type ExtractAllStartBlocks<TContracts extends ContractNetworkOverrides> =
TContracts extends readonly [
infer First extends ContractNetworkOverrides[number],
...infer Rest extends ContractNetworkOverrides
]
? readonly [ExtractStartBlock<First>, ...ExtractAllStartBlocks<Rest>]
: [];

/** Extract all endBlocks from a list of Contracts. */
export type ExtractAllEndBlocks<TContracts extends ContractNetworkOverrides> =
TContracts extends readonly [
infer First extends ContractNetworkOverrides[number],
...infer Rest extends ContractNetworkOverrides
]
? readonly [ExtractEndBlock<First>, ...ExtractAllEndBlocks<Rest>]
: [];

/** Transform Contracts into the appropriate type for PonderApp. */
type AppContracts<TContracts extends Config["contracts"]> =
TContracts extends readonly [
infer First extends Config["contracts"][number],
...infer Rest extends Config["contracts"]
]
? Record<
First["name"],
{
abi: First["abi"];
address:
| ExtractAddress<First>
| ExtractAllAddresses<First["network"]>[number];
startBlock:
| ExtractStartBlock<First>
| ExtractAllStartBlocks<First["network"]>[number];
endBlock:
| ExtractEndBlock<First>
| ExtractAllEndBlocks<First["network"]>[number];
}
> &
AppContracts<Rest>
: {};

export type PonderApp<TConfig extends Config> = {
on: <TName extends Names<TConfig["contracts"]>[number]>(
name: TName,
indexingFunction: ({
event,
context,
}: {
event: {
name: TName extends `${string}:${infer EventName}` ? EventName : string;
params: GetEventArgs<
Abi,
string,
{
EnableUnion: false;
IndexedOnly: false;
Required: true;
},
TName extends `${infer ContractName}:${infer EventName}`
? RecoverAbiEvent<
RecoverContract<TConfig["contracts"], ContractName> extends {
abi: infer _abi extends Abi;
}
? FilterAbiEvents<_abi>
: never,
EventName
>
: never
>;
log: Log;
block: Block;
transaction: Transaction;
};
context: {
contracts: AppContracts<TConfig["contracts"]>;
network: {
chainId: number;
name: TName extends `${infer ContractName}:${string}`
? RecoverContract<
TConfig["contracts"],
ContractName
>["network"][number]["name"]
: never;
};
client: ReadOnlyClient;
models: any; // use ts-schema to infer types
};
}) => Promise<void> | void
) => void;
};
6 changes: 3 additions & 3 deletions packages/core/src/build/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { ViteNodeRunner } from "vite-node/client";
// @ts-ignore
import type { ViteNodeServer } from "vite-node/server";

import type { ResolvedConfig } from "@/config/config";
import type { Config } from "@/config/config";
import { UserError } from "@/errors/user";
import type { Common } from "@/Ponder";
import { buildSchema } from "@/schema/schema";
Expand All @@ -22,7 +22,7 @@ import { readGraphqlSchema } from "./schema";
import { parseViteNodeError, ViteNodeError } from "./stacktrace";

type BuildServiceEvents = {
newConfig: { config: ResolvedConfig };
newConfig: { config: Config };
newIndexingFunctions: { indexingFunctions: RawIndexingFunctions };
newSchema: { schema: Schema; graphqlSchema: GraphQLSchema };
};
Expand Down Expand Up @@ -196,7 +196,7 @@ export class BuildService extends Emittery<BuildServiceEvents> {
const rawConfig = result.exports.config;
const resolvedConfig = (
typeof rawConfig === "function" ? await rawConfig() : await rawConfig
) as ResolvedConfig;
) as Config;

// TODO: Validate config lol

Expand Down
Loading
Loading