Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fresh-adults-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
---

Introduces ENSDb module which includes data model definitions.
5 changes: 5 additions & 0 deletions .changeset/itchy-clubs-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
---

Extends ENSIndexer module with functionality allowing compatibility check between two instances of ENSIndexer public config.
5 changes: 5 additions & 0 deletions .changeset/short-buttons-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-schema": minor
---

Includes schema for `ENSNodeMetadata`.
1 change: 1 addition & 0 deletions packages/ensnode-schema/src/ponder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Merge the various sub-schemas into a single ponder (drizzle) schema.
*/

export * from "./schemas/ensnode-metadata.schema";
export * from "./schemas/ensv2.schema";
export * from "./schemas/protocol-acceleration.schema";
export * from "./schemas/registrars.schema";
Expand Down
37 changes: 37 additions & 0 deletions packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Schema Definitions that hold metadata about the ENSNode instance.
*/

import { onchainTable } from "ponder";

/**
* ENSNode Metadata
*
* Possible key value pairs are defined by 'EnsNodeMetadata' type:
* - `EnsNodeMetadataEnsDbVersion`
* - `EnsNodeMetadataEnsIndexerPublicConfig`
* - `EnsNodeMetadataEnsIndexerIndexingStatus`
*/
export const ensNodeMetadata = onchainTable("ensnode_metadata", (t) => ({
/**
* Key
*
* Allowed keys:
* - `EnsNodeMetadataEnsDbVersion['key']`
* - `EnsNodeMetadataEnsIndexerPublicConfig['key']`
* - `EnsNodeMetadataEnsIndexerIndexingStatus['key']`
*/
key: t.text().primaryKey(),

/**
* Value
*
* Allowed values:
* - `EnsNodeMetadataEnsDbVersion['value']`
* - `EnsNodeMetadataEnsIndexerPublicConfig['value']`
* - `EnsNodeMetadataEnsIndexerIndexingStatus['value']`
*
* Guaranteed to be a serialized representation of JSON object.
*/
value: t.jsonb().notNull(),
}));
58 changes: 58 additions & 0 deletions packages/ensnode-sdk/src/ensdb/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { EnsIndexerPublicConfig } from "../ensindexer/config";
import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot";

/**
* ENSDb Client Query
*
* Includes methods for reading from ENSDb.
*/
export interface EnsDbClientQuery {
/**
* Get ENSDb Version
*
* @returns the existing record, or `undefined`.
*/
getEnsDbVersion(): Promise<string | undefined>;

/**
* Get ENSIndexer Public Config
*
* @returns the existing record, or `undefined`.
*/
getEnsIndexerPublicConfig(): Promise<EnsIndexerPublicConfig | undefined>;

/**
* Get Indexing Status Snapshot
*
* @returns the existing record, or `undefined`.
*/
getIndexingStatusSnapshot(): Promise<CrossChainIndexingStatusSnapshot | undefined>;
}

/**
* ENSDb Client Mutation
*
* Includes methods for writing into ENSDb.
*/
export interface EnsDbClientMutation {
/**
* Upsert ENSDb Version
*
* @throws when upsert operation failed.
*/
upsertEnsDbVersion(ensDbVersion: string): Promise<void>;

/**
* Upsert ENSIndexer Public Config
*
* @throws when upsert operation failed.
*/
upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: EnsIndexerPublicConfig): Promise<void>;

/**
* Upsert Indexing Status Snapshot
*
* @throws when upsert operation failed.
*/
upsertIndexingStatusSnapshot(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void>;
}
38 changes: 38 additions & 0 deletions packages/ensnode-sdk/src/ensdb/ensnode-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { EnsIndexerPublicConfig } from "../ensindexer/config";
import type { CrossChainIndexingStatusSnapshot } from "../indexing-status/cross-chain-indexing-status-snapshot";

/**
* Keys used to distinguish records in `ensnode_metadata` table in the ENSDb.
*/
export const EnsNodeMetadataKeys = {
EnsDbVersion: "ensdb_version",
EnsIndexerPublicConfig: "ensindexer_public_config",
EnsIndexerIndexingStatus: "ensindexer_indexing_status",
} as const;

export type EnsNodeMetadataKey = (typeof EnsNodeMetadataKeys)[keyof typeof EnsNodeMetadataKeys];

export interface EnsNodeMetadataEnsDbVersion {
key: typeof EnsNodeMetadataKeys.EnsDbVersion;
value: string;
}

