import {isBasicType, ListBasicType, Type, isCompositeType, ListCompositeType, ArrayType} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; import {IChainForkConfig} from "@lodestar/config"; import {objectToExpectedCase} from "@lodestar/utils"; import {APIClientHandler, ApiClientResponseData, APIServerHandler, ClientApi} from "../interfaces.js"; import {Schema, SchemaDefinition} from "./schema.js"; // See /packages/api/src/routes/index.ts for reasoning /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any */ /** All JSON inside the JS code must be camel case */ const codeCase = "camel" as const; export type RouteGroupDefinition< Api extends Record, ReqTypes extends {[K in keyof Api]: ReqGeneric} > = { routesData: RoutesData; getReqSerializers: (config: IChainForkConfig) => ReqSerializers; getReturnTypes: (config: IChainForkConfig) => ReturnTypes>; }; export type RouteDef = { url: string; method: "GET" | "POST" | "DELETE"; statusOk?: number; }; export type ReqGeneric = { params?: Record; query?: Record; body?: any; headers?: Record; }; export type ReqEmpty = ReqGeneric; export type Resolves any> = Awaited>; export type TypeJson = { toJson(val: T): unknown; fromJson(json: unknown): T; }; // // REQ // export type ReqSerializer any, ReqType extends ReqGeneric> = { writeReq: (...args: Parameters) => ReqType; parseReq: (arg: ReqType) => Parameters; schema?: SchemaDefinition; }; export type ReqSerializers< Api extends Record, ReqTypes extends {[K in keyof Api]: ReqGeneric} > = { [K in keyof Api]: ReqSerializer; }; /** Curried definition to infer only one of the two generic types */ export type ReqGenArg any, ReqType extends ReqGeneric> = ReqSerializer; // // Helpers // /** Shortcut for routes that have no params, query nor body */ export const reqEmpty: ReqSerializer<() => void, ReqEmpty> = { writeReq: () => ({}), parseReq: () => [] as [], }; /** Shortcut for routes that have only body */ export const reqOnlyBody = ( type: TypeJson, bodySchema: Schema ): ReqGenArg<(arg: T) => Promise, {body: unknown}> => ({ writeReq: (items) => ({body: type.toJson(items)}), parseReq: ({body}) => [type.fromJson(body)], schema: {body: bodySchema}, }); /** SSZ factory helper + typed. limit = 1e6 as a big enough random number */ export function ArrayOf(elementType: Type): ArrayType, unknown, unknown> { if (isCompositeType(elementType)) { return (new ListCompositeType(elementType, Infinity) as unknown) as ArrayType, unknown, unknown>; } else if (isBasicType(elementType)) { return (new ListBasicType(elementType, Infinity) as unknown) as ArrayType, unknown, unknown>; } else { throw Error(`Unknown type ${elementType.typeName}`); } } /** * SSZ factory helper + typed to return responses of type * ``` * data: T * ``` */ export function ContainerData(dataType: TypeJson): TypeJson<{data: T}> { return { toJson: ({data}) => ({ data: dataType.toJson(data), }), fromJson: ({data}: {data: unknown}) => { return { data: dataType.fromJson(data), }; }, }; } /** * SSZ factory helper + typed to return responses of type `{data: T; executionOptimistic: boolean}` */ export function ContainerDataExecutionOptimistic( dataType: TypeJson ): TypeJson<{data: T; executionOptimistic: boolean}> { return { toJson: ({data, executionOptimistic}) => ({ data: dataType.toJson(data), execution_optimistic: executionOptimistic, }), fromJson: ({data, execution_optimistic}: {data: unknown; execution_optimistic: boolean}) => { return { data: dataType.fromJson(data), executionOptimistic: execution_optimistic, }; }, }; } /** * SSZ factory helper + typed to return responses of type * ``` * data: T * version: ForkName * ``` */ export function WithVersion(getType: (fork: ForkName) => TypeJson): TypeJson<{data: T; version: ForkName}> { return { toJson: ({data, version}) => ({ data: getType(version || ForkName.phase0).toJson(data), version, }), fromJson: ({data, version}: {data: unknown; version: string}) => { // Teku returns fork as UPPERCASE version = version.toLowerCase(); // Un-safe external data, validate version is known ForkName value if (!ForkName[version as ForkName]) throw Error(`Invalid version ${version}`); return { data: getType(version as ForkName).fromJson(data), version: version as ForkName, }; }, }; } /** * SSZ factory helper to wrap an existing type with `{executionOptimistic: boolean}` */ export function WithExecutionOptimistic( type: TypeJson ): TypeJson { return { toJson: ({executionOptimistic, ...data}) => ({ ...(type.toJson((data as unknown) as T) as Record), execution_optimistic: executionOptimistic, }), fromJson: ({execution_optimistic, ...data}: T & {execution_optimistic: boolean}) => ({ ...type.fromJson(data), executionOptimistic: execution_optimistic, }), }; } /** * SSZ factory helper to wrap an existing type with `{blockValue: Wei}` */ export function WithBlockValue(type: TypeJson): TypeJson { return { toJson: ({blockValue, ...data}) => ({ ...(type.toJson((data as unknown) as T) as Record), block_value: blockValue.toString(), }), fromJson: ({block_value, ...data}: T & {block_value: string}) => ({ ...type.fromJson(data), blockValue: BigInt(block_value), }), }; } type JsonCase = "snake" | "constant" | "camel" | "param" | "header" | "pascal" | "dot" | "notransform"; /** Helper to only translate casing */ export function jsonType | Record[] | unknown[]>( jsonCase: JsonCase ): TypeJson { return { toJson: (val: T) => objectToExpectedCase(val as Record, jsonCase), fromJson: (json) => objectToExpectedCase(json as Record, codeCase) as T, }; } /** Helper to not do any transformation with the type */ export function sameType(): TypeJson { return { toJson: (val) => val as unknown, fromJson: (json) => (json as unknown) as T, }; } // // RETURN // export type KeysOfNonVoidResolveValues> = { [K in keyof Api]: ApiClientResponseData> extends void ? never : K; }[keyof Api]; export type ReturnTypes> = { [K in keyof Pick>]: TypeJson>>; }; export type RoutesData> = {[K in keyof Api]: RouteDef};