-
Notifications
You must be signed in to change notification settings - Fork 16
ENSDb Writer Worker for ENSIndexer #1702
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
Changes from 21 commits
369353c
90ebd28
3548040
df97f7e
b0a189d
ef35eeb
4233232
a96ee07
09d4cfe
e8e5d01
bdb9da3
cfd0508
e1aa937
8ebe95f
abe2d6c
cfbb6d0
5d720c3
548b0f7
5a81673
9dced07
fef06d7
cacea45
09d2b8e
cb70797
f608c31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Introduced `EnsDbClient` and `EnsDbWriterWorker` to enable storing metadata in ENSDb. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // This file is the entry point for the ENSDb Writer Worker. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest to move all the ideas here (including this nice comment) into Goal: Not a fan of the The call to |
||
| // It must be placed inside the `api` directory of the Ponder app to avoid | ||
| // the following build issue: | ||
| // Error: Invalid dependency graph. Config, schema, and indexing function files | ||
| // cannot import objects from the API function file "src/api/index.ts". | ||
|
|
||
| import { startEnsDbWriterWorker } from "@/lib/ensdb-writer-worker/singleton"; | ||
|
|
||
| startEnsDbWriterWorker(); | ||
|
tk-o marked this conversation as resolved.
Outdated
tk-o marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // This file is based on `packages/ponder-subgraph/src/drizzle.ts` file. | ||
| // We currently duplicate the makeDrizzle function, as we don't have | ||
| // a shared package for backend code yet. When we do, we can move | ||
| // this function to the shared package and import it in both places. | ||
| import { setDatabaseSchema } from "@ponder/client"; | ||
| import { drizzle } from "drizzle-orm/node-postgres"; | ||
|
|
||
| type Schema = { [name: string]: unknown }; | ||
|
|
||
| /** | ||
| * Makes a Drizzle DB object. | ||
| */ | ||
| export const makeDrizzle = <SCHEMA extends Schema>({ | ||
| schema, | ||
| databaseUrl, | ||
| databaseSchema, | ||
| }: { | ||
| schema: SCHEMA; | ||
| databaseUrl: string; | ||
| databaseSchema: string; | ||
| }) => { | ||
| // monkeypatch schema onto tables | ||
| setDatabaseSchema(schema, databaseSchema); | ||
|
|
||
| return drizzle(databaseUrl, { schema, casing: "snake_case" }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { | ||
| type BlockRef, | ||
| ChainIndexingStatusIds, | ||
| CrossChainIndexingStrategyIds, | ||
| type EnsIndexerPublicConfig, | ||
| OmnichainIndexingStatusIds, | ||
| PluginName, | ||
| RangeTypeIds, | ||
| type SerializedCrossChainIndexingStatusSnapshot, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| export const earlierBlockRef = { | ||
| timestamp: 1672531199, | ||
| number: 1024, | ||
| } as const satisfies BlockRef; | ||
|
|
||
| export const laterBlockRef = { | ||
| timestamp: 1672531200, | ||
| number: 1025, | ||
| } as const satisfies BlockRef; | ||
|
|
||
| export const databaseUrl = "postgres://user:pass@localhost:5432/ensdb"; | ||
|
|
||
| export const databaseSchemaName = "public"; | ||
|
|
||
| export const publicConfig = { | ||
| databaseSchemaName, | ||
| labelSet: { | ||
| labelSetId: "subgraph", | ||
| labelSetVersion: 0, | ||
| }, | ||
| indexedChainIds: new Set([1]), | ||
| isSubgraphCompatible: true, | ||
| namespace: "mainnet", | ||
| plugins: [PluginName.Subgraph], | ||
| versionInfo: { | ||
| nodejs: "v22.10.12", | ||
| ponder: "0.11.25", | ||
| ensDb: "0.32.0", | ||
| ensIndexer: "0.32.0", | ||
| ensNormalize: "1.11.1", | ||
| ensRainbow: "0.32.0", | ||
| ensRainbowSchema: 2, | ||
| }, | ||
| } satisfies EnsIndexerPublicConfig; | ||
|
|
||
| export const serializedSnapshot = { | ||
| strategy: CrossChainIndexingStrategyIds.Omnichain, | ||
| slowestChainIndexingCursor: earlierBlockRef.timestamp, | ||
| snapshotTime: earlierBlockRef.timestamp + 20, | ||
| omnichainSnapshot: { | ||
| omnichainStatus: OmnichainIndexingStatusIds.Following, | ||
| chains: { | ||
| "1": { | ||
| chainStatus: ChainIndexingStatusIds.Following, | ||
| config: { | ||
| rangeType: RangeTypeIds.LeftBounded, | ||
| startBlock: earlierBlockRef, | ||
| }, | ||
| latestIndexedBlock: earlierBlockRef, | ||
| latestKnownBlock: laterBlockRef, | ||
| }, | ||
| }, | ||
| omnichainIndexingCursor: earlierBlockRef.timestamp, | ||
| }, | ||
| } satisfies SerializedCrossChainIndexingStatusSnapshot; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { ensNodeMetadata } from "@ensnode/ensnode-schema"; | ||
| import { | ||
| deserializeCrossChainIndexingStatusSnapshot, | ||
| EnsNodeMetadataKeys, | ||
| serializeCrossChainIndexingStatusSnapshot, | ||
| serializeEnsIndexerPublicConfig, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { makeDrizzle } from "./drizzle"; | ||
| import { EnsDbClient } from "./ensdb-client"; | ||
| import * as ensDbClientMock from "./ensdb-client.mock"; | ||
|
|
||
| // Mock the config module to prevent it from trying to load actual environment variables during tests | ||
| vi.mock("@/config", () => ({ default: {} })); | ||
|
|
||
| // Mock the makeDrizzle function to return a mock database instance | ||
| vi.mock("./drizzle", () => ({ makeDrizzle: vi.fn() })); | ||
|
|
||
| describe("EnsDbClient", () => { | ||
| // Mock database query results and methods | ||
| const selectResult = { current: [] as Array<{ value: unknown }> }; | ||
| const whereMock = vi.fn(async () => selectResult.current); | ||
| const fromMock = vi.fn(() => ({ where: whereMock })); | ||
| const selectMock = vi.fn(() => ({ from: fromMock })); | ||
| const onConflictDoUpdateMock = vi.fn(async () => undefined); | ||
| const valuesMock = vi.fn(() => ({ onConflictDoUpdate: onConflictDoUpdateMock })); | ||
| const insertMock = vi.fn(() => ({ values: valuesMock })); | ||
| const dbMock = { select: selectMock, insert: insertMock }; | ||
|
|
||
| beforeEach(() => { | ||
| selectResult.current = []; | ||
| whereMock.mockClear(); | ||
| fromMock.mockClear(); | ||
| selectMock.mockClear(); | ||
| onConflictDoUpdateMock.mockClear(); | ||
| valuesMock.mockClear(); | ||
| insertMock.mockClear(); | ||
| vi.mocked(makeDrizzle).mockReturnValue(dbMock as unknown as ReturnType<typeof makeDrizzle>); | ||
| }); | ||
|
|
||
| describe("getEnsDbVersion", () => { | ||
| it("returns undefined when no record exists", async () => { | ||
| // arrange | ||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); | ||
|
|
||
| expect(selectMock).toHaveBeenCalledTimes(1); | ||
| expect(fromMock).toHaveBeenCalledWith(ensNodeMetadata); | ||
| }); | ||
|
|
||
| it("returns value when one record exists", async () => { | ||
| // arrange | ||
| selectResult.current = [{ value: "0.1.0" }]; | ||
|
|
||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); | ||
| }); | ||
|
|
||
| // This scenario should be impossible due to the primary key constraint on | ||
| // the 'key' column of 'ensnode_metadata' table. | ||
| it("throws when multiple records exist", async () => { | ||
| // arrange | ||
| selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; | ||
|
|
||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); | ||
| }); | ||
| }); | ||
|
|
||
| describe("getEnsIndexerPublicConfig", () => { | ||
| it("returns undefined when no record exists", async () => { | ||
| // arrange | ||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); | ||
| }); | ||
|
|
||
| it("deserializes the stored config", async () => { | ||
| // arrange | ||
| const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); | ||
| selectResult.current = [{ value: serializedConfig }]; | ||
|
|
||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( | ||
| ensDbClientMock.publicConfig, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("getIndexingStatusSnapshot", () => { | ||
| it("deserializes the stored indexing status snapshot", async () => { | ||
| // arrange | ||
| selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; | ||
|
|
||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
| const expected = deserializeCrossChainIndexingStatusSnapshot( | ||
| ensDbClientMock.serializedSnapshot, | ||
| ); | ||
|
|
||
| // act & assert | ||
| await expect(client.getIndexingStatusSnapshot()).resolves.toStrictEqual(expected); | ||
| }); | ||
| }); | ||
|
|
||
| describe("upsertEnsDbVersion", () => { | ||
| it("writes the database version metadata", async () => { | ||
| // arrange | ||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
|
|
||
| // act | ||
| await client.upsertEnsDbVersion("0.2.0"); | ||
|
|
||
| // assert | ||
| expect(insertMock).toHaveBeenCalledWith(ensNodeMetadata); | ||
| expect(valuesMock).toHaveBeenCalledWith({ | ||
| key: EnsNodeMetadataKeys.EnsDbVersion, | ||
| value: "0.2.0", | ||
| }); | ||
| expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ | ||
| target: ensNodeMetadata.key, | ||
| set: { value: "0.2.0" }, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("upsertEnsIndexerPublicConfig", () => { | ||
| it("serializes and writes the public config", async () => { | ||
| // arrange | ||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
| const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); | ||
|
|
||
| // act | ||
| await client.upsertEnsIndexerPublicConfig(ensDbClientMock.publicConfig); | ||
|
|
||
| // assert | ||
| expect(valuesMock).toHaveBeenCalledWith({ | ||
| key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, | ||
| value: expectedValue, | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe("upsertIndexingStatusSnapshot", () => { | ||
| it("serializes and writes the indexing status snapshot", async () => { | ||
| // arrange | ||
| const client = new EnsDbClient( | ||
| ensDbClientMock.databaseUrl, | ||
| ensDbClientMock.databaseSchemaName, | ||
| ); | ||
| const snapshot = deserializeCrossChainIndexingStatusSnapshot( | ||
| ensDbClientMock.serializedSnapshot, | ||
| ); | ||
| const expectedValue = serializeCrossChainIndexingStatusSnapshot(snapshot); | ||
|
|
||
| // act | ||
| await client.upsertIndexingStatusSnapshot(snapshot); | ||
|
|
||
| // assert | ||
| expect(valuesMock).toHaveBeenCalledWith({ | ||
| key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, | ||
| value: expectedValue, | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.