Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/spotty-pumas-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
Comment thread
tk-o marked this conversation as resolved.
---

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.
19 changes: 18 additions & 1 deletion apps/ensadmin/src/components/connection/cards/ensnode-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ function ENSNodeConfigCardContent({
icon={<ENSApiIcon width={24} height={24} />}
version={
<p className="text-sm leading-normal font-normal text-muted-foreground">
v{ensApiPublicConfig.version}
v{ensApiPublicConfig.versionInfo.ensApi}
</p>
}
docsLink={new URL("https://ensnode.io/ensapi")}
Expand Down Expand Up @@ -336,6 +336,23 @@ function ENSNodeConfigCardContent({
</p>
}
/>
<InfoCardItem
label="ens-normalize.js"
value={
<p className={cardItemValueStyles}>{ensApiPublicConfig.versionInfo.ensNormalize}</p>
}
additionalInfo={
<p>
Version of the{" "}
<ExternalLinkWithIcon
href={`https://www.npmjs.com/package/@adraffy/ens-normalize/v/${ensApiPublicConfig.versionInfo.ensNormalize}`}
>
@adraffy/ens-normalize
</ExternalLinkWithIcon>{" "}
package used for ENS name normalization.
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
</p>
Comment thread
tk-o marked this conversation as resolved.
}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</InfoCardItems>
<InfoCardFeatures activated={ensApiPublicConfig.theGraphFallback.canFallback}>
<InfoCardFeature
Expand Down
9 changes: 5 additions & 4 deletions apps/ensapi/src/config/config.schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { buildConfigFromEnvironment, buildEnsApiPublicConfig } from "@/config/co
import { ENSApi_DEFAULT_PORT } from "@/config/defaults";
import type { EnsApiEnvironment } from "@/config/environment";
import logger from "@/lib/logger";
import { ensApiVersionInfo } from "@/lib/version-info";

vi.mock("@/lib/logger", () => ({
default: {
Expand Down Expand Up @@ -45,9 +46,9 @@ const ENSINDEXER_PUBLIC_CONFIG = {
versionInfo: {
ensDb: packageJson.version,
ensIndexer: packageJson.version,
ensNormalize: "1.1.1",
nodejs: "1.1.1",
ponder: "1.1.1",
ensNormalize: ensApiVersionInfo.ensNormalize,
nodejs: "20.0.0",
ponder: "0.8.0",
},
} satisfies ENSIndexerPublicConfig;

Expand Down Expand Up @@ -168,7 +169,7 @@ describe("buildEnsApiPublicConfig", () => {
const result = buildEnsApiPublicConfig(mockConfig);

expect(result).toStrictEqual({
version: packageJson.version,
versionInfo: ensApiVersionInfo,
theGraphFallback: {
canFallback: false,
reason: "not-subgraph-compatible",
Expand Down
5 changes: 2 additions & 3 deletions apps/ensapi/src/config/config.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import packageJson from "@/../package.json" with { type: "json" };

import pRetry from "p-retry";
import { prettifyError, ZodError, z } from "zod/v4";

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

/**
* Schema for validating custom referral program edition config set URL.
Expand Down Expand Up @@ -119,7 +118,7 @@ export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promis
*/
export function buildEnsApiPublicConfig(config: EnsApiConfig): EnsApiPublicConfig {
return {
version: packageJson.version,
versionInfo: ensApiVersionInfo,
theGraphFallback: canFallbackToTheGraph({
namespace: config.namespace,
// NOTE: very important here that we replace the actual server-side api key with a placeholder
Expand Down
12 changes: 12 additions & 0 deletions apps/ensapi/src/config/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import packageJson from "@/../package.json" with { type: "json" };
import type { ENSIndexerPublicConfig } from "@ensnode/ensnode-sdk";
import type { ZodCheckFnInput } from "@ensnode/ensnode-sdk/internal";

import { ensApiVersionInfo } from "@/lib/version-info";

// Invariant: ENSIndexerPublicConfig VersionInfo must match ENSApi
export function invariant_ensIndexerPublicConfigVersionInfo(
ctx: ZodCheckFnInput<{
Expand Down Expand Up @@ -42,4 +44,14 @@ export function invariant_ensIndexerPublicConfigVersionInfo(
message: `Version Mismatch: ENSRainbow@${ensIndexerPublicConfig.ensRainbowPublicConfig.version} !== ENSApi@${packageJson.version}`,
});
}

// Invariant: `@adraffy/ens-normalize` package version must match between ENSApi & ENSIndexer
if (ensIndexerPublicConfig.versionInfo.ensNormalize !== ensApiVersionInfo.ensNormalize) {
ctx.issues.push({
code: "custom",
path: ["ensIndexerPublicConfig.versionInfo.ensNormalize"],
input: ensIndexerPublicConfig.versionInfo.ensNormalize,
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}`,
});
Comment thread
tk-o marked this conversation as resolved.
}
}
109 changes: 109 additions & 0 deletions apps/ensapi/src/lib/version-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import packageJson from "@/../package.json" with { type: "json" };

import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import type { EnsApiVersionInfo } from "@ensnode/ensnode-sdk";

/**
* Get ENS API version
*/
function getEnsApiVersion(): string {
return packageJson.version;
}

/**
* Get NPM package version.
*
* Note:
* Since we use PNPM's `catalog:` references, reading directly from
* the `package.json` file would give us `catalog:` values, and not resolved
* version values. We need the latter, so we implement our own version
* resolution method.
*/
function getPackageVersion(packageName: string): string {
try {
// Start from current file's directory
const currentFile = fileURLToPath(import.meta.url);
let searchDir = dirname(currentFile);

while (true) {
const workspaceFile = join(searchDir, "pnpm-workspace.yaml");
const isWorkspaceRoot = existsSync(workspaceFile);

// Check for node_modules in current directory
const nodeModulesPath = join(searchDir, "node_modules", packageName, "package.json");
if (existsSync(nodeModulesPath)) {
const packageJson = JSON.parse(readFileSync(nodeModulesPath, "utf8"));
return packageJson.version;
}

// Check PNPM's .pnpm virtual store
const pnpmDir = join(searchDir, "node_modules", ".pnpm");
if (existsSync(pnpmDir)) {
const version = getPackageVersionFromPnpmStore(pnpmDir, packageName);
if (version) return version;
}

// If we're at workspace root and still haven't found it, stop searching
if (isWorkspaceRoot) {
throw new Error(
`Package ${packageName} not found in any node_modules up to workspace root`,
);
}

// Move up one directory
const parentDir = dirname(searchDir);

// Prevent infinite loop if we reach filesystem root
if (parentDir === searchDir) {
throw new Error(`Package ${packageName} not found and no workspace root detected`);
}

searchDir = parentDir;
}
} catch {
return "unknown";
}
Comment thread
tk-o marked this conversation as resolved.
}

/**
* Get package version from PNPM virtual store.
*
* PNPM stores packages in its virtual store that
* can be located at, for example, `./node_modules/.pnpm` path.
*
* This function is used in a fallback method by {@link getPackageVersion} to
* get package version by package name in case it was not found
* directly in `./node_modules` directory.
*/
function getPackageVersionFromPnpmStore(pnpmDir: string, packageName: string): string | null {
try {
const entries = readdirSync(pnpmDir);

// Convert package name to PNPM's format: @scope/name -> @scope+name
const normalizedName = packageName.replace("/", "+");
Comment thread
tk-o marked this conversation as resolved.
Comment thread
vercel[bot] marked this conversation as resolved.

// Find entries that match the package name
// They will be in format: packagename@version or @scope+packagename@version
for (const entry of entries) {
if (entry.startsWith(`${normalizedName}@`)) {
const pkgPath = join(pnpmDir, entry, "node_modules", packageName, "package.json");
if (existsSync(pkgPath)) {
const packageJson = JSON.parse(readFileSync(pkgPath, "utf8"));
return packageJson.version;
}
Comment thread
tk-o marked this conversation as resolved.
}
}
} catch (_error) {
// Ignore errors in this helper
}

return null;
}

export const ensApiVersionInfo = {
ensApi: getEnsApiVersion(),
ensNormalize: getPackageVersion("@adraffy/ens-normalize"),
} as const satisfies EnsApiVersionInfo;
61 changes: 34 additions & 27 deletions docs/docs.ensnode.io/ensapi-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,6 @@
"schema": {
"type": "object",
"properties": {
"version": { "type": "string", "minLength": 1 },
"theGraphFallback": {
"oneOf": [
{
"type": "object",
"properties": {
"canFallback": { "type": "boolean", "enum": [true] },
"url": { "type": "string" }
},
"required": ["canFallback", "url"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"canFallback": { "type": "boolean", "enum": [false] },
"reason": {
"type": "string",
"enum": ["not-subgraph-compatible", "no-api-key", "no-subgraph-url"]
}
},
"required": ["canFallback", "reason"],
"additionalProperties": false
}
]
},
"ensIndexerPublicConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -144,9 +118,42 @@
"plugins",
"versionInfo"
]
},
"theGraphFallback": {
"oneOf": [
{
"type": "object",
"properties": {
"canFallback": { "type": "boolean", "enum": [true] },
"url": { "type": "string" }
},
"required": ["canFallback", "url"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"canFallback": { "type": "boolean", "enum": [false] },
"reason": {
"type": "string",
"enum": ["not-subgraph-compatible", "no-api-key", "no-subgraph-url"]
}
},
"required": ["canFallback", "reason"],
"additionalProperties": false
}
]
},
"versionInfo": {
"type": "object",
"properties": {
"ensApi": { "type": "string", "minLength": 1 },
"ensNormalize": { "type": "string", "minLength": 1 }
},
"required": ["ensApi", "ensNormalize"]
}
},
"required": ["version", "theGraphFallback", "ensIndexerPublicConfig"]
"required": ["ensIndexerPublicConfig", "theGraphFallback", "versionInfo"]
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion packages/ensnode-sdk/src/ensapi/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ const EXAMPLE_PRIMARY_NAMES_RESPONSE = {
const EXAMPLE_ERROR_RESPONSE: ErrorResponse = { message: "error" };

const EXAMPLE_CONFIG_RESPONSE = {
version: "0.32.0",
versionInfo: {
ensApi: "1.9.0",
ensNormalize: "1.11.1",
},
theGraphFallback: {
canFallback: false,
reason: "no-api-key",
Expand Down
17 changes: 13 additions & 4 deletions packages/ensnode-sdk/src/ensapi/config/conversions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type { SerializedEnsApiPublicConfig } from "./serialized-types";
import type { EnsApiPublicConfig } from "./types";

const MOCK_ENSAPI_PUBLIC_CONFIG = {
version: "0.36.0",
versionInfo: {
ensApi: "1.9.0",
ensNormalize: "1.11.1",
},
theGraphFallback: {
canFallback: false,
reason: "no-api-key",
Expand Down Expand Up @@ -44,7 +47,10 @@ describe("ENSApi Config Serialization/Deserialization", () => {
const result = serializeEnsApiPublicConfig(MOCK_ENSAPI_PUBLIC_CONFIG);

expect(result).toEqual({
version: "0.36.0",
versionInfo: {
ensApi: "1.9.0",
ensNormalize: "1.11.1",
},
theGraphFallback: {
canFallback: false,
reason: "no-api-key",
Expand Down Expand Up @@ -84,11 +90,14 @@ describe("ENSApi Config Serialization/Deserialization", () => {
it("handles validation errors with custom value label", () => {
const invalidConfig = {
...MOCK_SERIALIZED_ENSAPI_PUBLIC_CONFIG,
version: "", // Invalid: empty string
versionInfo: {
ensApi: "",
ensNormalize: "",
},
};

expect(() => deserializeEnsApiPublicConfig(invalidConfig, "testConfig")).toThrow(
/testConfig.version/,
/testConfig.versionInfo.ensApi/,
);
});
});
Expand Down
6 changes: 3 additions & 3 deletions packages/ensnode-sdk/src/ensapi/config/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import type { EnsApiPublicConfig } from "./types";
export function serializeEnsApiPublicConfig(
config: EnsApiPublicConfig,
): SerializedEnsApiPublicConfig {
const { version, theGraphFallback, ensIndexerPublicConfig } = config;
const { ensIndexerPublicConfig, theGraphFallback, versionInfo } = config;

return {
version,
theGraphFallback,
ensIndexerPublicConfig: serializeEnsIndexerPublicConfig(ensIndexerPublicConfig),
theGraphFallback,
versionInfo,
} satisfies SerializedEnsApiPublicConfig;
}

Expand Down
Loading
Loading