Skip to content

Commit ed6ee96

Browse files
authored
ENSApi Version Info (#1859)
1 parent 7fca45d commit ed6ee96

12 files changed

Lines changed: 235 additions & 51 deletions

File tree

.changeset/spotty-pumas-tap.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+
Replaced `version` field with `versionInfo` field in the `EnsApiPublicConfig` data model. This change allows tracking the version of `@adraffy/ens-normalize` package used in ENSApi.

apps/ensadmin/src/components/connection/cards/ensnode-info.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ function ENSNodeConfigCardContent({
291291
icon={<ENSApiIcon width={24} height={24} />}
292292
version={
293293
<p className="text-sm leading-normal font-normal text-muted-foreground">
294-
v{ensApiPublicConfig.version}
294+
v{ensApiPublicConfig.versionInfo.ensApi}
295295
</p>
296296
}
297297
docsLink={new URL("https://ensnode.io/ensapi")}
@@ -336,6 +336,23 @@ function ENSNodeConfigCardContent({
336336
</p>
337337
}
338338
/>
339+
<InfoCardItem
340+
label="ens-normalize.js"
341+
value={
342+
<p className={cardItemValueStyles}>{ensApiPublicConfig.versionInfo.ensNormalize}</p>
343+
}
344+
additionalInfo={
345+
<p>
346+
Version of the{" "}
347+
<ExternalLinkWithIcon
348+
href={`https://www.npmjs.com/package/@adraffy/ens-normalize/v/${ensApiPublicConfig.versionInfo.ensNormalize}`}
349+
>
350+
@adraffy/ens-normalize
351+
</ExternalLinkWithIcon>{" "}
352+
package used for ENS name normalization.
353+
</p>
354+
}
355+
/>
339356
</InfoCardItems>
340357
<InfoCardFeatures activated={ensApiPublicConfig.theGraphFallback.canFallback}>
341358
<InfoCardFeature

apps/ensapi/src/config/config.schema.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/co
1515
import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
1616
import type { EnsApiEnvironment } from "@/config/environment";
1717
import logger from "@/lib/logger";
18+
import { ensApiVersionInfo } from "@/lib/version-info";
1819

1920
vi.mock("@/lib/logger", () => ({
2021
default: {
@@ -45,8 +46,8 @@ const ENSINDEXER_PUBLIC_CONFIG = {
4546
versionInfo: {
4647
ensDb: packageJson.version,
4748
ensIndexer: packageJson.version,
48-
ensNormalize: "1.1.1",
49-
ponder: "1.1.1",
49+
ensNormalize: ensApiVersionInfo.ensNormalize,
50+
ponder: "0.8.0",
5051
},
5152
} satisfies ENSIndexerPublicConfig;
5253

@@ -167,7 +168,7 @@ describe("buildEnsApiPublicConfig", () => {
167168
const result = buildEnsApiPublicConfig(mockConfig);
168169

169170
expect(result).toStrictEqual({
170-
version: packageJson.version,
171+
versionInfo: ensApiVersionInfo,
171172
theGraphFallback: {
172173
canFallback: false,
173174
reason: "not-subgraph-compatible",

apps/ensapi/src/config/config.schema.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import packageJson from "@/../package.json" with { type: "json" };
2-
31
import pRetry from "p-retry";
42
import { prettifyError, ZodError, z } from "zod/v4";
53

@@ -21,6 +19,7 @@ import type { EnsApiEnvironment } from "@/config/environment";
2119
import { invariant_ensIndexerPublicConfigVersionInfo } from "@/config/validations";
2220
import { ensDbClient } from "@/lib/ensdb/singleton";
2321
import logger from "@/lib/logger";
22+
import { ensApiVersionInfo } from "@/lib/version-info";
2423

2524
/**
2625
* Schema for validating custom referral program edition config set URL.
@@ -119,7 +118,7 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis
119118
*/
120119
export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfig {
121120
return {
122-
version: packageJson.version,
121+
versionInfo: ensApiVersionInfo,
123122
theGraphFallback: canFallbackToTheGraph({
124123
namespace: config.namespace,
125124
// NOTE: very important here that we replace the actual server-side api key with a placeholder

apps/ensapi/src/config/validations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import packageJson from "@/../package.json" with { type: "json" };
33
import type { ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk";
44
import type { ZodCheckFnInput } from "@ensnode/ensnode-sdk/internal";
55

6+
import { ensApiVersionInfo } from "@/lib/version-info";
7+
68
// Invariant: ENSIndexerPublicConfig VersionInfo must match ENSApi
79
export function invariant_ensIndexerPublicConfigVersionInfo(
810
ctx: ZodCheckFnInput<{
@@ -42,4 +44,14 @@ export function invariant_ensIndexerPublicConfigVersionInfo(
4244
message: `Version Mismatch: ENSRainbow@${ensIndexerPublicConfig.ensRainbowPublicConfig.version} !== ENSApi@${packageJson.version}`,
4345
});
4446
}
47+
48+
// Invariant: `@adraffy/ens-normalize` package version must match between ENSApi & ENSIndexer
49+
if (ensIndexerPublicConfig.versionInfo.ensNormalize !== ensApiVersionInfo.ensNormalize) {
50+
ctx.issues.push({
51+
code: "custom",
52+
path: ["ensIndexerPublicConfig.versionInfo.ensNormalize"],
53+
input: ensIndexerPublicConfig.versionInfo.ensNormalize,
54+
message: `Dependency Version Mismatch: '@adraffy/ens-normalize' version must be the same between ENSIndexer and ENSApi. Found ENSApi@${ensApiVersionInfo.ensNormalize} and ENSIndexer@${ensIndexerPublicConfig.versionInfo.ensNormalize}`,
55+
});
56+
}
4557
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import packageJson from "@/../package.json" with { type: "json" };
2+
3+
import { existsSync, readdirSync, readFileSync } from "node:fs";
4+
import { dirname, join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
import type { EnsApiVersionInfo } from "@ensnode/ensnode-sdk";
8+
9+
/**
10+
* Get ENS API version
11+
*/
12+
function getEnsApiVersion(): string {
13+
return packageJson.version;
14+
}
15+
16+
/**
17+
* Get NPM package version.
18+
*
19+
* Note:
20+
* Since we use PNPM's `catalog:` references, reading directly from
21+
* the `package.json` file would give us `catalog:` values, and not resolved
22+
* version values. We need the latter, so we implement our own version
23+
* resolution method.
24+
*
25+
* @throws If the package version cannot be found in any
26+
* `node_modules` up to the workspace root.
27+
*/
28+
function getPackageVersion(packageName: string): string {
29+
// Start from current file's directory
30+
const currentFile = fileURLToPath(import.meta.url);
31+
let searchDir = dirname(currentFile);
32+
33+
while (true) {
34+
const workspaceFile = join(searchDir, "pnpm-workspace.yaml");
35+
const isWorkspaceRoot = existsSync(workspaceFile);
36+
37+
// Check for node_modules in current directory
38+
const nodeModulesPath = join(searchDir, "node_modules", packageName, "package.json");
39+
if (existsSync(nodeModulesPath)) {
40+
const packageJson = JSON.parse(readFileSync(nodeModulesPath, "utf8"));
41+
return packageJson.version;
42+
}
43+
44+
// Check PNPM's .pnpm virtual store
45+
const pnpmDir = join(searchDir, "node_modules", ".pnpm");
46+
if (existsSync(pnpmDir)) {
47+
const version = getPackageVersionFromPnpmStore(pnpmDir, packageName);
48+
if (version) return version;
49+
}
50+
51+
// If we're at workspace root and still haven't found it, stop searching
52+
if (isWorkspaceRoot) {
53+
throw new Error(`Package ${packageName} not found in any node_modules up to workspace root`);
54+
}
55+
56+
// Move up one directory
57+
const parentDir = dirname(searchDir);
58+
59+
// Prevent infinite loop if we reach filesystem root
60+
if (parentDir === searchDir) {
61+
throw new Error(`Package ${packageName} not found and no workspace root detected`);
62+
}
63+
64+
searchDir = parentDir;
65+
}
66+
}
67+
68+
/**
69+
* Get package version from PNPM virtual store.
70+
*
71+
* PNPM stores packages in its virtual store that
72+
* can be located at, for example, `./node_modules/.pnpm` path.
73+
*
74+
* This function is used in a fallback method by {@link getPackageVersion} to
75+
* get package version by package name in case it was not found
76+
* directly in `./node_modules` directory.
77+
*/
78+
function getPackageVersionFromPnpmStore(pnpmDir: string, packageName: string): string | null {
79+
try {
80+
const entries = readdirSync(pnpmDir);
81+
82+
// Convert package name to PNPM's format: @scope/name -> @scope+name
83+
const normalizedName = packageName.replace("/", "+");
84+
85+
// Find entries that match the package name
86+
// They will be in format: packagename@version or @scope+packagename@version
87+
for (const entry of entries) {
88+
if (entry.startsWith(`${normalizedName}@`)) {
89+
const pkgPath = join(pnpmDir, entry, "node_modules", packageName, "package.json");
90+
if (existsSync(pkgPath)) {
91+
const packageJson = JSON.parse(readFileSync(pkgPath, "utf8"));
92+
return packageJson.version;
93+
}
94+
}
95+
}
96+
} catch (_error) {
97+
// Ignore errors in this helper
98+
}
99+
100+
return null;
101+
}
102+
103+
export const ensApiVersionInfo = {
104+
ensApi: getEnsApiVersion(),
105+
ensNormalize: getPackageVersion("@adraffy/ens-normalize"),
106+
} as const satisfies EnsApiVersionInfo;

docs/docs.ensnode.io/ensapi-openapi.json

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -43,32 +43,6 @@
4343
"schema": {
4444
"type": "object",
4545
"properties": {
46-
"version": { "type": "string", "minLength": 1 },
47-
"theGraphFallback": {
48-
"oneOf": [
49-
{
50-
"type": "object",
51-
"properties": {
52-
"canFallback": { "type": "boolean", "enum": [true] },
53-
"url": { "type": "string" }
54-
},
55-
"required": ["canFallback", "url"],
56-
"additionalProperties": false
57-
},
58-
{
59-
"type": "object",
60-
"properties": {
61-
"canFallback": { "type": "boolean", "enum": [false] },
62-
"reason": {
63-
"type": "string",
64-
"enum": ["not-subgraph-compatible", "no-api-key", "no-subgraph-url"]
65-
}
66-
},
67-
"required": ["canFallback", "reason"],
68-
"additionalProperties": false
69-
}
70-
]
71-
},
7246
"ensIndexerPublicConfig": {
7347
"type": "object",
7448
"properties": {
@@ -143,9 +117,42 @@
143117
"plugins",
144118
"versionInfo"
145119
]
120+
},
121+
"theGraphFallback": {
122+
"oneOf": [
123+
{
124+
"type": "object",
125+
"properties": {
126+
"canFallback": { "type": "boolean", "enum": [true] },
127+
"url": { "type": "string" }
128+
},
129+
"required": ["canFallback", "url"],
130+
"additionalProperties": false
131+
},
132+
{
133+
"type": "object",
134+
"properties": {
135+
"canFallback": { "type": "boolean", "enum": [false] },
136+
"reason": {
137+
"type": "string",
138+
"enum": ["not-subgraph-compatible", "no-api-key", "no-subgraph-url"]
139+
}
140+
},
141+
"required": ["canFallback", "reason"],
142+
"additionalProperties": false
143+
}
144+
]
145+
},
146+
"versionInfo": {
147+
"type": "object",
148+
"properties": {
149+
"ensApi": { "type": "string", "minLength": 1 },
150+
"ensNormalize": { "type": "string", "minLength": 1 }
151+
},
152+
"required": ["ensApi", "ensNormalize"]
146153
}
147154
},
148-
"required": ["version", "theGraphFallback", "ensIndexerPublicConfig"]
155+
"required": ["ensIndexerPublicConfig", "theGraphFallback", "versionInfo"]
149156
}
150157
}
151158
}

packages/ensnode-sdk/src/ensapi/client.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ const EXAMPLE_PRIMARY_NAMES_RESPONSE = {
5858
const EXAMPLE_ERROR_RESPONSE: ErrorResponse = { message: "error" };
5959

6060
const EXAMPLE_CONFIG_RESPONSE = {
61-
version: "0.32.0",
61+
versionInfo: {
62+
ensApi: "1.9.0",
63+
ensNormalize: "1.11.1",
64+
},
6265
theGraphFallback: {
6366
canFallback: false,
6467
reason: "no-api-key",

packages/ensnode-sdk/src/ensapi/config/conversions.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import type { SerializedEnsApiPublicConfig } from "./serialized-types";
99
import type { EnsApiPublicConfig } from "./types";
1010

1111
const MOCK_ENSAPI_PUBLIC_CONFIG = {
12-
version: "0.36.0",
12+
versionInfo: {
13+
ensApi: "1.9.0",
14+
ensNormalize: "1.11.1",
15+
},
1316
theGraphFallback: {
1417
canFallback: false,
1518
reason: "no-api-key",
@@ -43,7 +46,10 @@ describe("ENSApi Config Serialization/Deserialization", () => {
4346
const result = serializeEnsApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG);
4447

4548
expect(result).toEqual({
46-
version: "0.36.0",
49+
versionInfo: {
50+
ensApi: "1.9.0",
51+
ensNormalize: "1.11.1",
52+
},
4753
theGraphFallback: {
4854
canFallback: false,
4955
reason: "no-api-key",
@@ -82,11 +88,14 @@ describe("ENSApi Config Serialization/Deserialization", () => {
8288
it("handles validation errors with custom value label", () => {
8389
const invalidConfig = {
8490
...MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG,
85-
version: "", // Invalid: empty string
91+
versionInfo: {
92+
ensApi: "",
93+
ensNormalize: "",
94+
},
8695
};
8796

8897
expect(() => deserializeEnsApiPublicConfig(invalidConfig, "testConfig")).toThrow(
89-
/testConfig.version/,
98+
/testConfig.versionInfo.ensApi/,
9099
);
91100
});
92101
});

packages/ensnode-sdk/src/ensapi/config/serialize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import type { EnsApiPublicConfig } from "./types";
88
export function serializeEnsApiPublicConfig(
99
config: EnsApiPublicConfig,
1010
): SerializedEnsApiPublicConfig {
11-
const { version, theGraphFallback, ensIndexerPublicConfig } = config;
11+
const { ensIndexerPublicConfig, theGraphFallback, versionInfo } = config;
1212

1313
return {
14-
version,
15-
theGraphFallback,
1614
ensIndexerPublicConfig: serializeEnsIndexerPublicConfig(ensIndexerPublicConfig),
15+
theGraphFallback,
16+
versionInfo,
1717
} satisfies SerializedEnsApiPublicConfig;
1818
}
1919

0 commit comments

Comments
 (0)