export interface EnsNodeMetadataEnsIndexerPublicConfig {
key: typeof EnsNodeMetadataKeys.EnsIndexerPublicConfig;
value: EnsIndexerPublicConfig;
}

export interface EnsNodeMetadataEnsIndexerIndexingStatus {
key: typeof EnsNodeMetadataKeys.EnsIndexerIndexingStatus;
value: CrossChainIndexingStatusSnapshot;
}

/**
* ENSNode Metadata
*
* Union type gathering all variants of ENSNode Metadata.
*/
export type EnsNodeMetadata =
| EnsNodeMetadataEnsDbVersion
| EnsNodeMetadataEnsIndexerPublicConfig
| EnsNodeMetadataEnsIndexerIndexingStatus;
3 changes: 3 additions & 0 deletions packages/ensnode-sdk/src/ensdb/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./client";
export * from "./ensnode-metadata";
export * from "./serialize/ensnode-metadata";
38 changes: 38 additions & 0 deletions packages/ensnode-sdk/src/ensdb/serialize/ensnode-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { SerializedEnsIndexerPublicConfig } from "../../ensindexer/config";
import type { SerializedCrossChainIndexingStatusSnapshot } from "../../indexing-status/serialize/cross-chain-indexing-status-snapshot";
import type {
EnsNodeMetadata,
EnsNodeMetadataEnsDbVersion,
EnsNodeMetadataEnsIndexerIndexingStatus,
EnsNodeMetadataEnsIndexerPublicConfig,
EnsNodeMetadataKeys,
} from "../ensnode-metadata";

/**
* Serialized representation of {@link EnsNodeMetadataEnsDbVersion}.
*/
export type SerializedEnsNodeMetadataEnsDbVersion = EnsNodeMetadataEnsDbVersion;

/**
* Serialized representation of {@link EnsNodeMetadataEnsIndexerPublicConfig}.
*/
export interface SerializedEnsNodeMetadataEnsIndexerPublicConfig {
key: typeof EnsNodeMetadataKeys.EnsIndexerPublicConfig;
value: SerializedEnsIndexerPublicConfig;
}

/**
* Serialized representation of {@link EnsNodeMetadataEnsIndexerIndexingStatus}.
*/
export interface SerializedEnsNodeMetadataEnsIndexerIndexingStatus {
key: typeof EnsNodeMetadataKeys.EnsIndexerIndexingStatus;
value: SerializedCrossChainIndexingStatusSnapshot;
}

/**
* Serialized representation of {@link EnsNodeMetadata}
*/
export type SerializedEnsNodeMetadata =
| SerializedEnsNodeMetadataEnsDbVersion
| SerializedEnsNodeMetadataEnsIndexerPublicConfig
| SerializedEnsNodeMetadataEnsIndexerIndexingStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";

import { ENSNamespaceIds } from "@ensnode/datasources";
import { PluginName } from "@ensnode/ensnode-sdk";

import {
type EnsIndexerPublicConfigCompatibilityCheck,
validateEnsIndexerPublicConfigCompatibility,
} from "./compatibility";

describe("EnsIndexerConfig compatibility", () => {
describe("validateEnsIndexerPublicConfigCompatibility()", () => {
const config = {
indexedChainIds: new Set([1, 10, 8453]),
isSubgraphCompatible: false,
namespace: ENSNamespaceIds.Mainnet,
plugins: [PluginName.Subgraph, PluginName.Basenames, PluginName.ThreeDNS],
} satisfies EnsIndexerPublicConfigCompatibilityCheck;

it("does not throw error when 'configB' is compatible with 'configA' ('configA' is subset of 'configB')", () => {
const configA = structuredClone(config);

const configB = structuredClone(config);
configB.indexedChainIds.add(59144);
configB.plugins.push(PluginName.Lineanames);

expect(() =>
validateEnsIndexerPublicConfigCompatibility(configA, configB),
).not.toThrowError();
});

it("throws error when 'configA.indexedChainIds' are not subset of 'configB.indexedChainIds'", () => {
const configA = structuredClone(config);

const configB = structuredClone(config);
configB.indexedChainIds.delete(8453);

expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
/'indexedChainIds' must be compatible. Stored Config 'indexedChainIds': '1, 10, 8453'. Current Config 'indexedChainIds': '1, 10'/i,
);
});

it("throws error when 'configA.isSubgraphCompatible' is not same as 'configB.isSubgraphCompatible'", () => {
const configA = structuredClone(config);

const configB = {
...structuredClone(config),
isSubgraphCompatible: !configA.isSubgraphCompatible,
} satisfies EnsIndexerPublicConfigCompatibilityCheck;

expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
/'isSubgraphCompatible' flag must be compatible. Stored Config 'isSubgraphCompatible' flag: 'false'. Current Config 'isSubgraphCompatible' flag: 'true'/i,
);
});

it("throws error when 'configA.namespace' is not same as 'configB.namespace'", () => {
const configA = structuredClone(config);

const configB = {
...structuredClone(config),
namespace: ENSNamespaceIds.Sepolia,
} satisfies EnsIndexerPublicConfigCompatibilityCheck;

expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
/'namespace' must be compatible. Stored Config 'namespace': 'mainnet'. Current Config 'namespace': 'sepolia'/i,
);
});

it("throws error when 'configA.plugins' are not subset of 'configB.plugins'", () => {
const configA = structuredClone(config);

const configB = structuredClone(config);
configB.plugins.pop();

expect(() => validateEnsIndexerPublicConfigCompatibility(configA, configB)).toThrowError(
/'plugins' must be compatible. Stored Config 'plugins': 'subgraph, basenames, threedns'. Current Config 'plugins': 'subgraph, basenames'/i,
);
});
});
});
63 changes: 63 additions & 0 deletions packages/ensnode-sdk/src/ensindexer/config/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { EnsIndexerPublicConfig } from "./types";

