diff --git a/.changeset/fair-ghosts-poke.md b/.changeset/fair-ghosts-poke.md new file mode 100644 index 0000000000..763fa3c301 --- /dev/null +++ b/.changeset/fair-ghosts-poke.md @@ -0,0 +1,5 @@ +--- +"ensapi": patch +--- + +Fixes error handling in app.onError to return correct HTTP status codes and resolves OpenAPI schema generation issue diff --git a/.changeset/nice-foxes-cheat.md b/.changeset/nice-foxes-cheat.md new file mode 100644 index 0000000000..196c4af1b3 --- /dev/null +++ b/.changeset/nice-foxes-cheat.md @@ -0,0 +1,5 @@ +--- +"ensapi": patch +--- + +use example requests for openapi doc diff --git a/.changeset/smooth-foxes-boil.md b/.changeset/smooth-foxes-boil.md new file mode 100644 index 0000000000..df568a349c --- /dev/null +++ b/.changeset/smooth-foxes-boil.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-sdk": patch +--- + +add examples for type system diff --git a/apps/ensapi/src/app.ts b/apps/ensapi/src/app.ts index 0bd7d13d14..9878bd488b 100644 --- a/apps/ensapi/src/app.ts +++ b/apps/ensapi/src/app.ts @@ -75,7 +75,7 @@ app.get("/health", async (c) => { // log hono errors to console app.onError((error, ctx) => { logger.error(error); - return errorResponse(ctx, "Internal Server Error"); + return errorResponse(ctx, error); }); export default app; diff --git a/apps/ensapi/src/handlers/api/explore/name-tokens-api.routes.ts b/apps/ensapi/src/handlers/api/explore/name-tokens-api.routes.ts index 01a1c773f4..291074a30d 100644 --- a/apps/ensapi/src/handlers/api/explore/name-tokens-api.routes.ts +++ b/apps/ensapi/src/handlers/api/explore/name-tokens-api.routes.ts @@ -4,6 +4,7 @@ import { ErrorResponseSchema, makeNameTokensResponseSchema, makeNodeSchema, + nameTokensResponseOkExample, } from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; @@ -42,7 +43,9 @@ export const getNameTokensRoute = createRoute({ description: "Name tokens known", content: { "application/json": { - schema: makeNameTokensResponseSchema("Name Tokens Response", true), + schema: makeNameTokensResponseSchema("Name Tokens Response", true).openapi({ + example: nameTokensResponseOkExample, + }), }, }, }, @@ -81,5 +84,3 @@ export const getNameTokensRoute = createRoute({ }, }, }); - -export const routes = [getNameTokensRoute]; diff --git a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.routes.ts b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.routes.ts index b9f70c9835..348f90d1df 100644 --- a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.routes.ts +++ b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.routes.ts @@ -6,10 +6,14 @@ import { RegistrarActionsOrders, } from "@ensnode/ensnode-sdk"; import { + ErrorResponseSchema, makeLowercaseAddressSchema, makeNodeSchema, makePositiveIntegerSchema, + makeRegistrarActionsResponseErrorSchema, + makeSerializedRegistrarActionsResponseOkSchema, makeUnixTimestampSchema, + registrarActionsResponseOkExample, } from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; @@ -59,12 +63,14 @@ export const registrarActionsQuerySchema = z .pipe(z.coerce.number()) .pipe(makeUnixTimestampSchema("beginTimestamp")) .optional() + .openapi({ type: "integer" }) .describe("Filter actions at or after this Unix timestamp"), endTimestamp: params.queryParam .pipe(z.coerce.number()) .pipe(makeUnixTimestampSchema("endTimestamp")) .optional() + .openapi({ type: "integer" }) .describe("Filter actions at or before this Unix timestamp"), }) .refine( @@ -96,12 +102,27 @@ export const getRegistrarActionsRoute = createRoute({ responses: { 200: { description: "Successfully retrieved registrar actions", + content: { + "application/json": { + schema: makeSerializedRegistrarActionsResponseOkSchema( + "Registrar Actions Response", + ).openapi({ + example: registrarActionsResponseOkExample, + }), + }, + }, }, 400: { description: "Invalid query", + content: { "application/json": { schema: ErrorResponseSchema } }, }, 500: { description: "Internal server error", + content: { + "application/json": { + schema: makeRegistrarActionsResponseErrorSchema("Registrar Actions Error Response"), + }, + }, }, }, }); @@ -125,14 +146,29 @@ export const getRegistrarActionsByParentNodeRoute = createRoute({ responses: { 200: { description: "Successfully retrieved registrar actions", + content: { + "application/json": { + schema: makeSerializedRegistrarActionsResponseOkSchema( + "Registrar Actions By ParentNode Response", + ).openapi({ + example: registrarActionsResponseOkExample, + }), + }, + }, }, 400: { description: "Invalid input", + content: { "application/json": { schema: ErrorResponseSchema } }, }, 500: { description: "Internal server error", + content: { + "application/json": { + schema: makeRegistrarActionsResponseErrorSchema( + "Registrar Actions By ParentNode Error Response", + ), + }, + }, }, }, }); - -export const routes = [getRegistrarActionsRoute, getRegistrarActionsByParentNodeRoute]; diff --git a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts index 1e14060ab4..ea245cc823 100644 --- a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts +++ b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts @@ -95,6 +95,7 @@ app.openapi(getRegistrarActionsRoute, async (c) => { registrarActions, pageContext, } satisfies RegistrarActionsResponseOk), + 200, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; @@ -166,6 +167,7 @@ app.openapi(getRegistrarActionsByParentNodeRoute, async (c) => { pageContext, accurateAsOf, } satisfies RegistrarActionsResponseOk), + 200, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; diff --git a/apps/ensapi/src/handlers/api/meta/realtime-api.routes.ts b/apps/ensapi/src/handlers/api/meta/realtime-api.routes.ts index 56fd2f5419..2b38f86c6d 100644 --- a/apps/ensapi/src/handlers/api/meta/realtime-api.routes.ts +++ b/apps/ensapi/src/handlers/api/meta/realtime-api.routes.ts @@ -46,5 +46,3 @@ export const realtimeGetMeta = createRoute({ }, }, }); - -export const routes = [realtimeGetMeta]; diff --git a/apps/ensapi/src/handlers/api/meta/status-api.routes.ts b/apps/ensapi/src/handlers/api/meta/status-api.routes.ts index a8487d3856..2fbd41f22d 100644 --- a/apps/ensapi/src/handlers/api/meta/status-api.routes.ts +++ b/apps/ensapi/src/handlers/api/meta/status-api.routes.ts @@ -53,5 +53,3 @@ export const getIndexingStatusRoute = createRoute({ }, }, }); - -export const routes = [getConfigRoute, getIndexingStatusRoute]; diff --git a/apps/ensapi/src/handlers/api/resolution/resolution-api.routes.ts b/apps/ensapi/src/handlers/api/resolution/resolution-api.routes.ts index c6eadb165d..076722baee 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolution-api.routes.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolution-api.routes.ts @@ -4,6 +4,9 @@ import { makeResolvePrimaryNameResponseSchema, makeResolvePrimaryNamesResponseSchema, makeResolveRecordsResponseSchema, + resolvePrimaryNameResponseExample, + resolvePrimaryNamesResponseExample, + resolveRecordsResponseExample, } from "@ensnode/ensnode-sdk/internal"; import { params } from "@/lib/handlers/params.schema"; @@ -38,7 +41,9 @@ export const resolveRecordsRoute = createRoute({ description: "Successfully resolved records", content: { "application/json": { - schema: makeResolveRecordsResponseSchema(), + schema: makeResolveRecordsResponseSchema().openapi({ + example: resolveRecordsResponseExample, + }), }, }, }, @@ -67,7 +72,9 @@ export const resolvePrimaryNameRoute = createRoute({ description: "Successfully resolved name", content: { "application/json": { - schema: makeResolvePrimaryNameResponseSchema(), + schema: makeResolvePrimaryNameResponseSchema().openapi({ + example: resolvePrimaryNameResponseExample, + }), }, }, }, @@ -96,11 +103,11 @@ export const resolvePrimaryNamesRoute = createRoute({ description: "Successfully resolved records", content: { "application/json": { - schema: makeResolvePrimaryNamesResponseSchema(), + schema: makeResolvePrimaryNamesResponseSchema().openapi({ + example: resolvePrimaryNamesResponseExample, + }), }, }, }, }, }); - -export const routes = [resolveRecordsRoute, resolvePrimaryNameRoute, resolvePrimaryNamesRoute]; diff --git a/apps/ensapi/src/lib/handlers/params.schema.test.ts b/apps/ensapi/src/lib/handlers/params.schema.test.ts index 3b9d83421b..13f43fb075 100644 --- a/apps/ensapi/src/lib/handlers/params.schema.test.ts +++ b/apps/ensapi/src/lib/handlers/params.schema.test.ts @@ -26,7 +26,7 @@ describe("params.selection", () => { it("parses selection", () => { expect( params.selection.parse({ - name: "true", + nameRecord: "true", addresses: "60,0", texts: "example,hello", }), diff --git a/apps/ensapi/src/lib/handlers/params.schema.ts b/apps/ensapi/src/lib/handlers/params.schema.ts index 00d83c22d6..cc9560e6ac 100644 --- a/apps/ensapi/src/lib/handlers/params.schema.ts +++ b/apps/ensapi/src/lib/handlers/params.schema.ts @@ -37,33 +37,67 @@ const stringarray = z const name = z .string() .refine(isNormalizedName, "Must be normalized, see https://docs.ens.domains/resolution/names/") - .transform((val) => val as Name); + .transform((val) => val as Name) + .describe("ENS name to resolve (e.g. 'vitalik.eth'). Must be normalized per ENSIP-15."); -const trace = z.optional(boolstring).default(false).openapi({ default: false }); -const accelerate = z.optional(boolstring).default(false).openapi({ default: false }); -const address = makeLowercaseAddressSchema(); -const defaultableChainId = makeDefaultableChainIdStringSchema(); -const coinType = makeCoinTypeStringSchema(); +const trace = z + .optional(boolstring) + .default(false) + .describe("Include detailed resolution trace information in the response.") + .openapi({ default: false }); -const chainIdsWithoutDefaultChainId = z.optional( - stringarray.pipe(z.array(defaultableChainId.pipe(excludingDefaultChainId))), +const accelerate = z + .optional(boolstring) + .default(false) + .describe("Attempt accelerated CCIP-Read resolution using L1 data.") + .openapi({ + default: false, + }); +const address = makeLowercaseAddressSchema().describe( + "EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045').", +); +const defaultableChainId = makeDefaultableChainIdStringSchema().describe( + "Chain ID as a string (e.g. '1' for Ethereum mainnet). Use '0' for the default EVM chain.", ); +const coinType = makeCoinTypeStringSchema(); + +const chainIdsWithoutDefaultChainId = z + .optional(stringarray.pipe(z.array(defaultableChainId.pipe(excludingDefaultChainId)))) + .describe( + "Comma-separated list of chain IDs to resolve primary names for (e.g. '1,10,8453'). The default EVM chain ID (0) is not allowed.", + ); const rawSelectionParams = z.object({ - name: z.string().optional(), - addresses: z.string().optional(), - texts: z.string().optional(), + nameRecord: z + .string() + .optional() + .describe("Whether to include the ENS name record in the response.") + .openapi({ + enum: ["true", "false"], + }), + addresses: z + .string() + .optional() + .describe( + "Comma-separated list of coin types to resolve addresses for (e.g. '60' for ETH, '2147483658' for OP).", + ), + texts: z + .string() + .optional() + .describe( + "Comma-separated list of text record keys to resolve (e.g. 'avatar,description,url').", + ), }); const selection = z .object({ - name: z.optional(boolstring), + nameRecord: z.optional(boolstring), addresses: z.optional(stringarray.pipe(z.array(coinType))), texts: z.optional(stringarray), }) .transform((value, ctx) => { const selection: ResolverRecordsSelection = { - ...(value.name && { name: true }), + ...(value.nameRecord && { name: true }), ...(value.addresses && { addresses: value.addresses }), ...(value.texts && { texts: value.texts }), }; diff --git a/apps/ensindexer/src/config/config.test.ts b/apps/ensindexer/src/config/config.test.ts index 3052b4b87b..f1f8a052b5 100644 --- a/apps/ensindexer/src/config/config.test.ts +++ b/apps/ensindexer/src/config/config.test.ts @@ -193,17 +193,17 @@ describe("config (with base env)", () => { it("throws an error when ENSINDEXER_SCHEMA_NAME is not set", async () => { vi.stubEnv("ENSINDEXER_SCHEMA_NAME", undefined); - await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name is required/); + await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME is required/); }); it("throws an error when ENSINDEXER_SCHEMA_NAME is empty", async () => { vi.stubEnv("ENSINDEXER_SCHEMA_NAME", ""); - await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/); + await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME cannot be an empty string/); }); it("throws an error when ENSINDEXER_SCHEMA_NAME is only whitespace", async () => { vi.stubEnv("ENSINDEXER_SCHEMA_NAME", " "); - await expect(getConfig()).rejects.toThrow(/ENSIndexer Schema Name cannot be an empty string/); + await expect(getConfig()).rejects.toThrow(/ENSINDEXER_SCHEMA_NAME cannot be an empty string/); }); }); @@ -423,27 +423,27 @@ describe("config (with base env)", () => { it("throws an error if ENSDB_URL is not set", async () => { vi.stubEnv("ENSDB_URL", undefined); - await expect(getConfig()).rejects.toThrow(/Invalid input/); + await expect(getConfig()).rejects.toThrow(/ENSDB_URL is required/); }); it("throws an error if ENSDB_URL is empty", async () => { vi.stubEnv("ENSDB_URL", ""); - await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/); + await expect(getConfig()).rejects.toThrow(/Invalid connection string/); }); it("throws an error if ENSDB_URL is not a valid postgres connection string", async () => { vi.stubEnv("ENSDB_URL", "not-a-postgres-connection-string"); - await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/); + await expect(getConfig()).rejects.toThrow(/Invalid connection string/); }); it("throws an error if ENSDB_URL uses the wrong protocol", async () => { vi.stubEnv("ENSDB_URL", "mysql://user:password@localhost:3306/mydb"); - await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/); + await expect(getConfig()).rejects.toThrow(/Invalid connection string/); }); it("throws an error if ENSDB_URL is missing required components", async () => { vi.stubEnv("ENSDB_URL", "postgresql://localhost:5432"); - await expect(getConfig()).rejects.toThrow(/Invalid PostgreSQL connection string/); + await expect(getConfig()).rejects.toThrow(/Invalid connection string/); }); it("accepts postgres:// protocol", async () => { diff --git a/docs/docs.ensnode.io/ensapi-openapi.json b/docs/docs.ensnode.io/ensapi-openapi.json index edefe03597..d28258b78c 100644 --- a/docs/docs.ensnode.io/ensapi-openapi.json +++ b/docs/docs.ensnode.io/ensapi-openapi.json @@ -969,19 +969,66 @@ "summary": "Resolve ENS Records", "description": "Resolves ENS records for a given name", "parameters": [ - { "schema": { "type": "string" }, "required": true, "name": "name", "in": "path" }, - { "schema": { "type": "string" }, "required": false, "name": "name", "in": "query" }, - { "schema": { "type": "string" }, "required": false, "name": "addresses", "in": "query" }, - { "schema": { "type": "string" }, "required": false, "name": "texts", "in": "query" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "string", + "description": "ENS name to resolve (e.g. 'vitalik.eth'). Must be normalized per ENSIP-15." + }, + "required": true, + "description": "ENS name to resolve (e.g. 'vitalik.eth'). Must be normalized per ENSIP-15.", + "name": "name", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Whether to include the ENS name record in the response.", + "enum": ["true", "false"] + }, + "required": false, + "description": "Whether to include the ENS name record in the response.", + "name": "nameRecord", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Comma-separated list of coin types to resolve addresses for (e.g. '60' for ETH, '2147483658' for OP)." + }, + "required": false, + "description": "Comma-separated list of coin types to resolve addresses for (e.g. '60' for ETH, '2147483658' for OP).", + "name": "addresses", + "in": "query" + }, + { + "schema": { + "type": "string", + "description": "Comma-separated list of text record keys to resolve (e.g. 'avatar,description,url')." + }, "required": false, + "description": "Comma-separated list of text record keys to resolve (e.g. 'avatar,description,url').", + "name": "texts", + "in": "query" + }, + { + "schema": { + "type": "boolean", + "description": "Include detailed resolution trace information in the response.", + "default": false + }, + "required": false, + "description": "Include detailed resolution trace information in the response.", "name": "trace", "in": "query" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "boolean", + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", + "default": false + }, "required": false, + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", "name": "accelerate", "in": "query" } @@ -1012,7 +1059,16 @@ "accelerationAttempted": { "type": "boolean" }, "trace": { "type": "array", "items": {} } }, - "required": ["records", "accelerationRequested", "accelerationAttempted"] + "required": ["records", "accelerationRequested", "accelerationAttempted"], + "example": { + "records": { + "name": "vitalik.eth", + "addresses": { "60": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" }, + "texts": { "description": "mi pinxe lo crino tcati" } + }, + "accelerationRequested": false, + "accelerationAttempted": false + } } } } @@ -1027,17 +1083,45 @@ "summary": "Resolve Primary Name", "description": "Resolves a primary name for a given `address` and `chainId`", "parameters": [ - { "schema": { "type": "string" }, "required": true, "name": "address", "in": "path" }, - { "schema": { "type": "string" }, "required": true, "name": "chainId", "in": "path" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "string", + "description": "EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045')." + }, + "required": true, + "description": "EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045').", + "name": "address", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Chain ID as a string (e.g. '1' for Ethereum mainnet). Use '0' for the default EVM chain." + }, + "required": true, + "description": "Chain ID as a string (e.g. '1' for Ethereum mainnet). Use '0' for the default EVM chain.", + "name": "chainId", + "in": "path" + }, + { + "schema": { + "type": "boolean", + "description": "Include detailed resolution trace information in the response.", + "default": false + }, "required": false, + "description": "Include detailed resolution trace information in the response.", "name": "trace", "in": "query" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "boolean", + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", + "default": false + }, "required": false, + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", "name": "accelerate", "in": "query" } @@ -1055,7 +1139,12 @@ "accelerationAttempted": { "type": "boolean" }, "trace": { "type": "array", "items": {} } }, - "required": ["name", "accelerationRequested", "accelerationAttempted"] + "required": ["name", "accelerationRequested", "accelerationAttempted"], + "example": { + "name": "jesse.base.eth", + "accelerationRequested": false, + "accelerationAttempted": false + } } } } @@ -1070,17 +1159,45 @@ "summary": "Resolve Primary Names", "description": "Resolves all primary names for a given address across multiple chains", "parameters": [ - { "schema": { "type": "string" }, "required": true, "name": "address", "in": "path" }, - { "schema": { "type": "string" }, "required": false, "name": "chainIds", "in": "query" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "string", + "description": "EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045')." + }, + "required": true, + "description": "EVM wallet address (e.g. '0xd8da6bf26964af9d7eed9e03e53415d37aa96045').", + "name": "address", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Comma-separated list of chain IDs to resolve primary names for (e.g. '1,10,8453'). The default EVM chain ID (0) is not allowed." + }, + "required": false, + "description": "Comma-separated list of chain IDs to resolve primary names for (e.g. '1,10,8453'). The default EVM chain ID (0) is not allowed.", + "name": "chainIds", + "in": "query" + }, + { + "schema": { + "type": "boolean", + "description": "Include detailed resolution trace information in the response.", + "default": false + }, "required": false, + "description": "Include detailed resolution trace information in the response.", "name": "trace", "in": "query" }, { - "schema": { "type": "boolean", "default": false }, + "schema": { + "type": "boolean", + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", + "default": false + }, "required": false, + "description": "Attempt accelerated CCIP-Read resolution using L1 data.", "name": "accelerate", "in": "query" } @@ -1101,7 +1218,19 @@ "accelerationAttempted": { "type": "boolean" }, "trace": { "type": "array", "items": {} } }, - "required": ["names", "accelerationRequested", "accelerationAttempted"] + "required": ["names", "accelerationRequested", "accelerationAttempted"], + "example": { + "names": { + "1": "jesse.base.eth", + "10": null, + "8453": "jesse.base.eth", + "42161": null, + "59144": null, + "534352": null + }, + "accelerationRequested": false, + "accelerationAttempted": false + } } } } @@ -1328,7 +1457,36 @@ } ] } - ] + ], + "example": { + "responseCode": "ok", + "registeredNameTokens": { + "domainId": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "name": "vitalik.eth", + "tokens": [ + { + "token": { + "assetNamespace": "erc721", + "contract": { + "chainId": 1, + "address": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85" + }, + "tokenId": "0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc" + }, + "ownership": { + "ownershipType": "fully-onchain", + "owner": { + "chainId": 1, + "address": "0x220866b1a2219f40e72f5c628b65d54268ca3a9d" + } + }, + "mintStatus": "minted" + } + ], + "expiresAt": 2461152330, + "accurateAsOf": 1700000000 + } + } } } } @@ -1825,14 +1983,20 @@ "in": "query" }, { - "schema": { "description": "Filter actions at or after this Unix timestamp" }, + "schema": { + "type": "integer", + "description": "Filter actions at or after this Unix timestamp" + }, "required": false, "description": "Filter actions at or after this Unix timestamp", "name": "beginTimestamp", "in": "query" }, { - "schema": { "description": "Filter actions at or before this Unix timestamp" }, + "schema": { + "type": "integer", + "description": "Filter actions at or before this Unix timestamp" + }, "required": false, "description": "Filter actions at or before this Unix timestamp", "name": "endTimestamp", @@ -1840,9 +2004,420 @@ } ], "responses": { - "200": { "description": "Successfully retrieved registrar actions" }, - "400": { "description": "Invalid query" }, - "500": { "description": "Internal server error" } + "200": { + "description": "Successfully retrieved registrar actions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "responseCode": { "type": "string", "enum": ["ok"] }, + "registrarActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string", "minLength": 1 }, + "incrementalDuration": { "type": "number" }, + "registrant": { "type": "string" }, + "registrationLifecycle": { + "type": "object", + "properties": { + "subregistry": { + "type": "object", + "properties": { + "subregistryId": { + "type": "object", + "properties": { + "chainId": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "address": { "type": "string" } + }, + "required": ["chainId", "address"], + "additionalProperties": false + }, + "node": { "type": "string" } + }, + "required": ["subregistryId", "node"] + }, + "node": { "type": "string" }, + "expiresAt": { "type": "integer" } + }, + "required": ["subregistry", "node", "expiresAt"] + }, + "pricing": { + "anyOf": [ + { + "type": "object", + "properties": { + "baseCost": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "premium": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "total": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + } + }, + "required": ["baseCost", "premium", "total"] + }, + { + "type": "object", + "properties": { + "baseCost": { "type": "null" }, + "premium": { "type": "null" }, + "total": { "type": "null" } + }, + "required": ["baseCost", "premium", "total"] + } + ] + }, + "referral": { + "anyOf": [ + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "string" }, + "decodedReferrer": { "type": "string" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + }, + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "null" }, + "decodedReferrer": { "type": "null" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + } + ] + }, + "block": { + "type": "object", + "properties": { + "timestamp": { "type": "integer" }, + "number": { "type": "integer", "minimum": 0 } + }, + "required": ["timestamp", "number"], + "additionalProperties": false + }, + "transactionHash": { "type": "string" }, + "eventIds": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "type": { "type": "string", "enum": ["registration"] } + }, + "required": [ + "id", + "incrementalDuration", + "registrant", + "registrationLifecycle", + "pricing", + "referral", + "block", + "transactionHash", + "eventIds", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { "type": "string", "minLength": 1 }, + "incrementalDuration": { "type": "number" }, + "registrant": { "type": "string" }, + "registrationLifecycle": { + "type": "object", + "properties": { + "subregistry": { + "type": "object", + "properties": { + "subregistryId": { + "type": "object", + "properties": { + "chainId": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "address": { "type": "string" } + }, + "required": ["chainId", "address"], + "additionalProperties": false + }, + "node": { "type": "string" } + }, + "required": ["subregistryId", "node"] + }, + "node": { "type": "string" }, + "expiresAt": { "type": "integer" } + }, + "required": ["subregistry", "node", "expiresAt"] + }, + "pricing": { + "anyOf": [ + { + "type": "object", + "properties": { + "baseCost": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "premium": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "total": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + } + }, + "required": ["baseCost", "premium", "total"] + }, + { + "type": "object", + "properties": { + "baseCost": { "type": "null" }, + "premium": { "type": "null" }, + "total": { "type": "null" } + }, + "required": ["baseCost", "premium", "total"] + } + ] + }, + "referral": { + "anyOf": [ + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "string" }, + "decodedReferrer": { "type": "string" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + }, + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "null" }, + "decodedReferrer": { "type": "null" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + } + ] + }, + "block": { + "type": "object", + "properties": { + "timestamp": { "type": "integer" }, + "number": { "type": "integer", "minimum": 0 } + }, + "required": ["timestamp", "number"], + "additionalProperties": false + }, + "transactionHash": { "type": "string" }, + "eventIds": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "type": { "type": "string", "enum": ["renewal"] } + }, + "required": [ + "id", + "incrementalDuration", + "registrant", + "registrationLifecycle", + "pricing", + "referral", + "block", + "transactionHash", + "eventIds", + "type" + ] + } + ] + }, + "name": { "type": "string" } + }, + "required": ["action", "name"] + } + }, + "pageContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "totalRecords": { "type": "number", "enum": [0] }, + "totalPages": { "type": "number", "enum": [1] }, + "hasNext": { "type": "boolean", "enum": [false] }, + "hasPrev": { "type": "boolean", "enum": [false] }, + "page": { "type": "integer", "exclusiveMinimum": 0 }, + "recordsPerPage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100 + } + }, + "required": [ + "totalRecords", + "totalPages", + "hasNext", + "hasPrev", + "page", + "recordsPerPage" + ] + }, + { + "type": "object", + "properties": { + "totalRecords": { "type": "integer", "exclusiveMinimum": 0 }, + "totalPages": { "type": "integer", "exclusiveMinimum": 0 }, + "hasNext": { "type": "boolean" }, + "hasPrev": { "type": "boolean" }, + "startIndex": { "type": "integer", "minimum": 0 }, + "endIndex": { "type": "integer", "minimum": 0 }, + "page": { "type": "integer", "exclusiveMinimum": 0 }, + "recordsPerPage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100 + } + }, + "required": [ + "totalRecords", + "totalPages", + "hasNext", + "hasPrev", + "startIndex", + "endIndex", + "page", + "recordsPerPage" + ] + } + ] + }, + "accurateAsOf": { "type": "integer" } + }, + "required": ["responseCode", "registrarActions", "pageContext"], + "example": { + "responseCode": "ok", + "registrarActions": [ + { + "action": { + "type": "registration", + "id": "0x0000000000000000000000000000000000000000000000000000000000000001", + "incrementalDuration": 31536000, + "registrant": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + "registrationLifecycle": { + "subregistry": { + "subregistryId": { + "chainId": 1, + "address": "0x253553366da8546fc250f225fe3d25d0c782303b" + }, + "node": "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae" + }, + "node": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "expiresAt": 1893456000 + }, + "pricing": { + "baseCost": { "amount": "1000000000000000", "currency": "ETH" }, + "premium": { "amount": "0", "currency": "ETH" }, + "total": { "amount": "1000000000000000", "currency": "ETH" } + }, + "referral": { "encodedReferrer": null, "decodedReferrer": null }, + "block": { "timestamp": 1700000000, "number": 18500000 }, + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "eventIds": [ + "0x0000000000000000000000000000000000000000000000000000000000000001" + ] + }, + "name": "vitalik.eth" + } + ], + "pageContext": { + "page": 1, + "recordsPerPage": 25, + "totalRecords": 1, + "totalPages": 1, + "hasNext": false, + "hasPrev": false, + "startIndex": 0, + "endIndex": 0 + }, + "accurateAsOf": 1700000000 + } + } + } + } + }, + "400": { + "description": "Invalid query", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "message": { "type": "string" }, "details": {} }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "responseCode": { "type": "string", "enum": ["error"] }, + "error": { + "type": "object", + "properties": { "message": { "type": "string" }, "details": {} }, + "required": ["message"] + } + }, + "required": ["responseCode", "error"], + "additionalProperties": false + } + } + } + } } } }, @@ -1919,14 +2494,20 @@ "in": "query" }, { - "schema": { "description": "Filter actions at or after this Unix timestamp" }, + "schema": { + "type": "integer", + "description": "Filter actions at or after this Unix timestamp" + }, "required": false, "description": "Filter actions at or after this Unix timestamp", "name": "beginTimestamp", "in": "query" }, { - "schema": { "description": "Filter actions at or before this Unix timestamp" }, + "schema": { + "type": "integer", + "description": "Filter actions at or before this Unix timestamp" + }, "required": false, "description": "Filter actions at or before this Unix timestamp", "name": "endTimestamp", @@ -1934,9 +2515,420 @@ } ], "responses": { - "200": { "description": "Successfully retrieved registrar actions" }, - "400": { "description": "Invalid input" }, - "500": { "description": "Internal server error" } + "200": { + "description": "Successfully retrieved registrar actions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "responseCode": { "type": "string", "enum": ["ok"] }, + "registrarActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string", "minLength": 1 }, + "incrementalDuration": { "type": "number" }, + "registrant": { "type": "string" }, + "registrationLifecycle": { + "type": "object", + "properties": { + "subregistry": { + "type": "object", + "properties": { + "subregistryId": { + "type": "object", + "properties": { + "chainId": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "address": { "type": "string" } + }, + "required": ["chainId", "address"], + "additionalProperties": false + }, + "node": { "type": "string" } + }, + "required": ["subregistryId", "node"] + }, + "node": { "type": "string" }, + "expiresAt": { "type": "integer" } + }, + "required": ["subregistry", "node", "expiresAt"] + }, + "pricing": { + "anyOf": [ + { + "type": "object", + "properties": { + "baseCost": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "premium": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "total": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + } + }, + "required": ["baseCost", "premium", "total"] + }, + { + "type": "object", + "properties": { + "baseCost": { "type": "null" }, + "premium": { "type": "null" }, + "total": { "type": "null" } + }, + "required": ["baseCost", "premium", "total"] + } + ] + }, + "referral": { + "anyOf": [ + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "string" }, + "decodedReferrer": { "type": "string" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + }, + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "null" }, + "decodedReferrer": { "type": "null" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + } + ] + }, + "block": { + "type": "object", + "properties": { + "timestamp": { "type": "integer" }, + "number": { "type": "integer", "minimum": 0 } + }, + "required": ["timestamp", "number"], + "additionalProperties": false + }, + "transactionHash": { "type": "string" }, + "eventIds": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "type": { "type": "string", "enum": ["registration"] } + }, + "required": [ + "id", + "incrementalDuration", + "registrant", + "registrationLifecycle", + "pricing", + "referral", + "block", + "transactionHash", + "eventIds", + "type" + ] + }, + { + "type": "object", + "properties": { + "id": { "type": "string", "minLength": 1 }, + "incrementalDuration": { "type": "number" }, + "registrant": { "type": "string" }, + "registrationLifecycle": { + "type": "object", + "properties": { + "subregistry": { + "type": "object", + "properties": { + "subregistryId": { + "type": "object", + "properties": { + "chainId": { + "type": "integer", + "exclusiveMinimum": 0 + }, + "address": { "type": "string" } + }, + "required": ["chainId", "address"], + "additionalProperties": false + }, + "node": { "type": "string" } + }, + "required": ["subregistryId", "node"] + }, + "node": { "type": "string" }, + "expiresAt": { "type": "integer" } + }, + "required": ["subregistry", "node", "expiresAt"] + }, + "pricing": { + "anyOf": [ + { + "type": "object", + "properties": { + "baseCost": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "premium": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + }, + "total": { + "type": "object", + "properties": { + "amount": { "type": "string", "pattern": "^\\d+$" }, + "currency": { "type": "string", "enum": ["ETH"] } + }, + "required": ["amount", "currency"] + } + }, + "required": ["baseCost", "premium", "total"] + }, + { + "type": "object", + "properties": { + "baseCost": { "type": "null" }, + "premium": { "type": "null" }, + "total": { "type": "null" } + }, + "required": ["baseCost", "premium", "total"] + } + ] + }, + "referral": { + "anyOf": [ + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "string" }, + "decodedReferrer": { "type": "string" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + }, + { + "type": "object", + "properties": { + "encodedReferrer": { "type": "null" }, + "decodedReferrer": { "type": "null" } + }, + "required": ["encodedReferrer", "decodedReferrer"] + } + ] + }, + "block": { + "type": "object", + "properties": { + "timestamp": { "type": "integer" }, + "number": { "type": "integer", "minimum": 0 } + }, + "required": ["timestamp", "number"], + "additionalProperties": false + }, + "transactionHash": { "type": "string" }, + "eventIds": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "minItems": 1 + }, + "type": { "type": "string", "enum": ["renewal"] } + }, + "required": [ + "id", + "incrementalDuration", + "registrant", + "registrationLifecycle", + "pricing", + "referral", + "block", + "transactionHash", + "eventIds", + "type" + ] + } + ] + }, + "name": { "type": "string" } + }, + "required": ["action", "name"] + } + }, + "pageContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "totalRecords": { "type": "number", "enum": [0] }, + "totalPages": { "type": "number", "enum": [1] }, + "hasNext": { "type": "boolean", "enum": [false] }, + "hasPrev": { "type": "boolean", "enum": [false] }, + "page": { "type": "integer", "exclusiveMinimum": 0 }, + "recordsPerPage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100 + } + }, + "required": [ + "totalRecords", + "totalPages", + "hasNext", + "hasPrev", + "page", + "recordsPerPage" + ] + }, + { + "type": "object", + "properties": { + "totalRecords": { "type": "integer", "exclusiveMinimum": 0 }, + "totalPages": { "type": "integer", "exclusiveMinimum": 0 }, + "hasNext": { "type": "boolean" }, + "hasPrev": { "type": "boolean" }, + "startIndex": { "type": "integer", "minimum": 0 }, + "endIndex": { "type": "integer", "minimum": 0 }, + "page": { "type": "integer", "exclusiveMinimum": 0 }, + "recordsPerPage": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100 + } + }, + "required": [ + "totalRecords", + "totalPages", + "hasNext", + "hasPrev", + "startIndex", + "endIndex", + "page", + "recordsPerPage" + ] + } + ] + }, + "accurateAsOf": { "type": "integer" } + }, + "required": ["responseCode", "registrarActions", "pageContext"], + "example": { + "responseCode": "ok", + "registrarActions": [ + { + "action": { + "type": "registration", + "id": "0x0000000000000000000000000000000000000000000000000000000000000001", + "incrementalDuration": 31536000, + "registrant": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + "registrationLifecycle": { + "subregistry": { + "subregistryId": { + "chainId": 1, + "address": "0x253553366da8546fc250f225fe3d25d0c782303b" + }, + "node": "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae" + }, + "node": "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + "expiresAt": 1893456000 + }, + "pricing": { + "baseCost": { "amount": "1000000000000000", "currency": "ETH" }, + "premium": { "amount": "0", "currency": "ETH" }, + "total": { "amount": "1000000000000000", "currency": "ETH" } + }, + "referral": { "encodedReferrer": null, "decodedReferrer": null }, + "block": { "timestamp": 1700000000, "number": 18500000 }, + "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "eventIds": [ + "0x0000000000000000000000000000000000000000000000000000000000000001" + ] + }, + "name": "vitalik.eth" + } + ], + "pageContext": { + "page": 1, + "recordsPerPage": 25, + "totalRecords": 1, + "totalPages": 1, + "hasNext": false, + "hasPrev": false, + "startIndex": 0, + "endIndex": 0 + }, + "accurateAsOf": 1700000000 + } + } + } + } + }, + "400": { + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "message": { "type": "string" }, "details": {} }, + "required": ["message"] + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "responseCode": { "type": "string", "enum": ["error"] }, + "error": { + "type": "object", + "properties": { "message": { "type": "string" }, "details": {} }, + "required": ["message"] + } + }, + "required": ["responseCode", "error"], + "additionalProperties": false + } + } + } + } } } }, diff --git a/package.json b/package.json index 5616dbff7a..1da9c17524 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "yauzl@<3.2.1": "^3.2.1", "fast-xml-parser@>=5.0.0 <5.5.7": ">=5.5.7", "kysely@>=0.26.0 <0.28.14": ">=0.28.14", + "defu@<=6.1.4": ">=6.1.5", "h3@<1.15.9": ">=1.15.9", "yaml@>=2.0.0 <2.8.3": ">=2.8.3", "picomatch@<2.3.2": ">=2.3.2", diff --git a/packages/ensdb-sdk/src/client/zod-schemas/ensdb-config.ts b/packages/ensdb-sdk/src/client/zod-schemas/ensdb-config.ts index c88114b7d7..30afe65792 100644 --- a/packages/ensdb-sdk/src/client/zod-schemas/ensdb-config.ts +++ b/packages/ensdb-sdk/src/client/zod-schemas/ensdb-config.ts @@ -1,31 +1,35 @@ import { parse as parseConnectionString } from "pg-connection-string"; import { z } from "zod/v4"; -export const EnsDbUrlSchema = z.string().refine( - (url) => { - try { - if (!url.startsWith("postgresql://") && !url.startsWith("postgres://")) { +export const EnsDbUrlSchema = z + .string({ + error: "ENV variable ENSDB_URL is required.", + }) + .refine( + (url) => { + try { + if (!url.startsWith("postgresql://") && !url.startsWith("postgres://")) { + return false; + } + const config = parseConnectionString(url); + return !!(config.host && config.port && config.database); + } catch { return false; } - const config = parseConnectionString(url); - return !!(config.host && config.port && config.database); - } catch { - return false; - } - }, - { - error: - "Invalid PostgreSQL connection string for ENSDb. Expected format: postgresql://username:password@host:port/database", - }, -); + }, + { + error: + "Invalid connection string in ENV variable ENSDB_URL. Expected format: postgresql://username:password@host:port/database", + }, + ); const EnsIndexerSchemaNameSchema = z .string({ - error: "ENSIndexer Schema Name is required.", + error: "ENV variable ENSINDEXER_SCHEMA_NAME is required.", }) .trim() .nonempty({ - error: "ENSIndexer Schema Name cannot be an empty string.", + error: "ENV variable ENSINDEXER_SCHEMA_NAME cannot be an empty string.", }); export const EnsDbConfigSchema = z.object({ diff --git a/packages/ensnode-sdk/src/ensapi/api/name-tokens/examples.ts b/packages/ensnode-sdk/src/ensapi/api/name-tokens/examples.ts new file mode 100644 index 0000000000..b703ca3bcf --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/api/name-tokens/examples.ts @@ -0,0 +1,39 @@ +import type { InterpretedName } from "enssdk"; + +import type { SerializedNameTokensResponseOk } from "./serialized-response"; + +/** + * Example value for {@link SerializedNameTokensResponseOk}, for use in OpenAPI documentation. + * + * - domainId and tokenId correspond to "vitalik.eth" + * - contract is the ENS BaseRegistrar (ERC-721) on Ethereum mainnet + */ +export const nameTokensResponseOkExample = { + responseCode: "ok", + registeredNameTokens: { + domainId: "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + name: "vitalik.eth" as InterpretedName, + tokens: [ + { + token: { + assetNamespace: "erc721", + contract: { + chainId: 1, + address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", + }, + tokenId: "0xaf2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc", + }, + ownership: { + ownershipType: "fully-onchain", + owner: { + chainId: 1, + address: "0x220866b1a2219f40e72f5c628b65d54268ca3a9d", + }, + }, + mintStatus: "minted", + }, + ], + expiresAt: 2461152330, + accurateAsOf: 1700000000, + }, +} satisfies SerializedNameTokensResponseOk; diff --git a/packages/ensnode-sdk/src/ensapi/api/name-tokens/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensapi/api/name-tokens/zod-schemas.test.ts index 7cec63add7..706f0068cd 100644 --- a/packages/ensnode-sdk/src/ensapi/api/name-tokens/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/ensapi/api/name-tokens/zod-schemas.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { NFTMintStatuses } from "../../../tokenscope/assets"; import { NameTokenOwnershipTypes } from "../../../tokenscope/name-token"; +import { nameTokensResponseOkExample } from "./examples"; import { NameTokensResponseCodes, type NameTokensResponseOk } from "./response"; import type { SerializedNameTokensResponseOk } from "./serialized-response"; import { makeNameTokensResponseSchema } from "./zod-schemas"; @@ -56,6 +57,14 @@ const responseOk = { } satisfies SerializedNameTokensResponseOk; describe("Name Tokens: Zod Schemas", () => { + it("nameTokensResponseOkExample passes schema", () => { + expect( + makeNameTokensResponseSchema("Name Tokens Response", true).safeParse( + nameTokensResponseOkExample, + ).success, + ).toBe(true); + }); + it("can parse response OK correctly", () => { const schema = makeNameTokensResponseSchema(); const parsed = schema.safeParse(responseOk); diff --git a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/deserialize.ts b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/deserialize.ts index 5cf7e5b4c8..df5a6ec6aa 100644 --- a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/deserialize.ts +++ b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/deserialize.ts @@ -2,7 +2,10 @@ import { prettifyError } from "zod/v4"; import type { RegistrarActionsResponse } from "./response"; import type { SerializedRegistrarActionsResponse } from "./serialized-response"; -import { makeRegistrarActionsResponseSchema } from "./zod-schemas"; +import { + makeRegistrarActionsResponseSchema, + makeSerializedRegistrarActionsResponseSchema, +} from "./zod-schemas"; /** * Deserialize a {@link RegistrarActionsResponse} object. @@ -10,7 +13,15 @@ import { makeRegistrarActionsResponseSchema } from "./zod-schemas"; export function deserializeRegistrarActionsResponse( maybeResponse: SerializedRegistrarActionsResponse, ): RegistrarActionsResponse { - const parsed = makeRegistrarActionsResponseSchema().safeParse(maybeResponse); + const serialized = makeSerializedRegistrarActionsResponseSchema().safeParse(maybeResponse); + + if (serialized.error) { + throw new Error( + `Cannot deserialize RegistrarActionsResponse:\n${prettifyError(serialized.error)}\n`, + ); + } + + const parsed = makeRegistrarActionsResponseSchema().safeParse(serialized.data); if (parsed.error) { throw new Error( diff --git a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/examples.ts b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/examples.ts new file mode 100644 index 0000000000..188a517a16 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/examples.ts @@ -0,0 +1,56 @@ +import type { InterpretedName } from "enssdk"; + +import type { SerializedRegistrarActionsResponseOk } from "./serialized-response"; + +/** + * Example value for {@link SerializedRegistrarActionsResponseOk}, for use in OpenAPI documentation. + * + * - registrationLifecycle.node is namehash("vitalik.eth") + * - subregistry.node is namehash("eth") + * - subregistry.subregistryId is the ETH Registrar Controller on Ethereum mainnet + */ +export const registrarActionsResponseOkExample = { + responseCode: "ok", + registrarActions: [ + { + action: { + type: "registration", + id: "0x0000000000000000000000000000000000000000000000000000000000000001", + incrementalDuration: 31536000, + registrant: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", + registrationLifecycle: { + subregistry: { + subregistryId: { + chainId: 1, + address: "0x253553366da8546fc250f225fe3d25d0c782303b", + }, + node: "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", + }, + node: "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835", + expiresAt: 1893456000, + }, + pricing: { + baseCost: { amount: "1000000000000000", currency: "ETH" }, + premium: { amount: "0", currency: "ETH" }, + total: { amount: "1000000000000000", currency: "ETH" }, + }, + referral: { encodedReferrer: null, decodedReferrer: null }, + block: { timestamp: 1700000000, number: 18500000 }, + transactionHash: "0x0000000000000000000000000000000000000000000000000000000000000001", + eventIds: ["0x0000000000000000000000000000000000000000000000000000000000000001"], + }, + name: "vitalik.eth" as InterpretedName, + }, + ], + pageContext: { + page: 1, + recordsPerPage: 25, + totalRecords: 1, + totalPages: 1, + hasNext: false, + hasPrev: false, + startIndex: 0, + endIndex: 0, + }, + accurateAsOf: 1700000000, +} satisfies SerializedRegistrarActionsResponseOk; diff --git a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/serialize.ts b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/serialize.ts index 6b8287957a..11d8815f1d 100644 --- a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/serialize.ts +++ b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/serialize.ts @@ -3,10 +3,13 @@ import { type NamedRegistrarAction, type RegistrarActionsResponse, RegistrarActionsResponseCodes, + type RegistrarActionsResponseError, + type RegistrarActionsResponseOk, } from "./response"; import type { SerializedNamedRegistrarAction, SerializedRegistrarActionsResponse, + SerializedRegistrarActionsResponseError, SerializedRegistrarActionsResponseOk, } from "./serialized-response"; @@ -20,6 +23,12 @@ export function serializeNamedRegistrarAction({ }; } +export function serializeRegistrarActionsResponse( + response: RegistrarActionsResponseOk, +): SerializedRegistrarActionsResponseOk; +export function serializeRegistrarActionsResponse( + response: RegistrarActionsResponseError, +): SerializedRegistrarActionsResponseError; export function serializeRegistrarActionsResponse( response: RegistrarActionsResponse, ): SerializedRegistrarActionsResponse { diff --git a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.test.ts index b92a67b5e9..6033c745d9 100644 --- a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.test.ts @@ -1,126 +1,32 @@ -import type { InterpretedName } from "enssdk"; import { describe, expect, it } from "vitest"; +import { registrarActionsResponseOkExample } from "./examples"; import { RegistrarActionsResponseCodes, type RegistrarActionsResponseError } from "./response"; -import type { - SerializedNamedRegistrarAction, - SerializedRegistrarActionsResponseError, - SerializedRegistrarActionsResponseOk, -} from "./serialized-response"; -import { makeRegistrarActionsResponseSchema } from "./zod-schemas"; +import type { SerializedRegistrarActionsResponseError } from "./serialized-response"; +import { + makeRegistrarActionsResponseSchema, + makeSerializedRegistrarActionsResponseSchema, +} from "./zod-schemas"; describe("ENSNode API Schema", () => { describe("Registrar Actions API", () => { - const validNamedRegistrarActionNormalizedWithReferral = { - action: { - id: "176209761600000000111551110000000009545322000000000000006750000000000000067", - type: "registration", - incrementalDuration: 2419200, - registrant: "0x877dd7fa7a6813361de23552c12d25af4a89cda7", - registrationLifecycle: { - subregistry: { - subregistryId: { - chainId: 11155111, - address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", - }, - node: "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", - }, - node: "0x5bcdea30f2d591f5357045b89d3470d4ba4da00fd344a32fe323ab6fa2c0f343", - expiresAt: 1764516816, - }, - pricing: { - baseCost: { - currency: "ETH", - amount: "7671232876711824", - }, - premium: { - currency: "ETH", - amount: "0", - }, - total: { - currency: "ETH", - amount: "7671232876711824", - }, - }, - referral: { - encodedReferrer: "0x0000000000000000000000007bddd635be34bcf860d5f02ae53b16fcd17e8f6f", - decodedReferrer: "0x7bddd635be34bcf860d5f02ae53b16fcd17e8f6f", - }, - block: { - number: 9545322, - timestamp: 1762097616, - }, - transactionHash: "0x8b3316e97a92ea0f676943a206ef1722b90b279c0a769456a89b2afe37f205fa", - eventIds: [ - "176209761600000000111551110000000009545322000000000000006750000000000000067", - "176209761600000000111551110000000009545322000000000000006750000000000000071", - ], - }, - name: "nh35.eth" as InterpretedName, - } satisfies SerializedNamedRegistrarAction; - - const validNamedRegistrarActionEncodedLabelHash = { - action: { - id: "176234701200000000111551110000000009566045000000000000014150000000000000198", - type: "registration", - incrementalDuration: 31536000, - registrant: "0x5505957ff5927f29eacabbbe8a304968bf2dc064", - registrationLifecycle: { - subregistry: { - subregistryId: { - chainId: 11155111, - address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", - }, - node: "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae", - }, - node: "0xf1c0e6aa95596e0199f3a6341cdbe055b64ba6041662465e577ed80c4dfac2af", - expiresAt: 1793883012, - }, - pricing: { - baseCost: null, - premium: null, - total: null, - }, - referral: { - encodedReferrer: null, - decodedReferrer: null, - }, - block: { - number: 9566045, - timestamp: 1762347012, - }, - transactionHash: "0xa71cf08102ae1f634b22349dac8dc158fe96ae74008b5e24cfcda8587e056d53", - eventIds: ["176234701200000000111551110000000009566045000000000000014150000000000000198"], - }, - name: "[e4310bf4547cb18b16b5348881d24a66d61fa94a013e5636b730b86ee64a3923].eth" as InterpretedName, - } satisfies SerializedNamedRegistrarAction; - - const validResponseOk = { - responseCode: RegistrarActionsResponseCodes.Ok, - registrarActions: [ - validNamedRegistrarActionEncodedLabelHash, - validNamedRegistrarActionNormalizedWithReferral, - ], - pageContext: { - page: 1, - recordsPerPage: 10, - totalRecords: 2, - totalPages: 1, - hasNext: false, - hasPrev: false, - startIndex: 0, - endIndex: 1, - }, - accurateAsOf: 1767718566, - } satisfies SerializedRegistrarActionsResponseOk; - const validResponseError = { responseCode: RegistrarActionsResponseCodes.Error, error: { message: "any message", details: "any details" }, } satisfies SerializedRegistrarActionsResponseError; - it("can parse valid ResponseOk object", () => { - expect(() => makeRegistrarActionsResponseSchema().parse(validResponseOk)).not.toThrowError(); + it("registrarActionsResponseOkExample passes schema", () => { + expect( + makeSerializedRegistrarActionsResponseSchema().safeParse(registrarActionsResponseOkExample) + .success, + ).toBe(true); + }); + + it("can deserialize ResponseOk object via domain schema", () => { + const serialized = makeSerializedRegistrarActionsResponseSchema().parse( + registrarActionsResponseOkExample, + ); + expect(() => makeRegistrarActionsResponseSchema().parse(serialized)).not.toThrowError(); }); it("can parse valid ResponseError object", () => { diff --git a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.ts index 49ca24011c..5365af00c9 100644 --- a/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensapi/api/registrar-actions/zod-schemas.ts @@ -2,7 +2,10 @@ import { namehash } from "viem/ens"; import { z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import { makeRegistrarActionSchema } from "../../../registrars/zod-schemas"; +import { + makeRegistrarActionSchema, + makeSerializedRegistrarActionSchema, +} from "../../../registrars/zod-schemas"; import { makeReinterpretedNameSchema, makeUnixTimestampSchema } from "../../../shared/zod-schemas"; import { ErrorResponseSchema } from "../shared/errors/zod-schemas"; import { makeResponsePageContextSchema } from "../shared/pagination/zod-schemas"; @@ -33,6 +36,17 @@ export const makeNamedRegistrarActionSchema = (valueLabel: string = "Named Regis }) .check(invariant_registrationLifecycleNodeMatchesName); +/** + * Schema for serialized {@link NamedRegistrarAction}. + */ +export const makeSerializedNamedRegistrarActionSchema = ( + valueLabel: string = "Serialized Named Registrar Action", +) => + z.object({ + action: makeSerializedRegistrarActionSchema(valueLabel), + name: makeReinterpretedNameSchema(valueLabel), + }); + /** * Schema for {@link RegistrarActionsResponseOk} */ @@ -67,3 +81,27 @@ export const makeRegistrarActionsResponseSchema = ( makeRegistrarActionsResponseOkSchema(valueLabel), makeRegistrarActionsResponseErrorSchema(valueLabel), ]); + +/** + * Schema for serialized {@link RegistrarActionsResponseOk} + */ +export const makeSerializedRegistrarActionsResponseOkSchema = ( + valueLabel: string = "Serialized Registrar Actions Response OK", +) => + z.object({ + responseCode: z.literal(RegistrarActionsResponseCodes.Ok), + registrarActions: z.array(makeSerializedNamedRegistrarActionSchema(valueLabel)), + pageContext: makeResponsePageContextSchema(`${valueLabel}.pageContext`), + accurateAsOf: makeUnixTimestampSchema(`${valueLabel}.accurateAsOf`).optional(), + }); + +/** + * Schema for serialized {@link RegistrarActionsResponse} + */ +export const makeSerializedRegistrarActionsResponseSchema = ( + valueLabel: string = "Serialized Registrar Actions Response", +) => + z.discriminatedUnion("responseCode", [ + makeSerializedRegistrarActionsResponseOkSchema(valueLabel), + makeRegistrarActionsResponseErrorSchema(valueLabel), + ]); diff --git a/packages/ensnode-sdk/src/ensapi/api/resolution/examples.ts b/packages/ensnode-sdk/src/ensapi/api/resolution/examples.ts new file mode 100644 index 0000000000..f65cf9c825 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/api/resolution/examples.ts @@ -0,0 +1,39 @@ +/** + * Example values for {@link ResolveRecordsResponse}, for use in OpenAPI documentation. + */ +export const resolveRecordsResponseExample = { + records: { + name: "vitalik.eth", + addresses: { "60": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" }, + texts: { + description: "mi pinxe lo crino tcati", + }, + }, + accelerationRequested: false, + accelerationAttempted: false, +}; + +/** + * Example values for {@link ResolvePrimaryNameResponse}, for use in OpenAPI documentation. + */ +export const resolvePrimaryNameResponseExample = { + name: "jesse.base.eth", + accelerationRequested: false, + accelerationAttempted: false, +}; + +/** + * Example values for {@link ResolvePrimaryNamesResponse}, for use in OpenAPI documentation. + */ +export const resolvePrimaryNamesResponseExample = { + names: { + "1": "jesse.base.eth", + "10": null, + "8453": "jesse.base.eth", + "42161": null, + "59144": null, + "534352": null, + }, + accelerationRequested: false, + accelerationAttempted: false, +}; diff --git a/packages/ensnode-sdk/src/ensapi/api/resolution/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensapi/api/resolution/zod-schemas.test.ts new file mode 100644 index 0000000000..fdc3f99950 --- /dev/null +++ b/packages/ensnode-sdk/src/ensapi/api/resolution/zod-schemas.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { + resolvePrimaryNameResponseExample, + resolvePrimaryNamesResponseExample, + resolveRecordsResponseExample, +} from "./examples"; +import { + makeResolvePrimaryNameResponseSchema, + makeResolvePrimaryNamesResponseSchema, + makeResolveRecordsResponseSchema, +} from "./zod-schemas"; + +describe("Resolution: Zod Schemas", () => { + it("resolveRecordsResponseExample passes schema", () => { + expect( + makeResolveRecordsResponseSchema().safeParse(resolveRecordsResponseExample).success, + ).toBe(true); + }); + + it("resolvePrimaryNameResponseExample passes schema", () => { + expect( + makeResolvePrimaryNameResponseSchema().safeParse(resolvePrimaryNameResponseExample).success, + ).toBe(true); + }); + + it("resolvePrimaryNamesResponseExample passes schema", () => { + expect( + makeResolvePrimaryNamesResponseSchema().safeParse(resolvePrimaryNamesResponseExample).success, + ).toBe(true); + }); +}); diff --git a/packages/ensnode-sdk/src/ensapi/api/shared/pagination/response.ts b/packages/ensnode-sdk/src/ensapi/api/shared/pagination/response.ts index 5f48dc4e24..56ca305223 100644 --- a/packages/ensnode-sdk/src/ensapi/api/shared/pagination/response.ts +++ b/packages/ensnode-sdk/src/ensapi/api/shared/pagination/response.ts @@ -24,12 +24,12 @@ export interface ResponsePageContextWithNoRecords extends Required { diff --git a/packages/ensnode-sdk/src/ensapi/api/shared/pagination/zod-schemas.ts b/packages/ensnode-sdk/src/ensapi/api/shared/pagination/zod-schemas.ts index 9e314e721b..fc90f0f2ab 100644 --- a/packages/ensnode-sdk/src/ensapi/api/shared/pagination/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensapi/api/shared/pagination/zod-schemas.ts @@ -36,8 +36,6 @@ export const makeResponsePageContextSchemaWithNoRecords = ( totalPages: z.literal(1), hasNext: z.literal(false), hasPrev: z.literal(false), - startIndex: z.undefined(), - endIndex: z.undefined(), }) .extend(makeRequestPageParamsSchema(valueLabel).shape); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 8029f1db3d..0880b7ae0f 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -13,8 +13,11 @@ */ export * from "./ensapi/api/indexing-status/zod-schemas"; +export * from "./ensapi/api/name-tokens/examples"; export * from "./ensapi/api/name-tokens/zod-schemas"; +export * from "./ensapi/api/registrar-actions/examples"; export * from "./ensapi/api/registrar-actions/zod-schemas"; +export * from "./ensapi/api/resolution/examples"; export * from "./ensapi/api/resolution/zod-schemas"; export * from "./ensapi/api/shared/errors/zod-schemas"; export * from "./ensapi/api/shared/pagination/zod-schemas"; diff --git a/packages/ensnode-sdk/src/registrars/zod-schemas.ts b/packages/ensnode-sdk/src/registrars/zod-schemas.ts index 8bef60a73c..3aa5764a22 100644 --- a/packages/ensnode-sdk/src/registrars/zod-schemas.ts +++ b/packages/ensnode-sdk/src/registrars/zod-schemas.ts @@ -87,6 +87,35 @@ const makeRegistrarActionPricingSchema = (valueLabel: string = "Registrar Action .transform((v) => v as RegistrarActionPricingUnknown), ]); +const makeSerializedPriceEthSchema = (valueLabel: string = "Serialized Price ETH") => + z.object({ + amount: z.string({ error: `${valueLabel} amount must be a string.` }).regex(/^\d+$/, { + error: `${valueLabel} amount must be a non-negative integer string.`, + }), + currency: z.literal("ETH", { + error: `${valueLabel} currency must be set to 'ETH'.`, + }), + }); + +/** + * Schema for parsing objects into serialized {@link RegistrarActionPricing}. + */ +const makeSerializedRegistrarActionPricingSchema = ( + valueLabel: string = "Serialized Registrar Action Pricing", +) => + z.union([ + z.object({ + baseCost: makeSerializedPriceEthSchema(`${valueLabel} Base Cost`), + premium: makeSerializedPriceEthSchema(`${valueLabel} Premium`), + total: makeSerializedPriceEthSchema(`${valueLabel} Total`), + }), + z.object({ + baseCost: z.null(), + premium: z.null(), + total: z.null(), + }), + ]); + /** Invariant: decodedReferrer is based on encodedReferrer */ function invariant_registrarActionDecodedReferrerBasedOnRawReferrer( ctx: ParsePayload, @@ -194,3 +223,45 @@ export const makeRegistrarActionSchema = (valueLabel: string = "Registrar Action makeRegistrarActionRegistrationSchema(`${valueLabel} Registration`), makeRegistrarActionRenewalSchema(`${valueLabel} Renewal`), ]); + +const makeBaseSerializedRegistrarActionSchema = ( + valueLabel: string = "Serialized Base Registrar Action", +) => + z + .object({ + id: EventIdSchema, + incrementalDuration: makeDurationSchema(`${valueLabel} Incremental Duration`), + registrant: makeLowercaseAddressSchema(`${valueLabel} Registrant`), + registrationLifecycle: makeRegistrationLifecycleSchema( + `${valueLabel} Registration Lifecycle`, + ), + pricing: makeSerializedRegistrarActionPricingSchema(`${valueLabel} Pricing`), + referral: makeRegistrarActionReferralSchema(`${valueLabel} Referral`), + block: makeBlockRefSchema(`${valueLabel} Block`), + transactionHash: makeTransactionHashSchema(`${valueLabel} Transaction Hash`), + eventIds: EventIdsSchema, + }) + .check(invariant_eventIdsInitialElementIsTheActionId); + +const makeSerializedRegistrarActionRegistrationSchema = ( + valueLabel: string = "Serialized Registration", +) => + makeBaseSerializedRegistrarActionSchema(valueLabel).extend({ + type: z.literal(RegistrarActionTypes.Registration), + }); + +const makeSerializedRegistrarActionRenewalSchema = (valueLabel: string = "Serialized Renewal") => + makeBaseSerializedRegistrarActionSchema(valueLabel).extend({ + type: z.literal(RegistrarActionTypes.Renewal), + }); + +/** + * Schema for {@link SerializedRegistrarAction}. + */ +export const makeSerializedRegistrarActionSchema = ( + valueLabel: string = "Serialized Registrar Action", +) => + z.discriminatedUnion("type", [ + makeSerializedRegistrarActionRegistrationSchema(`${valueLabel} Registration`), + makeSerializedRegistrarActionRenewalSchema(`${valueLabel} Renewal`), + ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ccd7328b8..f23e7c30a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,7 @@ overrides: yauzl@<3.2.1: ^3.2.1 fast-xml-parser@>=5.0.0 <5.5.7: '>=5.5.7' kysely@>=0.26.0 <0.28.14: '>=0.28.14' + defu@<=6.1.4: '>=6.1.5' h3@<1.15.9: '>=1.15.9' yaml@>=2.0.0 <2.8.3: '>=2.8.3' picomatch@<2.3.2: '>=2.3.2' @@ -5642,8 +5643,8 @@ packages: resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} engines: {node: '>=16.0.0'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.6: + resolution: {integrity: sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug==} delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} @@ -13391,14 +13392,6 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.0.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3) - '@vitest/mocker@4.0.5(vite@7.1.12(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.0.5 @@ -14575,7 +14568,7 @@ snapshots: deepmerge-ts@7.1.5: {} - defu@6.1.4: {} + defu@6.1.6: {} delaunator@5.0.1: dependencies: @@ -15288,7 +15281,7 @@ snapshots: dependencies: cookie-es: 1.2.2 crossws: 0.3.5 - defu: 6.1.4 + defu: 6.1.6 destr: 2.0.5 iron-webcrypto: 1.2.1 node-mock-http: 1.0.4 @@ -18567,7 +18560,7 @@ snapshots: vitest@4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.5 - '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.3)) + '@vitest/mocker': 4.0.5(vite@7.1.12(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.0.5 '@vitest/runner': 4.0.5 '@vitest/snapshot': 4.0.5