Skip to content

Commit 9bffd55

Browse files
authored
ENSDb data model (#1660)
1 parent 66563ea commit 9bffd55

14 files changed

Lines changed: 385 additions & 0 deletions

File tree

.changeset/fresh-adults-decide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensnode/ensnode-sdk": minor
3+
---
4+
5+
Introduces ENSDb module which includes data model definitions.

.changeset/itchy-clubs-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensnode/ensnode-sdk": minor
3+
---
4+
5+
Extends ENSIndexer module with functionality allowing compatibility check between two instances of ENSIndexer public config.

.changeset/short-buttons-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ensnode/ensnode-schema": minor
3+
---
4+
5+
Includes schema for `ENSNodeMetadata`.

packages/ensnode-schema/src/ponder.schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Merge the various sub-schemas into a single ponder (drizzle) schema.
33
*/
44

5+
export * from "./schemas/ensnode-metadata.schema";
56
export * from "./schemas/ensv2.schema";
67
export * from "./schemas/protocol-acceleration.schema";
78
export * from "./schemas/registrars.schema";
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Schema Definitions that hold metadata about the ENSNode instance.
3+
*/
4+
5+
import { onchainTable } from "ponder";
6+
7+
/**
8+
* ENSNode Metadata
9+
*
10+
* Possible key value pairs are defined by 'EnsNodeMetadata' type:
11+
* - `EnsNodeMetadataEnsDbVersion`
12+
* - `EnsNodeMetadataEnsIndexerPublicConfig`
13+
* - `EnsNodeMetadataEnsIndexerIndexingStatus`
14+
*/
15+
export const ensNodeMetadata = onchainTable("ensnode_metadata", (t) => ({
16+
/**
17+
* Key
18+
*
19+
* Allowed keys:
20+
* - `EnsNodeMetadataEnsDbVersion['key']`
21+
* - `EnsNodeMetadataEnsIndexerPublicConfig['key']`
22+
* - `EnsNodeMetadataEnsIndexerIndexingStatus['key']`
23+
*/
24+
key: t.text().primaryKey(),
25+
26+
/**
27+
* Value
28+
*
29+
* Allowed values:
30+
* - `EnsNodeMetadataEnsDbVersion['value']`
31+
* - `EnsNodeMetadataEnsIndexerPublicConfig['value']`
32+
* - `EnsNodeMetadataEnsIndexerIndexingStatus['value']`
33+
*
34+
* Guaranteed to be a serialized representation of JSON object.
35+
*/
36+
value: t.jsonb().notNull(),
37+
}));
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { EnsIndexerPublicConfig } from "../ensindexer/config";
2+
import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot";
3+
4+
/**
5+
* ENSDb Client Query
6+
*
7+
* Includes methods for reading from ENSDb.
8+
*/
9+
export interface EnsDbClientQuery {
10+
/**
11+
* Get ENSDb Version
12+
*
13+
* @returns the existing record, or `undefined`.
14+
*/
15+
getEnsDbVersion(): Promise<string | undefined>;
16+
17+
/**
18+
* Get ENSIndexer Public Config
19+
*
20+
* @returns the existing record, or `undefined`.
21+
*/
22+
getEnsIndexerPublicConfig(): Promise<EnsIndexerPublicConfig | undefined>;
23+
24+
/**
25+
* Get Indexing Status Snapshot
26+
*
27+
* @returns the existing record, or `undefined`.
28+
*/
29+
getIndexingStatusSnapshot(): Promise<CrossChainIndexingStatusSnapshot | undefined>;
30+
}
31+
32+
/**
33+
* ENSDb Client Mutation
34+
*
35+
* Includes methods for writing into ENSDb.
36+
*/
37+
export interface EnsDbClientMutation {
38+
/**
39+
* Upsert ENSDb Version
40+
*
41+
* @throws when upsert operation failed.
42+
*/
43+
upsertEnsDbVersion(ensDbVersion: string): Promise<void>;
44+
45+
/**
46+
* Upsert ENSIndexer Public Config
47+
*
48+
* @throws when upsert operation failed.
49+
*/
50+
upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: EnsIndexerPublicConfig): Promise<void>;
51+
52+
/**
53+
* Upsert Indexing Status Snapshot
54+
*
55+
* @throws when upsert operation failed.
56+
*/
57+
upsertIndexingStatusSnapshot(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void>;
58+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { EnsIndexerPublicConfig } from "../ensindexer/config";
2+
import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot";
3+
4+
/**
5+
* Keys used to distinguish records in `ensnode_metadata` table in the ENSDb.
6+
*/
7+
export const EnsNodeMetadataKeys = {
8+
EnsDbVersion: "ensdb_version",
9+
EnsIndexerPublicConfig: "ensindexer_public_config",
10+
EnsIndexerIndexingStatus: "ensindexer_indexing_status",
11+
} as const;
12+
13+
export type EnsNodeMetadataKey = (typeof EnsNodeMetadataKeys)[keyof typeof EnsNodeMetadataKeys];
14+
15+
export interface EnsNodeMetadataEnsDbVersion {
16+
key: typeof EnsNodeMetadataKeys.EnsDbVersion;
17+
value: string;
18+
}
19+
20+
export interface EnsNodeMetadataEnsIndexerPublicConfig {
21+
key: typeof EnsNodeMetadataKeys.EnsIndexerPublicConfig;
22+
value: EnsIndexerPublicConfig;
23+
}
24+
25+
export interface EnsNodeMetadataEnsIndexerIndexingStatus {
26+
key: typeof EnsNodeMetadataKeys.EnsIndexerIndexingStatus;
27+
value: CrossChainIndexingStatusSnapshot;
28+
}
29+
30+
/**
31+
* ENSNode Metadata
32+
*
33+
* Union type gathering all variants of ENSNode Metadata.
34+
*/
35+
export type EnsNodeMetadata =
36+
| EnsNodeMetadataEnsDbVersion
37+
| EnsNodeMetadataEnsIndexerPublicConfig
38+
| EnsNodeMetadataEnsIndexerIndexingStatus;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./client";
2+
export * from "./ensnode-metadata";
3+
export * from "./serialize/ensnode-metadata";
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { SerializedEnsIndexerPublicConfig } from "../../ensindexer/config";
2+
import type { SerializedCrossChainIndexingStatusSnapshot } from "../../indexing-status/serialize/cross-chain-indexing-status-snapshot";
3+
import type {
4+
EnsNodeMetadata,
5+
EnsNodeMetadataEnsDbVersion,
6+
EnsNodeMetadataEnsIndexerIndexingStatus,
7+
EnsNodeMetadataEnsIndexerPublicConfig,
8+
EnsNodeMetadataKeys,
9+
} from "../ensnode-metadata";
10+
11+
/**
12+
* Serialized representation of {@link EnsNodeMetadataEnsDbVersion}.
13+
*/
14+
export type SerializedEnsNodeMetadataEnsDbVersion = EnsNodeMetadataEnsDbVersion;
15+
16+
/**
17+
* Serialized representation of {@link EnsNodeMetadataEnsIndexerPublicConfig}.
18+
*/
19+
export interface SerializedEnsNodeMetadataEnsIndexerPublicConfig {
20+
key: typeof EnsNodeMetadataKeys.EnsIndexerPublicConfig;
21+
value: SerializedEnsIndexerPublicConfig;
22+
}
23+
24+
/**
25+
* Serialized representation of {@link EnsNodeMetadataEnsIndexerIndexingStatus}.
26+
*/
27+
export interface SerializedEnsNodeMetadataEnsIndexerIndexingStatus {
28+
key: typeof EnsNodeMetadataKeys.EnsIndexerIndexingStatus;
29+
value: SerializedCrossChainIndexingStatusSnapshot;
30+
}
31+
32+
/**
33+
* Serialized representation of {@link EnsNodeMetadata}
34+
*/
35+
export type SerializedEnsNodeMetadata =
36+
| SerializedEnsNodeMetadataEnsDbVersion
37+
| SerializedEnsNodeMetadataEnsIndexerPublicConfig
38+
| SerializedEnsNodeMetadataEnsIndexerIndexingStatus;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { ENSNamespaceIds } from "@ensnode/datasources";
4+
import { PluginName } from "@ensnode/ensnode-sdk";
5+
6+
import {
7+
type EnsIndexerPublicConfigCompatibilityCheck,
8+
validateEnsIndexerPublicConfigCompatibility,
9+
} from "./compatibility";
10+
11+
describe("EnsIndexerConfig compatibility", () => {
12+
describe("validateEnsIndexerPublicConfigCompatibility()", () => {
13+
const config = {
14+
indexedChainIds: new Set([1, 10, 8453]),
15+
labelSet: {
16+
labelSetId: "test-label-set",
17+
labelSetVersion: 1,
18+
},
19+
isSubgraphCompatible: false,
20+
namespace: ENSNamespaceIds.Mainnet,
21+
plugins: [PluginName.Subgraph, PluginName.Basenames, PluginName.ThreeDNS],
22+
} satisfies EnsIndexerPublicConfigCompatibilityCheck;
23+
24+
it("does not throw error when 'configA' and 'configB' are equal", () => {
25+
const configA = structuredClone(config);
26+
const configB = structuredClone(config);
27+
28+
expect(() =>
29+
validateEnsIndexerPublicConfigCompatibility(configA, configB),
30+
).not.toThrowError();
31+
});
32+
33+
it("throws error when 'configA.indexedChainIds' differ from 'configB.indexedChainIds'", () => {
34+
const configA = structuredClone(config);
35+
36+
const configB = structuredClone(config);
37+
configB.indexedChainIds.delete(8453);
38+
39+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
40+
/'indexedChainIds' must be compatible. Stored Config 'indexedChainIds': '1, 10, 8453'. Current Config 'indexedChainIds': '1, 10'/i,
41+
);
42+
});
43+
44+
it("throws error when 'configA.isSubgraphCompatible' is not same as 'configB.isSubgraphCompatible'", () => {
45+
const configA = structuredClone(config);
46+
47+
const configB = {
48+
...structuredClone(config),
49+
isSubgraphCompatible: !configA.isSubgraphCompatible,
50+
} satisfies EnsIndexerPublicConfigCompatibilityCheck;
51+
52+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
53+
/'isSubgraphCompatible' flag must be compatible. Stored Config 'isSubgraphCompatible' flag: 'false'. Current Config 'isSubgraphCompatible' flag: 'true'/i,
54+
);
55+
});
56+
57+
it("throws error when 'configA.namespace' is not same as 'configB.namespace'", () => {
58+
const configA = structuredClone(config);
59+
60+
const configB = {
61+
...structuredClone(config),
62+
namespace: ENSNamespaceIds.Sepolia,
63+
} satisfies EnsIndexerPublicConfigCompatibilityCheck;
64+
65+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
66+
/'namespace' must be compatible. Stored Config 'namespace': 'mainnet'. Current Config 'namespace': 'sepolia'/i,
67+
);
68+
});
69+
70+
it("throws error when 'configA.labelSet.labelSetId' is not same as 'configB.labelSet.labelSetId'", () => {
71+
const configA = structuredClone(config);
72+
73+
const configB = {
74+
...structuredClone(config),
75+
labelSet: {
76+
...structuredClone(config.labelSet),
77+
labelSetId: "different-label-set",
78+
},
79+
} satisfies EnsIndexerPublicConfigCompatibilityCheck;
80+
81+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
82+
/'labelSet.labelSetId' must be compatible. Stored Config 'labelSet.labelSetId': 'test-label-set'. Current Config 'labelSet.labelSetId': 'different-label-set'/i,
83+
);
84+
});
85+
86+
it("throws error when 'configA.labelSet.labelSetVersion' is not same as 'configB.labelSet.labelSetVersion'", () => {
87+
const configA = structuredClone(config);
88+
89+
const configB = {
90+
...structuredClone(config),
91+
labelSet: {
92+
...structuredClone(config.labelSet),
93+
labelSetVersion: config.labelSet.labelSetVersion + 1,
94+
},
95+
} satisfies EnsIndexerPublicConfigCompatibilityCheck;
96+
97+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
98+
/'labelSet.labelSetVersion' must be compatible. Stored Config 'labelSet.labelSetVersion': '1'. Current Config 'labelSet.labelSetVersion': '2'/i,
99+
);
100+
});
101+
102+
it("throws error when 'configA.plugins' differ from 'configB.plugins'", () => {
103+
const configA = structuredClone(config);
104+
105+
const configB = structuredClone(config);
106+
configB.plugins.pop();
107+
108+
expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
109+
/'plugins' must be compatible. Stored Config 'plugins': 'subgraph, basenames, threedns'. Current Config 'plugins': 'subgraph, basenames'/i,
110+
);
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)