Skip to content

Commit

Permalink
Merge pull request #415 from 0xOlias/kjs/create-config-validation
Browse files Browse the repository at this point in the history
Fix the runtime `context` object to match PonderApp
  • Loading branch information
kyscott18 authored Nov 10, 2023
2 parents f77f99a + 6802b5b commit cf6340f
Show file tree
Hide file tree
Showing 15 changed files with 257 additions and 142 deletions.
1 change: 1 addition & 0 deletions packages/core/src/Ponder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export class Ponder {
indexingStore: this.indexingStore,
syncGatewayService: this.syncGatewayService,
sources: this.sources,
networks: config.networks,
});

this.serverService = new ServerService({
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/_test/art-gobblers/app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ponder } from "@/generated";

ponder.on("setup", async ({ context }) => {
const { SetupEntity } = context.entities;
const { SetupEntity } = context.models;

await SetupEntity.upsert({
id: "setup_id",
Expand All @@ -11,7 +11,7 @@ ponder.on("setup", async ({ context }) => {
});

ponder.on("ArtGobblers:Transfer", async ({ event, context }) => {
const { Account, Token } = context.entities;
const { Account, Token } = context.models;

await Account.upsert({ id: event.params.from, create: {}, update: {} });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ponder } from "@/generated";

ponder.on("ArtGobblers:GobblerClaimed", async ({ event, context }) => {
const { Account, Token } = context.entities;
const { Account, Token } = context.models;

await Account.upsert({ id: event.params.user, create: {}, update: {} });

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/_test/ens/app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ponder } from "@/generated";
ponder.on(
"BaseRegistrarImplementation:Transfer",
async ({ event, context }) => {
const { EnsNft, Account } = context.entities;
const { EnsNft, Account } = context.models;

await EnsNft.upsert({
id: event.params.tokenId.toString(),
Expand Down
67 changes: 30 additions & 37 deletions packages/core/src/config/config.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { http } from "viem";
import { http, ParseAbi, ParseAbiItem } from "viem";
import { assertType, test } from "vitest";

import {
Expand Down Expand Up @@ -83,49 +83,56 @@ export const abiWithSameEvent = [
},
] as const;

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

test("filter events", () => {
type t = FilterAbiEvents<typeof abiWithSameEvent>;
type t = FilterAbiEvents<OneAbi>;
// ^?

assertType<t>([
abiWithSameEvent[1],
abiWithSameEvent[2],
abiWithSameEvent[4],
] as const);
assertType<t>(
[] as unknown as ParseAbi<
["event Event0(bytes32 indexed arg3)", "event Event1(bytes32 indexed)"]
>
);
});

test("safe event names", () => {
type a = SafeEventNames<
// ^?
FilterAbiEvents<typeof abiSimple>
FilterAbiEvents<OneAbi>
>;
assertType<a>(["Approve", "Transfer"] as const);
assertType<a>(["Event0", "Event1"] as const);

type b = SafeEventNames<
// ^?
FilterAbiEvents<typeof abiWithSameEvent>
FilterAbiEvents<TwoAbi>
>;
assertType<b>([
"Approve(address indexed from, address indexed to, uint256 amount)",
"Transfer",
"Approve(address indexed, bytes32 indexed, uint256)",
]);
assertType<b>(["Event(bytes32 indexed)", "Event()"] as const);
});

test("ResolvedConfig default values", () => {
type a = NonNullable<Config["contracts"]>[number]["filter"];
// ^?
type a = NonNullable<
// ^?
Config["contracts"]
>[number]["network"][number]["filter"];
assertType<a>({} as { event: string[] } | { event: string } | undefined);
});

test("RecoverAbiEvent", () => {
type a = RecoverAbiEvent<
// ^?
FilterAbiEvents<typeof abiSimple>,
"Approve"
FilterAbiEvents<OneAbi>,
"Event1"
>;

assertType<a>(abiSimple[1]);
assertType<a>({} as ParseAbiItem<"event Event1(bytes32 indexed)">);
});

test("createConfig() strict config names", () => {
Expand All @@ -151,7 +158,7 @@ test("createConfig() strict config names", () => {
});

test("createConfig() has strict events inferred from abi", () => {
const config = createConfig({
createConfig({
networks: [
{ name: "mainnet", chainId: 1, transport: http("http://127.0.0.1:8545") },
],
Expand All @@ -173,16 +180,10 @@ test("createConfig() has strict events inferred from abi", () => {
},
],
});
assertType<
readonly [
"Transfer",
"Approve(address indexed from, address indexed to, uint256 amount)"
]
>(config.contracts[0].filter.event);
});

test("createConfig() has strict arg types for event", () => {
const config = createConfig({
createConfig({
networks: [
{ name: "mainnet", chainId: 1, transport: http("http://127.0.0.1:8545") },
],
Expand All @@ -192,16 +193,12 @@ test("createConfig() has strict arg types for event", () => {
network: [
{
name: "mainnet",
address: "0x",
filter: { event: "Approve", args: { from: "0x", to: "0x" } },
},
],
abi: abiSimple,
filter: {
event: "Approve",
args: {
to: ["0x1", "0x2"],
},
args: { to: ["0x2"] },
},
address: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
startBlock: 16370000,
Expand All @@ -210,8 +207,4 @@ test("createConfig() has strict arg types for event", () => {
},
],
});

assertType<
{ to?: `0x${string}` | `0x${string}`[] | null | undefined } | undefined
>(config.contracts[0].filter?.args);
});
112 changes: 82 additions & 30 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,30 +112,32 @@ export type ContractFilter<
filter?: readonly AbiEvent[] extends TAbi
?
| { event: readonly string[]; args?: never }
| { event: string; args?: GetEventArgs<Abi, string> }
| { event: string; args?: GetEventArgs<Abi, string> | unknown }
:
| {
event: readonly SafeEventNames<FilterAbiEvents<TAbi>>[number][];
args?: never;
}
| {
event: SafeEventNames<FilterAbiEvents<TAbi>>[number];
args?: GetEventArgs<
Abi,
string,
{
EnableUnion: true;
IndexedOnly: true;
Required: false;
},
RecoverAbiEvent<
TAbi,
TEventName,
SafeEventNames<FilterAbiEvents<TAbi>>
> extends infer _abiEvent extends AbiEvent
? _abiEvent
: AbiEvent
>;
args?:
| GetEventArgs<
Abi,
string,
{
EnableUnion: true;
IndexedOnly: true;
Required: false;
},
RecoverAbiEvent<
TAbi,
TEventName,
SafeEventNames<FilterAbiEvents<TAbi>>
> extends infer _abiEvent extends AbiEvent
? _abiEvent
: AbiEvent
>
| unknown;
};
};

Expand Down Expand Up @@ -191,6 +193,12 @@ type Option = {
maxHealthcheckDuration?: number;
};

type InternalContracts = readonly Contract<
string,
readonly AbiEvent[],
string
>[];

export type Config = {
/** Database to use for storing blockchain & entity data. Default: `"postgres"` if `DATABASE_URL` env var is present, otherwise `"sqlite"`. */
database?: Database;
Expand All @@ -202,26 +210,70 @@ export type Config = {
options?: Option;
};

type InferContracts<
TContracts extends InternalContracts,
TNetworks extends readonly Network[]
> = TContracts extends readonly [
infer First extends Contract<string, readonly AbiEvent[], string>,
...infer Rest extends InternalContracts
]
? readonly [
Contract<
TNetworks[number]["name"],
FilterAbiEvents<First["abi"]>,
First["filter"] extends {
event: infer _event extends string;
}
? _event
: string
>,
...InferContracts<Rest, TNetworks>
]
: [];

/**
* Identity function for type-level validation of config
* Validates type of config, and returns a strictly typed, resolved config.
*/
export const createConfig = <
const TConfig extends {
database?: Database;
networks: readonly Network[];
contracts: {
[key in keyof TConfig["contracts"] & number]: Contract<
TConfig["networks"][number]["name"],
FilterAbiEvents<TConfig["contracts"][key]["abi"]>,
TConfig["contracts"][key]["filter"] extends {
event: infer _event extends string;
}
? _event
: string
>;
};
contracts: InferContracts<
Readonly<TConfig["contracts"]>,
TConfig["networks"]
>;
options?: Option;
}
>(
config: TConfig
) => config;
): TConfig => {
// convert to an easier type to use
const contracts = config.contracts as readonly Contract<
string,
AbiEvent[],
string
>[];

contracts.forEach((contract) => {
contract.network.forEach((contractOverride) => {
// Make sure network matches an element in config.networks
const network = config.networks.find(
(n) => n.name === contractOverride.name
);
if (!network)
throw Error('Contract network does not match a network in "networks"');

// Validate the address / factory data
const resolvedFactory =
("factory" in contractOverride && contractOverride.factory) ||
("factory" in contract && contract.factory);
const resolvedAddress =
("address" in contractOverride && contractOverride.address) ||
("address" in contract && contract.address);
if (resolvedFactory && resolvedAddress)
throw Error("Factory and address cannot both be defined");
});
});

return config;
};
2 changes: 1 addition & 1 deletion packages/core/src/config/sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test("buildSources() builds topics for multiple events", () => {
maxBlockRange: 10,
},
],
}) as unknown as Config,
}) as Config,
});

expect(sources[0].criteria.topics).toMatchObject([
Expand Down
8 changes: 2 additions & 6 deletions packages/core/src/config/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Address,
encodeEventTopics,
getAbiItem,
GetEventArgs,
getEventSelector,
Hex,
} from "viem";
Expand Down Expand Up @@ -81,7 +82,6 @@ export const buildSources = ({ config }: { config: Config }): Source[] => {
// Resolve the contract per network, filling in default values where applicable
return contract.network
.map((networkContract) => {
// Note: this is missing config validation for checking if the network is valid
const network = config.networks.find(
(n) => n.name === networkContract.name
)!;
Expand All @@ -93,13 +93,11 @@ export const buildSources = ({ config }: { config: Config }): Source[] => {
: undefined;

const sharedSource = {
// constants
name: contract.name,
abi: contract.abi,
network: network.name,
chainId: network.chainId,
events,
// optionally overridden properties
startBlock: networkContract.startBlock ?? contract.startBlock ?? 0,
endBlock: networkContract.endBlock ?? contract.endBlock,
maxBlockRange:
Expand All @@ -113,8 +111,6 @@ export const buildSources = ({ config }: { config: Config }): Source[] => {
const resolvedAddress =
("address" in networkContract && networkContract.address) ||
("address" in contract && contract.address);
if (resolvedFactory && resolvedAddress)
throw Error("Factory and address cannot both be defined");

if (resolvedFactory) {
// factory
Expand Down Expand Up @@ -162,7 +158,7 @@ const buildTopics = (
// Single event with args
return encodeEventTopics({
abi: [findAbiEvent(abi, filter.event)],
args: filter.args,
args: filter.args as GetEventArgs<Abi, string>,
});
}
};
Expand Down
Loading

1 comment on commit cf6340f

@vercel
Copy link

@vercel vercel bot commented on cf6340f Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.