export type EnsIndexerPublicConfigCompatibilityCheck = Pick<
EnsIndexerPublicConfig,
"indexedChainIds" | "isSubgraphCompatible" | "namespace" | "plugins"
>;

/**
* Validate if `configB` is compatible with `configA`, such that `configA` is
* a subset of `configB`.
*
* @throws error if configs are incompatible.
*/
export function validateEnsIndexerPublicConfigCompatibility(
configA: EnsIndexerPublicConfigCompatibilityCheck,
configB: EnsIndexerPublicConfigCompatibilityCheck,
): void {
const configAIndexedChainIds = Array.from(configA.indexedChainIds);
const configBIndexedChainIds = Array.from(configB.indexedChainIds);
if (
!configAIndexedChainIds.every((configAChainId) =>
configBIndexedChainIds.includes(configAChainId),
)
) {
throw new Error(
[
`'indexedChainIds' must be compatible.`,
`Stored Config 'indexedChainIds': '${configAIndexedChainIds.join(", ")}'.`,
`Current Config 'indexedChainIds': '${configBIndexedChainIds.join(", ")}'.`,
].join(" "),
);
}

if (configA.isSubgraphCompatible !== configB.isSubgraphCompatible) {
throw new Error(
[
`'isSubgraphCompatible' flag must be compatible.`,
`Stored Config 'isSubgraphCompatible' flag: '${configA.isSubgraphCompatible}'.`,
`Current Config 'isSubgraphCompatible' flag: '${configB.isSubgraphCompatible}'.`,
].join(" "),
);
}

if (configA.namespace !== configB.namespace) {
throw new Error(
[
`'namespace' must be compatible.`,
`Stored Config 'namespace': '${configA.namespace}'.`,
`Current Config 'namespace': '${configB.namespace}'.`,
].join(" "),
);
}

if (!configA.plugins.every((configAPlugin) => configB.plugins.includes(configAPlugin))) {
throw new Error(
[
`'plugins' must be compatible.`,
`Stored Config 'plugins': '${configA.plugins.join(", ")}'.`,
`Current Config 'plugins': '${configB.plugins.join(", ")}'.`,
].join(" "),
);
}
}
1 change: 1 addition & 0 deletions packages/ensnode-sdk/src/ensindexer/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./compatibility";
export * from "./deserialize";
export * from "./is-subgraph-compatible";
export * from "./label-utils";
Expand Down
1 change: 1 addition & 0 deletions packages/ensnode-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./ens";
export * from "./ensapi";
export * from "./ensdb";
export * from "./ensindexer";
export * from "./ensrainbow";
export * from "./ensv2";
Expand Down
1 change: 1 addition & 0 deletions packages/ensnode-sdk/src/indexing-status/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./deserialize/realtime-indexing-status-projection";
export * from "./omnichain-indexing-status-snapshot";
export * from "./realtime-indexing-status-projection";
export * from "./serialize/chain-indexing-status-snapshot";
export * from "./serialize/cross-chain-indexing-status-snapshot";
export * from "./serialize/omnichain-indexing-status-snapshot";
export * from "./serialize/realtime-indexing-status-projection";
export * from "./validate/chain-indexing-status-snapshot";
Expand Down