diff --git a/packages/api/src/beacon/server/beacon.ts b/packages/api/src/beacon/server/beacon.ts index b5862f766b56..a4d94b711785 100644 --- a/packages/api/src/beacon/server/beacon.ts +++ b/packages/api/src/beacon/server/beacon.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/beacon/server/config.ts b/packages/api/src/beacon/server/config.ts index 26d78ab9a5f1..39c87199465f 100644 --- a/packages/api/src/beacon/server/config.ts +++ b/packages/api/src/beacon/server/config.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/beacon/server/debug.ts b/packages/api/src/beacon/server/debug.ts index 7dd767d4a2fd..bc5c1b9829db 100644 --- a/packages/api/src/beacon/server/debug.ts +++ b/packages/api/src/beacon/server/debug.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { const reqSerializers = getReqSerializers(); const returnTypes = getReturnTypes(); @@ -21,11 +21,12 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR ...serverRoutes.getState, handler: async (req) => { const response = await api.getState(...reqSerializers.getState.parseReq(req)); - if (response instanceof Uint8Array) { + const res = "status" in response ? response.response : response; + if (res instanceof Uint8Array) { // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer - return Buffer.from(response); + return Buffer.from(res); } else { - return returnTypes.getState.toJson(response); + return returnTypes.getState.toJson(res); } }, }, @@ -33,11 +34,12 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR ...serverRoutes.getStateV2, handler: async (req) => { const response = await api.getStateV2(...reqSerializers.getStateV2.parseReq(req)); - if (response instanceof Uint8Array) { + const res = "status" in response ? response.response : response; + if (res instanceof Uint8Array) { // Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer - return Buffer.from(response); + return Buffer.from(res); } else { - return returnTypes.getStateV2.toJson(response); + return returnTypes.getStateV2.toJson(res); } }, }, diff --git a/packages/api/src/beacon/server/events.ts b/packages/api/src/beacon/server/events.ts index fa8b857f8779..fb700c51dde9 100644 --- a/packages/api/src/beacon/server/events.ts +++ b/packages/api/src/beacon/server/events.ts @@ -4,7 +4,7 @@ import {Api, ReqTypes, routesData, getEventSerdes} from "../routes/events.js"; import {ServerRoutes} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { const eventSerdes = getEventSerdes(config); return { diff --git a/packages/api/src/beacon/server/lightclient.ts b/packages/api/src/beacon/server/lightclient.ts index d587ec29823d..fdb2d5c5d0f2 100644 --- a/packages/api/src/beacon/server/lightclient.ts +++ b/packages/api/src/beacon/server/lightclient.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/beacon/server/lodestar.ts b/packages/api/src/beacon/server/lodestar.ts index b52ed9832f69..5c61b93e0798 100644 --- a/packages/api/src/beacon/server/lodestar.ts +++ b/packages/api/src/beacon/server/lodestar.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/beacon/server/proof.ts b/packages/api/src/beacon/server/proof.ts index a03ae472bf78..325da95afa5d 100644 --- a/packages/api/src/beacon/server/proof.ts +++ b/packages/api/src/beacon/server/proof.ts @@ -18,7 +18,8 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR ...serverRoutes.getStateProof, handler: async (req) => { const args = reqSerializers.getStateProof.parseReq(req); - const {data} = await api.getStateProof(...args); + const apiRes = await api.getStateProof(...args); + const data = "status" in apiRes ? apiRes.response?.data : apiRes.data; const leaves = (data as CompactMultiProof).leaves; const response = new Uint8Array(32 * leaves.length); for (let i = 0; i < leaves.length; i++) { @@ -32,7 +33,8 @@ export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerR ...serverRoutes.getBlockProof, handler: async (req) => { const args = reqSerializers.getBlockProof.parseReq(req); - const {data} = await api.getBlockProof(...args); + const apiRes = await api.getBlockProof(...args); + const data = "status" in apiRes ? apiRes.response?.data : apiRes.data; const leaves = (data as CompactMultiProof).leaves; const response = new Uint8Array(32 * leaves.length); for (let i = 0; i < leaves.length; i++) { diff --git a/packages/api/src/beacon/server/validator.ts b/packages/api/src/beacon/server/validator.ts index 6bf446e05a16..f128557d2820 100644 --- a/packages/api/src/beacon/server/validator.ts +++ b/packages/api/src/beacon/server/validator.ts @@ -3,7 +3,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js"; import {ServerApi} from "../../interfaces.js"; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/builder/server/index.ts b/packages/api/src/builder/server/index.ts index 2421abbc0dcc..902004b9daef 100644 --- a/packages/api/src/builder/server/index.ts +++ b/packages/api/src/builder/server/index.ts @@ -12,7 +12,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r // Re-export for convenience export {RouteConfig}; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/interfaces.ts b/packages/api/src/interfaces.ts index a5f245de9131..9a441148fe03 100644 --- a/packages/api/src/interfaces.ts +++ b/packages/api/src/interfaces.ts @@ -4,8 +4,7 @@ import {Resolves} from "./utils/types.js"; /* eslint-disable @typescript-eslint/no-explicit-any */ export type APIClientHandler = (...args: any) => PromiseLike; -export type APIServerHandler = (...args: any) => PromiseLike; - +export type APIServerHandler = (...args: any) => PromiseLike>; export type ApiClientSuccessResponse = {ok: true; status: S; response: T; error?: never}; export type ApiClientErrorResponse> = { ok: false; @@ -21,17 +20,36 @@ export type ApiClientResponse< | {[K in E]: ApiClientErrorResponse}[E] | ApiClientErrorResponse; -export type ApiClientResponseData = T extends {ok: true; response: infer R} ? R : never; +export type ApiServerResponse = T extends { + ok: true; + response: infer R; + status: infer S; +} + ? R | {status: S; response: R} + : never; + +export type ApiClientResolvesData = T extends {ok: true; response: infer R} ? R : never; +export type ApiServerResolvesData> = T extends void + ? never + : T extends { + status: number; + response: infer R; + } + ? R + : never; export type GenericRequestObject = Record; -export type GenericResponseObject = {code: (code: number) => void}; export type ServerApi> = { [K in keyof T]: ( - ...args: [...args: Parameters, req?: GenericRequestObject, res?: GenericResponseObject] - ) => Promise>>; + ...args: [...args: Parameters, req?: GenericRequestObject] + ) => Promise>>; }; export type ClientApi> = { - [K in keyof T]: (...args: Parameters) => Promise}>>; + [K in keyof T]: ApiServerResolvesData> extends void + ? never + : (...args: Parameters) => Promise}>>; }; + +type T = ApiServerResponse>; diff --git a/packages/api/src/keymanager/server/index.ts b/packages/api/src/keymanager/server/index.ts index 2421abbc0dcc..902004b9daef 100644 --- a/packages/api/src/keymanager/server/index.ts +++ b/packages/api/src/keymanager/server/index.ts @@ -12,7 +12,7 @@ import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../r // Re-export for convenience export {RouteConfig}; -export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes { +export function getRoutes(config: ChainForkConfig, api: ServerApi): ServerRoutes, ReqTypes> { // All routes return JSON, use a server auto-generator return getGenericJsonServer, ReqTypes>({routesData, getReturnTypes, getReqSerializers}, config, api); } diff --git a/packages/api/src/utils/server/genericJsonServer.ts b/packages/api/src/utils/server/genericJsonServer.ts index a27f1ea69723..e7753a25fedf 100644 --- a/packages/api/src/utils/server/genericJsonServer.ts +++ b/packages/api/src/utils/server/genericJsonServer.ts @@ -1,10 +1,11 @@ import type {FastifyInstance} from "fastify"; import {mapValues} from "@lodestar/utils"; import {ChainForkConfig} from "@lodestar/config"; -import {ReqGeneric, TypeJson, Resolves, RouteGroupDefinition} from "../types.js"; +import {ReqGeneric, TypeJson, RouteGroupDefinition} from "../types.js"; import {getFastifySchema} from "../schema.js"; import {toColonNotationPath} from "../urlFormat.js"; import {APIServerHandler} from "../../interfaces.js"; +import {HttpStatusCode} from "../client/httpStatusCode.js"; import {ServerRoute} from "./types.js"; // See /packages/api/src/routes/index.ts for reasoning @@ -42,14 +43,27 @@ export function getGenericJsonServer< handler: async function handler(this: FastifyInstance, req, resp): Promise { const args: any[] = routeSerdes.parseReq(req as ReqGeneric as ReqTypes[keyof Api]); - const data = (await api[routeId](...args, req, resp)) as Resolves; - if (routeDef.statusOk !== undefined) { - resp.statusCode = routeDef.statusOk; - } + // The type resolves here is `unknown | void | {status: number; response: unknown | void}` + // which end up being just `unknown` in the end because of the `unknown | void` part + const data = await api[routeId](...args, req, resp); + + const status = + "status" in (data as {status: number; response: unknown}) + ? (data as {status: number; response: unknown}).status + : routeDef.statusOk !== undefined + ? routeDef.statusOk + : HttpStatusCode.OK; + + const response = + "status" in (data as {status: number; response: unknown}) + ? (data as {status: number; response: unknown}).response + : data; + + resp.statusCode = status; if (returnType) { - return returnType.toJson(data); + return returnType.toJson(response); } else { return {}; } diff --git a/packages/api/src/utils/types.ts b/packages/api/src/utils/types.ts index a62fb30b7270..9da861fab266 100644 --- a/packages/api/src/utils/types.ts +++ b/packages/api/src/utils/types.ts @@ -2,7 +2,8 @@ import {isBasicType, ListBasicType, Type, isCompositeType, ListCompositeType, Ar import {ForkName} from "@lodestar/params"; import {ChainForkConfig} from "@lodestar/config"; import {objectToExpectedCase} from "@lodestar/utils"; -import {APIClientHandler, ApiClientResponseData, APIServerHandler, ClientApi} from "../interfaces.js"; +import {APIClientHandler, ApiClientResolvesData, ApiServerResponse} from "../interfaces.js"; +import {APIServerHandler, ClientApi} from "../interfaces.js"; import {Schema, SchemaDefinition} from "./schema.js"; // See /packages/api/src/routes/index.ts for reasoning @@ -219,11 +220,11 @@ export function sameType(): TypeJson { // RETURN // export type KeysOfNonVoidResolveValues> = { - [K in keyof Api]: ApiClientResponseData> extends void ? never : K; + [K in keyof Api]: ApiClientResolvesData> extends void ? never : K; }[keyof Api]; export type ReturnTypes> = { - [K in keyof Pick>]: TypeJson>>; + [K in keyof Pick>]: TypeJson>>; }; export type RoutesData> = {[K in keyof Api]: RouteDef}; diff --git a/packages/api/test/utils/genericServerTest.ts b/packages/api/test/utils/genericServerTest.ts index 9f6dd45da2c8..7f13d959f1b3 100644 --- a/packages/api/test/utils/genericServerTest.ts +++ b/packages/api/test/utils/genericServerTest.ts @@ -7,23 +7,20 @@ import {FetchOpts, HttpClient, IHttpClient} from "../../src/utils/client/index.j import {ServerRoutes} from "../../src/utils/server/genericJsonServer.js"; import {registerRoute} from "../../src/utils/server/registerRoute.js"; import {HttpStatusCode} from "../../src/utils/client/httpStatusCode.js"; -import {APIClientHandler, ApiClientResponseData, ServerApi} from "../../src/interfaces.js"; +import {APIClientHandler, ApiServerResponse, ServerApi} from "../../src/interfaces.js"; import {getMockApi, getTestServer} from "./utils.js"; type IgnoreVoid = T extends void ? undefined : T; -export type GenericServerTestCases> = { +export type GenericServerTestCases = { [K in keyof Api]: { args: Parameters; - res: IgnoreVoid>>; + res: IgnoreVoid>>; query?: FetchOpts["query"]; }; }; -export function runGenericServerTest< - Api extends Record, - ReqTypes extends {[K in keyof Api]: ReqGeneric}, ->( +export function runGenericServerTest( config: ChainForkConfig, getClient: (config: ChainForkConfig, https: IHttpClient) => Api, getRoutes: (config: ChainForkConfig, api: ServerApi) => ServerRoutes, ReqTypes>, diff --git a/packages/beacon-node/src/api/impl/node/index.ts b/packages/beacon-node/src/api/impl/node/index.ts index 902fa6fd6275..1087594ce94f 100644 --- a/packages/beacon-node/src/api/impl/node/index.ts +++ b/packages/beacon-node/src/api/impl/node/index.ts @@ -66,7 +66,7 @@ export function getNodeApi( return {data: sync.getSyncStatus()}; }, - async getHealth(options, _req, res) { + async getHealth(options) { // Custom value passed via `syncing_status` query parameter const syncingStatus = options?.syncingStatus; @@ -77,10 +77,10 @@ export function getNodeApi( if (sync.getSyncStatus().isSyncing) { // 206: Node is syncing but can serve incomplete data - res?.code(syncingStatus ?? routes.node.NodeHealth.SYNCING); + return {status: syncingStatus ?? routes.node.NodeHealth.SYNCING, response: undefined}; } else { // 200: Node is ready - res?.code(routes.node.NodeHealth.READY); + return {status: routes.node.NodeHealth.READY, response: undefined}; } // else { // 503: Node not initialized or having issues diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index bc0b25a953d6..c7c9fd939c57 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -287,7 +287,9 @@ export function getValidatorApi({ randaoReveal, graffiti ) { - const {data, version, blockValue} = await produceBlockV2(slot, randaoReveal, graffiti); + const produceBlockV2Resp = await produceBlockV2(slot, randaoReveal, graffiti); + const {data, version, blockValue} = + "status" in produceBlockV2Resp ? produceBlockV2Resp.response : produceBlockV2Resp; if ((data as BlockContents).block !== undefined) { throw Error(`Invalid block contents for produceBlock at fork=${version}`); } else {