@lodestar/api 1.35.0-dev.f80d2d52da → 1.35.0-dev.fcf8d024ea

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/lib/beacon/client/beacon.d.ts.map +1 -0
  2. package/lib/beacon/client/config.d.ts.map +1 -0
  3. package/lib/beacon/client/debug.d.ts.map +1 -0
  4. package/lib/beacon/client/events.d.ts.map +1 -0
  5. package/lib/beacon/client/index.d.ts.map +1 -0
  6. package/lib/beacon/client/index.js.map +1 -1
  7. package/lib/beacon/client/lightclient.d.ts.map +1 -0
  8. package/lib/beacon/client/lodestar.d.ts.map +1 -0
  9. package/lib/beacon/client/node.d.ts.map +1 -0
  10. package/lib/beacon/client/proof.d.ts.map +1 -0
  11. package/lib/beacon/client/validator.d.ts.map +1 -0
  12. package/lib/beacon/index.d.ts +1 -1
  13. package/lib/beacon/index.d.ts.map +1 -0
  14. package/lib/beacon/index.js.map +1 -1
  15. package/lib/beacon/routes/beacon/block.d.ts.map +1 -0
  16. package/lib/beacon/routes/beacon/index.d.ts +3 -3
  17. package/lib/beacon/routes/beacon/index.d.ts.map +1 -0
  18. package/lib/beacon/routes/beacon/index.js.map +1 -1
  19. package/lib/beacon/routes/beacon/pool.d.ts +1 -1
  20. package/lib/beacon/routes/beacon/pool.d.ts.map +1 -0
  21. package/lib/beacon/routes/beacon/rewards.d.ts.map +1 -0
  22. package/lib/beacon/routes/beacon/rewards.js.map +1 -1
  23. package/lib/beacon/routes/beacon/state.d.ts +2 -2
  24. package/lib/beacon/routes/beacon/state.d.ts.map +1 -0
  25. package/lib/beacon/routes/config.d.ts +1 -1
  26. package/lib/beacon/routes/config.d.ts.map +1 -0
  27. package/lib/beacon/routes/debug.d.ts.map +1 -0
  28. package/lib/beacon/routes/events.d.ts.map +1 -0
  29. package/lib/beacon/routes/events.js.map +1 -1
  30. package/lib/beacon/routes/index.d.ts.map +1 -0
  31. package/lib/beacon/routes/index.js.map +1 -1
  32. package/lib/beacon/routes/lightclient.d.ts.map +1 -0
  33. package/lib/beacon/routes/lodestar.d.ts.map +1 -0
  34. package/lib/beacon/routes/node.d.ts.map +1 -0
  35. package/lib/beacon/routes/proof.d.ts.map +1 -0
  36. package/lib/beacon/routes/validator.d.ts +1 -1
  37. package/lib/beacon/routes/validator.d.ts.map +1 -0
  38. package/lib/beacon/server/beacon.d.ts.map +1 -0
  39. package/lib/beacon/server/config.d.ts.map +1 -0
  40. package/lib/beacon/server/debug.d.ts.map +1 -0
  41. package/lib/beacon/server/events.d.ts.map +1 -0
  42. package/lib/beacon/server/index.d.ts +1 -1
  43. package/lib/beacon/server/index.d.ts.map +1 -0
  44. package/lib/beacon/server/index.js.map +1 -1
  45. package/lib/beacon/server/lightclient.d.ts.map +1 -0
  46. package/lib/beacon/server/lodestar.d.ts.map +1 -0
  47. package/lib/beacon/server/node.d.ts.map +1 -0
  48. package/lib/beacon/server/proof.d.ts.map +1 -0
  49. package/lib/beacon/server/validator.d.ts.map +1 -0
  50. package/lib/builder/client.d.ts.map +1 -0
  51. package/lib/builder/index.d.ts.map +1 -0
  52. package/lib/builder/index.js.map +1 -1
  53. package/lib/builder/routes.d.ts.map +1 -0
  54. package/lib/builder/routes.js.map +1 -1
  55. package/lib/builder/server/index.d.ts +1 -1
  56. package/lib/builder/server/index.d.ts.map +1 -0
  57. package/lib/index.d.ts +6 -6
  58. package/lib/index.d.ts.map +1 -0
  59. package/lib/index.js +3 -3
  60. package/lib/index.js.map +1 -1
  61. package/lib/keymanager/client.d.ts.map +1 -0
  62. package/lib/keymanager/index.d.ts +2 -2
  63. package/lib/keymanager/index.d.ts.map +1 -0
  64. package/lib/keymanager/index.js +1 -2
  65. package/lib/keymanager/index.js.map +1 -1
  66. package/lib/keymanager/routes.d.ts.map +1 -0
  67. package/lib/keymanager/server/index.d.ts +1 -1
  68. package/lib/keymanager/server/index.d.ts.map +1 -0
  69. package/lib/server/index.d.ts.map +1 -0
  70. package/lib/utils/client/error.d.ts.map +1 -0
  71. package/lib/utils/client/error.js +2 -0
  72. package/lib/utils/client/error.js.map +1 -1
  73. package/lib/utils/client/eventSource.d.ts.map +1 -0
  74. package/lib/utils/client/format.d.ts.map +1 -0
  75. package/lib/utils/client/httpClient.d.ts +1 -2
  76. package/lib/utils/client/httpClient.d.ts.map +1 -0
  77. package/lib/utils/client/httpClient.js +13 -9
  78. package/lib/utils/client/httpClient.js.map +1 -1
  79. package/lib/utils/client/index.d.ts +1 -1
  80. package/lib/utils/client/index.d.ts.map +1 -0
  81. package/lib/utils/client/index.js +1 -1
  82. package/lib/utils/client/index.js.map +1 -1
  83. package/lib/utils/client/method.d.ts.map +1 -0
  84. package/lib/utils/client/metrics.d.ts.map +1 -0
  85. package/lib/utils/client/request.d.ts.map +1 -0
  86. package/lib/utils/client/response.d.ts.map +1 -0
  87. package/lib/utils/client/response.js +6 -0
  88. package/lib/utils/client/response.js.map +1 -1
  89. package/lib/utils/codecs.d.ts.map +1 -0
  90. package/lib/utils/fork.d.ts.map +1 -0
  91. package/lib/utils/headers.d.ts.map +1 -0
  92. package/lib/utils/httpStatusCode.d.ts.map +1 -0
  93. package/lib/utils/index.d.ts.map +1 -0
  94. package/lib/utils/metadata.d.ts.map +1 -0
  95. package/lib/utils/schema.d.ts.map +1 -0
  96. package/lib/utils/serdes.d.ts.map +1 -0
  97. package/lib/utils/server/error.d.ts.map +1 -0
  98. package/lib/utils/server/error.js +1 -0
  99. package/lib/utils/server/error.js.map +1 -1
  100. package/lib/utils/server/handler.d.ts.map +1 -0
  101. package/lib/utils/server/index.d.ts.map +1 -0
  102. package/lib/utils/server/method.d.ts.map +1 -0
  103. package/lib/utils/server/parser.d.ts.map +1 -0
  104. package/lib/utils/server/route.d.ts.map +1 -0
  105. package/lib/utils/server/route.js.map +1 -1
  106. package/lib/utils/types.d.ts.map +1 -0
  107. package/lib/utils/urlFormat.d.ts.map +1 -0
  108. package/lib/utils/wireFormat.d.ts.map +1 -0
  109. package/package.json +17 -11
  110. package/src/beacon/client/beacon.ts +12 -0
  111. package/src/beacon/client/config.ts +12 -0
  112. package/src/beacon/client/debug.ts +12 -0
  113. package/src/beacon/client/events.ts +69 -0
  114. package/src/beacon/client/index.ts +46 -0
  115. package/src/beacon/client/lightclient.ts +12 -0
  116. package/src/beacon/client/lodestar.ts +12 -0
  117. package/src/beacon/client/node.ts +12 -0
  118. package/src/beacon/client/proof.ts +12 -0
  119. package/src/beacon/client/validator.ts +12 -0
  120. package/src/beacon/index.ts +24 -0
  121. package/src/beacon/routes/beacon/block.ts +602 -0
  122. package/src/beacon/routes/beacon/index.ts +66 -0
  123. package/src/beacon/routes/beacon/pool.ts +503 -0
  124. package/src/beacon/routes/beacon/rewards.ts +216 -0
  125. package/src/beacon/routes/beacon/state.ts +588 -0
  126. package/src/beacon/routes/config.ts +114 -0
  127. package/src/beacon/routes/debug.ts +231 -0
  128. package/src/beacon/routes/events.ts +337 -0
  129. package/src/beacon/routes/index.ts +33 -0
  130. package/src/beacon/routes/lightclient.ts +241 -0
  131. package/src/beacon/routes/lodestar.ts +456 -0
  132. package/src/beacon/routes/node.ts +286 -0
  133. package/src/beacon/routes/proof.ts +79 -0
  134. package/src/beacon/routes/validator.ts +1014 -0
  135. package/src/beacon/server/beacon.ts +7 -0
  136. package/src/beacon/server/config.ts +7 -0
  137. package/src/beacon/server/debug.ts +7 -0
  138. package/src/beacon/server/events.ts +73 -0
  139. package/src/beacon/server/index.ts +55 -0
  140. package/src/beacon/server/lightclient.ts +7 -0
  141. package/src/beacon/server/lodestar.ts +7 -0
  142. package/src/beacon/server/node.ts +7 -0
  143. package/src/beacon/server/proof.ts +7 -0
  144. package/src/beacon/server/validator.ts +7 -0
  145. package/src/builder/client.ts +9 -0
  146. package/src/builder/index.ts +26 -0
  147. package/src/builder/routes.ts +227 -0
  148. package/src/builder/server/index.ts +19 -0
  149. package/src/index.ts +19 -0
  150. package/src/keymanager/client.ts +9 -0
  151. package/src/keymanager/index.ts +39 -0
  152. package/src/keymanager/routes.ts +699 -0
  153. package/src/keymanager/server/index.ts +19 -0
  154. package/src/server/index.ts +2 -0
  155. package/src/utils/client/error.ts +10 -0
  156. package/src/utils/client/eventSource.ts +7 -0
  157. package/src/utils/client/format.ts +22 -0
  158. package/src/utils/client/httpClient.ts +444 -0
  159. package/src/utils/client/index.ts +6 -0
  160. package/src/utils/client/method.ts +50 -0
  161. package/src/utils/client/metrics.ts +9 -0
  162. package/src/utils/client/request.ts +113 -0
  163. package/src/utils/client/response.ts +205 -0
  164. package/src/utils/codecs.ts +143 -0
  165. package/src/utils/fork.ts +44 -0
  166. package/src/utils/headers.ts +173 -0
  167. package/src/utils/httpStatusCode.ts +392 -0
  168. package/src/utils/index.ts +3 -0
  169. package/src/utils/metadata.ts +170 -0
  170. package/src/utils/schema.ts +141 -0
  171. package/src/utils/serdes.ts +120 -0
  172. package/src/utils/server/error.ts +9 -0
  173. package/src/utils/server/handler.ts +149 -0
  174. package/src/utils/server/index.ts +5 -0
  175. package/src/utils/server/method.ts +38 -0
  176. package/src/utils/server/parser.ts +15 -0
  177. package/src/utils/server/route.ts +45 -0
  178. package/src/utils/types.ts +161 -0
  179. package/src/utils/urlFormat.ts +112 -0
  180. package/src/utils/wireFormat.ts +24 -0
@@ -0,0 +1,141 @@
1
+ import {MediaType} from "./headers.js";
2
+ import {Endpoint, HeaderParams, PathParams, QueryParams} from "./types.js";
3
+
4
+ // Reasoning: Allows to declare JSON schemas for server routes in a succinct typesafe way.
5
+ // The enums exposed here are very feature incomplete but cover the minimum necessary for
6
+ // the existing routes. Since the arguments for Ethereum Consensus server routes are very simple it suffice.
7
+
8
+ type JsonSchema = Record<string, unknown>;
9
+ type JsonSchemaObj = {
10
+ type: "object";
11
+ required: string[];
12
+ properties: Record<string, JsonSchema>;
13
+ };
14
+ type RequireSchema<T> = {[K in keyof T]-?: Schema};
15
+
16
+ export type SchemaDefinition<ReqType extends Endpoint["request"]> = (ReqType["params"] extends PathParams
17
+ ? {params: RequireSchema<ReqType["params"]>}
18
+ : {params?: never}) &
19
+ (ReqType["query"] extends QueryParams ? {query: RequireSchema<ReqType["query"]>} : {query?: never}) &
20
+ (ReqType["headers"] extends HeaderParams ? {headers: RequireSchema<ReqType["headers"]>} : {headers?: never}) &
21
+ (ReqType extends {body: unknown} ? {body: Schema} : {body?: never});
22
+
23
+ export enum Schema {
24
+ Uint,
25
+ UintRequired,
26
+ UintArray,
27
+ String,
28
+ StringRequired,
29
+ StringArray,
30
+ StringArrayRequired,
31
+ UintOrStringRequired,
32
+ UintOrStringArray,
33
+ Object,
34
+ ObjectArray,
35
+ AnyArray,
36
+ Boolean,
37
+ }
38
+
39
+ /**
40
+ * Return JSON schema from a Schema enum. Useful to declare schemas in a succinct format
41
+ */
42
+ function getJsonSchemaItem(schema: Schema): JsonSchema {
43
+ switch (schema) {
44
+ case Schema.Uint:
45
+ case Schema.UintRequired:
46
+ return {type: "integer", minimum: 0};
47
+
48
+ case Schema.UintArray:
49
+ return {type: "array", items: {type: "integer", minimum: 0}};
50
+
51
+ case Schema.String:
52
+ case Schema.StringRequired:
53
+ return {type: "string"};
54
+
55
+ case Schema.StringArray:
56
+ case Schema.StringArrayRequired:
57
+ return {type: "array", items: {type: "string"}};
58
+
59
+ case Schema.UintOrStringRequired:
60
+ return {anyOf: [{type: "string"}, {type: "integer"}]};
61
+ case Schema.UintOrStringArray:
62
+ return {type: "array", items: {anyOf: [{type: "string"}, {type: "integer"}]}};
63
+
64
+ case Schema.Object:
65
+ return {type: "object"};
66
+
67
+ case Schema.ObjectArray:
68
+ return {type: "array", items: {type: "object"}};
69
+
70
+ case Schema.AnyArray:
71
+ return {type: "array"};
72
+
73
+ case Schema.Boolean:
74
+ return {type: "boolean"};
75
+ }
76
+ }
77
+
78
+ function isRequired(schema: Schema): boolean {
79
+ switch (schema) {
80
+ case Schema.UintRequired:
81
+ case Schema.StringRequired:
82
+ case Schema.UintOrStringRequired:
83
+ case Schema.StringArrayRequired:
84
+ return true;
85
+
86
+ default:
87
+ return false;
88
+ }
89
+ }
90
+
91
+ export function getFastifySchema<T extends Endpoint["request"]>(schemaDef: SchemaDefinition<T>): JsonSchema {
92
+ const schema: {params?: JsonSchemaObj; querystring?: JsonSchemaObj; headers?: JsonSchemaObj; body?: JsonSchema} = {};
93
+
94
+ if (schemaDef.body != null) {
95
+ schema.body = {
96
+ content: {
97
+ [MediaType.json]: {
98
+ schema: getJsonSchemaItem(schemaDef.body),
99
+ },
100
+ [MediaType.ssz]: {
101
+ schema: {},
102
+ },
103
+ },
104
+ };
105
+ }
106
+
107
+ if (schemaDef.params) {
108
+ schema.params = {type: "object", required: [], properties: {}};
109
+
110
+ for (const [key, def] of Object.entries<Schema>(schemaDef.params)) {
111
+ schema.params.properties[key] = getJsonSchemaItem(def);
112
+ if (isRequired(def)) {
113
+ schema.params.required.push(key);
114
+ }
115
+ }
116
+ }
117
+
118
+ if (schemaDef.query) {
119
+ schema.querystring = {type: "object", required: [], properties: {}};
120
+
121
+ for (const [key, def] of Object.entries<Schema>(schemaDef.query)) {
122
+ schema.querystring.properties[key] = getJsonSchemaItem(def);
123
+ if (isRequired(def)) {
124
+ schema.querystring.required.push(key);
125
+ }
126
+ }
127
+ }
128
+
129
+ if (schemaDef.headers) {
130
+ schema.headers = {type: "object", required: [], properties: {}};
131
+
132
+ for (const [key, def] of Object.entries<Schema>(schemaDef.headers)) {
133
+ schema.headers.properties[key] = getJsonSchemaItem(def);
134
+ if (isRequired(def)) {
135
+ schema.headers.required.push(key);
136
+ }
137
+ }
138
+ }
139
+
140
+ return schema;
141
+ }
@@ -0,0 +1,120 @@
1
+ import {JsonPath} from "@chainsafe/ssz";
2
+ import {fromHex, toHex} from "@lodestar/utils";
3
+
4
+ /**
5
+ * Serialize proof path to JSON.
6
+ * @param paths `[["finalized_checkpoint", 0, "root", 12000]]`
7
+ * @returns `['["finalized_checkpoint",0,"root",12000]']`
8
+ */
9
+ export function querySerializeProofPathsArr(paths: JsonPath[]): string[] {
10
+ return paths.map((path) => JSON.stringify(path));
11
+ }
12
+
13
+ /**
14
+ * Deserialize JSON proof path to proof path
15
+ * @param pathStrs `['["finalized_checkpoint",0,"root",12000]']`
16
+ * @returns `[["finalized_checkpoint", 0, "root", 12000]]`
17
+ */
18
+ export function queryParseProofPathsArr(pathStrs: string | string[]): JsonPath[] {
19
+ if (Array.isArray(pathStrs)) {
20
+ return pathStrs.map((pathStr) => queryParseProofPaths(pathStr));
21
+ }
22
+ return [queryParseProofPaths(pathStrs)];
23
+ }
24
+
25
+ /**
26
+ * Deserialize single JSON proof path to proof path
27
+ * @param pathStr `'["finalized_checkpoint",0,"root",12000]'`
28
+ * @returns `["finalized_checkpoint", 0, "root", 12000]`
29
+ */
30
+ export function queryParseProofPaths(pathStr: string): JsonPath {
31
+ const path = JSON.parse(pathStr) as JsonPath;
32
+
33
+ if (!Array.isArray(path)) {
34
+ throw Error("Proof pathStr is not an array");
35
+ }
36
+
37
+ for (let i = 0; i < path.length; i++) {
38
+ const elType = typeof path[i];
39
+ if (elType !== "string" && elType !== "number") {
40
+ throw Error(`Proof pathStr[${i}] not string or number`);
41
+ }
42
+ }
43
+
44
+ return path;
45
+ }
46
+
47
+ export type U64 = number;
48
+ export type U64Str = string;
49
+
50
+ export function fromU64Str(u64Str: U64Str): number {
51
+ const u64 = parseInt(u64Str, 10);
52
+ if (!Number.isFinite(u64)) {
53
+ throw Error(`Invalid uin64 ${u64Str}`);
54
+ }
55
+ return u64;
56
+ }
57
+
58
+ export function toU64Str(u64: U64): U64Str {
59
+ return u64.toString(10);
60
+ }
61
+
62
+ export function fromU64StrOpt(u64Str: U64Str | undefined): U64 | undefined {
63
+ return u64Str !== undefined ? fromU64Str(u64Str) : undefined;
64
+ }
65
+
66
+ export function toU64StrOpt(u64: U64 | undefined): U64Str | undefined {
67
+ return u64 !== undefined ? toU64Str(u64) : undefined;
68
+ }
69
+
70
+ export function toValidatorIdsStr(ids?: (string | number)[]): string[] | undefined {
71
+ return ids?.map((id) => (typeof id === "string" ? id : toU64Str(id)));
72
+ }
73
+
74
+ export function fromValidatorIdsStr(ids?: string[]): (string | number)[] | undefined {
75
+ return ids?.map((id) => (typeof id === "string" && id.startsWith("0x") ? id : fromU64Str(id)));
76
+ }
77
+
78
+ const GRAFFITI_HEX_LENGTH = 66;
79
+
80
+ export function toGraffitiHex(utf8?: string): string | undefined {
81
+ if (utf8 === undefined) {
82
+ return undefined;
83
+ }
84
+
85
+ const hex = toHex(new TextEncoder().encode(utf8));
86
+
87
+ if (hex.length > GRAFFITI_HEX_LENGTH) {
88
+ // remove characters from the end if hex string is too long
89
+ return hex.slice(0, GRAFFITI_HEX_LENGTH);
90
+ }
91
+
92
+ if (hex.length < GRAFFITI_HEX_LENGTH) {
93
+ // right-pad with zeros if hex string is too short
94
+ return hex.padEnd(GRAFFITI_HEX_LENGTH, "0");
95
+ }
96
+
97
+ return hex;
98
+ }
99
+
100
+ export function fromGraffitiHex(hex?: string): string | undefined {
101
+ if (hex === undefined) {
102
+ return undefined;
103
+ }
104
+ try {
105
+ return new TextDecoder("utf8").decode(fromHex(hex));
106
+ } catch (_e) {
107
+ // allow malformed graffiti hex string
108
+ return hex;
109
+ }
110
+ }
111
+
112
+ export function toBoolean(value: string): boolean {
113
+ value = value.toLowerCase();
114
+
115
+ if (value !== "true" && value !== "false") {
116
+ throw Error(`Invalid boolean ${value}`);
117
+ }
118
+
119
+ return value === "true";
120
+ }
@@ -0,0 +1,9 @@
1
+ import {HttpErrorCodes} from "../httpStatusCode.js";
2
+
3
+ export class ApiError extends Error {
4
+ statusCode: HttpErrorCodes;
5
+ constructor(statusCode: HttpErrorCodes, message?: string) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ }
9
+ }
@@ -0,0 +1,149 @@
1
+ import type * as fastify from "fastify";
2
+ import {HttpHeader, MediaType, SUPPORTED_MEDIA_TYPES, parseAcceptHeader, parseContentTypeHeader} from "../headers.js";
3
+ import {
4
+ Endpoint,
5
+ JsonRequestData,
6
+ JsonRequestMethods,
7
+ RequestData,
8
+ RequestWithBodyCodec,
9
+ RequestWithoutBodyCodec,
10
+ RouteDefinition,
11
+ SszRequestData,
12
+ SszRequestMethods,
13
+ isRequestWithoutBody,
14
+ } from "../types.js";
15
+ import {WireFormat, fromWireFormat, getWireFormat} from "../wireFormat.js";
16
+ import {ApiError} from "./error.js";
17
+ import {ApplicationMethod} from "./method.js";
18
+
19
+ export type FastifyHandler<E extends Endpoint> = fastify.RouteHandlerMethod<
20
+ fastify.RawServerDefault,
21
+ fastify.RawRequestDefaultExpression<fastify.RawServerDefault>,
22
+ fastify.RawReplyDefaultExpression<fastify.RawServerDefault>,
23
+ {
24
+ Body: E["request"] extends JsonRequestData ? E["request"]["body"] : undefined;
25
+ Querystring: E["request"]["query"];
26
+ Params: E["request"]["params"];
27
+ Headers: E["request"]["headers"];
28
+ },
29
+ fastify.ContextConfigDefault
30
+ >;
31
+
32
+ export function createFastifyHandler<E extends Endpoint>(
33
+ definition: RouteDefinition<E>,
34
+ method: ApplicationMethod<E>,
35
+ _operationId: string
36
+ ): FastifyHandler<E> {
37
+ return async (req, resp) => {
38
+ // Determine response wire format first to inform application method
39
+ // about the preferable return type to avoid unnecessary serialization
40
+ let responseMediaType: MediaType | null;
41
+
42
+ const acceptHeader = req.headers.accept;
43
+ if (definition.resp.isEmpty) {
44
+ // Ignore Accept header, the response will be sent without body
45
+ responseMediaType = null;
46
+ } else if (acceptHeader === undefined) {
47
+ // Default to json to not force user to set header, e.g. when using curl
48
+ responseMediaType = MediaType.json;
49
+ } else {
50
+ const {onlySupport} = definition.resp;
51
+ const supportedMediaTypes = onlySupport !== undefined ? [fromWireFormat(onlySupport)] : SUPPORTED_MEDIA_TYPES;
52
+ responseMediaType = parseAcceptHeader(acceptHeader, supportedMediaTypes);
53
+
54
+ if (responseMediaType === null) {
55
+ throw new ApiError(406, `Accepted media types not supported: ${acceptHeader}`);
56
+ }
57
+ }
58
+ const responseWireFormat = responseMediaType !== null ? getWireFormat(responseMediaType) : null;
59
+
60
+ let requestWireFormat: WireFormat | null;
61
+ if (isRequestWithoutBody(definition)) {
62
+ requestWireFormat = null;
63
+ } else {
64
+ const contentType = req.headers[HttpHeader.ContentType];
65
+ if (contentType === undefined && req.body === undefined) {
66
+ // Default to json parser if body is omitted. This is not possible for most
67
+ // routes as request will fail schema validation before this handler is called
68
+ requestWireFormat = WireFormat.json;
69
+ } else {
70
+ if (contentType === undefined) {
71
+ throw new ApiError(400, "Content-Type header is required");
72
+ }
73
+ const requestMediaType = parseContentTypeHeader(contentType);
74
+ if (requestMediaType === null) {
75
+ throw new ApiError(415, `Unsupported media type: ${contentType.split(";", 1)[0]}`);
76
+ }
77
+ requestWireFormat = getWireFormat(requestMediaType);
78
+ }
79
+
80
+ const {onlySupport} = definition.req as RequestWithBodyCodec<E>;
81
+ if (onlySupport !== undefined && onlySupport !== requestWireFormat) {
82
+ throw new ApiError(415, `Endpoint only supports ${onlySupport.toUpperCase()} requests`);
83
+ }
84
+ }
85
+
86
+ let args: E["args"];
87
+ try {
88
+ switch (requestWireFormat) {
89
+ case WireFormat.json:
90
+ args = (definition.req as JsonRequestMethods<E>).parseReqJson(req as JsonRequestData);
91
+ break;
92
+ case WireFormat.ssz:
93
+ args = (definition.req as SszRequestMethods<E>).parseReqSsz(req as SszRequestData<E["request"]>);
94
+ break;
95
+ case null:
96
+ args = (definition.req as RequestWithoutBodyCodec<E>).parseReq(req as RequestData);
97
+ break;
98
+ }
99
+ } catch (e) {
100
+ if (e instanceof ApiError) throw e;
101
+ // Errors related to parsing should return 400 status code
102
+ throw new ApiError(400, (e as Error).message);
103
+ }
104
+
105
+ const response = await method(args, {
106
+ sszBytes: requestWireFormat === WireFormat.ssz ? (req.body as Uint8Array) : null,
107
+ returnBytes: responseWireFormat === WireFormat.ssz,
108
+ });
109
+
110
+ if (response?.status !== undefined) {
111
+ resp.statusCode = response.status;
112
+ }
113
+
114
+ switch (responseWireFormat) {
115
+ case WireFormat.json: {
116
+ const metaHeaders = definition.resp.meta.toHeadersObject(response?.meta);
117
+ metaHeaders[HttpHeader.ContentType] = MediaType.json;
118
+ void resp.headers(metaHeaders);
119
+ const data =
120
+ response?.data instanceof Uint8Array
121
+ ? definition.resp.data.toJson(definition.resp.data.deserialize(response.data, response.meta), response.meta)
122
+ : definition.resp.data.toJson(response?.data, response?.meta);
123
+ const metaJson = definition.resp.meta.toJson(response?.meta);
124
+ if (definition.resp.transform) {
125
+ return definition.resp.transform.toResponse(data, metaJson);
126
+ }
127
+ return {
128
+ data,
129
+ ...(metaJson as object),
130
+ };
131
+ }
132
+ case WireFormat.ssz: {
133
+ const metaHeaders = definition.resp.meta.toHeadersObject(response?.meta);
134
+ metaHeaders[HttpHeader.ContentType] = MediaType.ssz;
135
+ void resp.headers(metaHeaders);
136
+ const data =
137
+ response?.data instanceof Uint8Array
138
+ ? response.data
139
+ : definition.resp.data.serialize(response?.data, response?.meta);
140
+ // Fastify supports returning `Uint8Array` from handler and will efficiently
141
+ // convert it to a `Buffer` internally without copying the underlying `ArrayBuffer`
142
+ return data;
143
+ }
144
+ case null:
145
+ // Send response without body
146
+ return;
147
+ }
148
+ };
149
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./error.js";
2
+ export * from "./handler.js";
3
+ export * from "./method.js";
4
+ export * from "./parser.js";
5
+ export * from "./route.js";
@@ -0,0 +1,38 @@
1
+ import {EmptyMeta, EmptyResponseData} from "../codecs.js";
2
+ import {HttpSuccessCodes} from "../httpStatusCode.js";
3
+ import {Endpoint, HasOnlyOptionalProps} from "../types.js";
4
+
5
+ type ApplicationResponseObject<E extends Endpoint> = {
6
+ /**
7
+ * Set non-200 success status code
8
+ */
9
+ status?: HttpSuccessCodes;
10
+ } & (E["return"] extends EmptyResponseData
11
+ ? {data?: never}
12
+ : {data: E["return"] | (E["return"] extends undefined ? undefined : Uint8Array)}) &
13
+ (E["meta"] extends EmptyMeta ? {meta?: never} : {meta: E["meta"]});
14
+
15
+ export type ApplicationResponse<E extends Endpoint> = HasOnlyOptionalProps<ApplicationResponseObject<E>> extends true
16
+ ? ApplicationResponseObject<E> | void
17
+ : ApplicationResponseObject<E>;
18
+
19
+ export type ApiContext = {
20
+ /**
21
+ * Raw ssz bytes from request payload, only available for ssz requests
22
+ */
23
+ sszBytes?: Uint8Array | null;
24
+ /**
25
+ * Informs application method about preferable return type to avoid unnecessary serialization
26
+ */
27
+ returnBytes?: boolean;
28
+ };
29
+
30
+ type GenericOptions = Record<string, unknown>;
31
+
32
+ export type ApplicationMethod<E extends Endpoint> = (
33
+ args: E["args"],
34
+ context?: ApiContext,
35
+ opts?: GenericOptions
36
+ ) => Promise<ApplicationResponse<E>>;
37
+
38
+ export type ApplicationMethods<Es extends Record<string, Endpoint>> = {[K in keyof Es]: ApplicationMethod<Es[K]>};
@@ -0,0 +1,15 @@
1
+ import type * as fastify from "fastify";
2
+ import {MediaType} from "../headers.js";
3
+
4
+ export function addSszContentTypeParser(server: fastify.FastifyInstance): void {
5
+ server.addContentTypeParser(
6
+ MediaType.ssz,
7
+ {parseAs: "buffer"},
8
+ async (_request: fastify.FastifyRequest, payload: Buffer) => {
9
+ // We could just return the `Buffer` here which is a subclass of `Uint8Array` but downstream code does not require it
10
+ // and it's better to convert it here to avoid unexpected behavior such as `Buffer.prototype.slice` not copying memory
11
+ // See https://github.com/nodejs/node/issues/41588#issuecomment-1016269584
12
+ return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
13
+ }
14
+ );
15
+ }
@@ -0,0 +1,45 @@
1
+ import type * as fastify from "fastify";
2
+ import {mapValues} from "@lodestar/utils";
3
+ import {getFastifySchema} from "../schema.js";
4
+ import {Endpoint, RouteDefinition, RouteDefinitions} from "../types.js";
5
+ import {toColonNotationPath} from "../urlFormat.js";
6
+ import {FastifyHandler, createFastifyHandler} from "./handler.js";
7
+ import {ApplicationMethod, ApplicationMethods} from "./method.js";
8
+
9
+ export type FastifySchema = fastify.FastifySchema & {
10
+ operationId: string;
11
+ tags?: string[];
12
+ };
13
+
14
+ export type FastifyRoute<E extends Endpoint> = {
15
+ url: string;
16
+ method: fastify.HTTPMethods;
17
+ handler: FastifyHandler<E>;
18
+ schema: FastifySchema;
19
+ };
20
+ export type FastifyRoutes<Es extends Record<string, Endpoint>> = {[K in keyof Es]: FastifyRoute<Es[K]>};
21
+
22
+ export function createFastifyRoute<E extends Endpoint>(
23
+ definition: RouteDefinition<E>,
24
+ method: ApplicationMethod<E>,
25
+ operationId: string
26
+ ): FastifyRoute<E> {
27
+ return {
28
+ url: toColonNotationPath(definition.url),
29
+ method: definition.method,
30
+ handler: createFastifyHandler(definition, method, operationId),
31
+ schema: {
32
+ ...getFastifySchema(definition.req.schema),
33
+ operationId,
34
+ },
35
+ };
36
+ }
37
+
38
+ export function createFastifyRoutes<Es extends Record<string, Endpoint>>(
39
+ definitions: RouteDefinitions<Es>,
40
+ methods: ApplicationMethods<Es>
41
+ ): FastifyRoutes<Es> {
42
+ return mapValues(definitions, (definition, operationId) =>
43
+ createFastifyRoute(definition, methods?.[operationId]?.bind(methods), operationId as string)
44
+ );
45
+ }
@@ -0,0 +1,161 @@
1
+ import {ExtraRequestInit} from "./client/request.js";
2
+ import {EmptyMeta} from "./codecs.js";
3
+ import {HeadersExtra} from "./headers.js";
4
+ import {SchemaDefinition} from "./schema.js";
5
+ import {WireFormat} from "./wireFormat.js";
6
+
7
+ export type HasOnlyOptionalProps<T> = {
8
+ [K in keyof T]-?: object extends Pick<T, K> ? never : K;
9
+ } extends {[K2 in keyof T]: never}
10
+ ? true
11
+ : false;
12
+
13
+ export type PathParams = Record<string, string | number>;
14
+ export type QueryParams = Record<string, string | number | boolean | (string | number)[]>;
15
+ export type HeaderParams = Record<string, string>;
16
+
17
+ export type RequestData<
18
+ P extends PathParams = PathParams,
19
+ Q extends QueryParams = QueryParams,
20
+ H extends HeaderParams = HeaderParams,
21
+ > = {
22
+ params?: P;
23
+ query?: Q;
24
+ headers?: H;
25
+ };
26
+
27
+ export type JsonRequestData<
28
+ B = unknown,
29
+ P extends PathParams = PathParams,
30
+ Q extends QueryParams = QueryParams,
31
+ H extends HeaderParams = HeaderParams,
32
+ > = RequestData<P, Q, H> & {
33
+ body?: B;
34
+ };
35
+
36
+ export type SszRequestData<P extends JsonRequestData> = Omit<P, "body"> &
37
+ ("body" extends keyof P ? (P["body"] extends void ? {body?: never} : {body: Uint8Array}) : {body?: never});
38
+
39
+ export type HttpMethod = "GET" | "POST" | "DELETE";
40
+
41
+ /**
42
+ * This type describes the general shape of a route
43
+ *
44
+ * This includes both http and application-level shape
45
+ * - The http method
46
+ * - Used to more strictly enforce the shape of the request
47
+ * - The application-level parameters
48
+ * - this enforces the shape of the input data passed by the client and to the route handler
49
+ * - The http request
50
+ * - this enforces the shape of the querystring, url params, request body
51
+ * - The application-level return data
52
+ * - this enforces the shape of the output data passed back to the client and returned by the route handler
53
+ * - The application-level return metadata
54
+ * - this enforces the shape of the returned metadata, used informationally and to help decode the return data
55
+ */
56
+ export type Endpoint<
57
+ Method extends HttpMethod = HttpMethod,
58
+ ArgsType = unknown,
59
+ RequestType extends Method extends "GET" ? RequestData : JsonRequestData = JsonRequestData,
60
+ ReturnType = unknown,
61
+ Meta = unknown,
62
+ > = {
63
+ method: Method;
64
+ /** The parameters the client passes / server app code ingests */
65
+ args: ArgsType;
66
+ /** The parameters in the http request */
67
+ request: RequestType;
68
+ /** The return data */
69
+ return: ReturnType;
70
+ /** The return metadata */
71
+ meta: Meta;
72
+ };
73
+
74
+ // Request codec
75
+
76
+ /** Encode / decode requests to & from function params, as well as schema definitions */
77
+ export type RequestWithoutBodyCodec<E extends Endpoint> = {
78
+ writeReq: (p: E["args"]) => E["request"]; // client
79
+ parseReq: (r: E["request"]) => E["args"]; // server
80
+ schema: SchemaDefinition<E["request"]>;
81
+ };
82
+
83
+ export type JsonRequestMethods<E extends Endpoint> = {
84
+ writeReqJson: (p: E["args"]) => E["request"]; // client
85
+ parseReqJson: (r: E["request"]) => E["args"]; // server
86
+ };
87
+
88
+ export type SszRequestMethods<E extends Endpoint> = {
89
+ writeReqSsz: (p: E["args"]) => SszRequestData<E["request"]>; // client
90
+ parseReqSsz: (r: SszRequestData<E["request"]>) => E["args"]; // server
91
+ };
92
+
93
+ export type RequestWithBodyCodec<E extends Endpoint> = JsonRequestMethods<E> &
94
+ SszRequestMethods<E> & {
95
+ schema: SchemaDefinition<E["request"]>;
96
+ /** Support ssz-only or json-only requests */
97
+ onlySupport?: WireFormat;
98
+ };
99
+
100
+ /**
101
+ * Handles translation between `Endpoint["args"]` and `Endpoint["request"]`
102
+ */
103
+ export type RequestCodec<E extends Endpoint> = E["method"] extends "GET"
104
+ ? RequestWithoutBodyCodec<E>
105
+ : "body" extends keyof E["request"]
106
+ ? RequestWithBodyCodec<E>
107
+ : RequestWithoutBodyCodec<E>;
108
+
109
+ export function isRequestWithoutBody<E extends Endpoint>(
110
+ definition: RouteDefinition<E>
111
+ ): definition is RouteDefinition<E> & {req: RequestWithoutBodyCodec<E>} {
112
+ return definition.method === "GET" || definition.req.schema.body === undefined;
113
+ }
114
+
115
+ // Response codec
116
+
117
+ export type ResponseDataCodec<T, M> = {
118
+ toJson: (data: T, meta: M) => unknown; // server
119
+ fromJson: (data: unknown, meta: M) => T; // client
120
+ serialize: (data: T, meta: M) => Uint8Array; // server
121
+ deserialize: (data: Uint8Array, meta: M) => T; // client
122
+ };
123
+
124
+ export type ResponseMetadataCodec<T> = {
125
+ toJson: (val: T) => unknown; // server
126
+ fromJson: (val: unknown) => T; // client
127
+ toHeadersObject: (val: T) => Record<string, string>; // server
128
+ fromHeaders: (headers: HeadersExtra) => T; // server
129
+ };
130
+
131
+ export type ResponseCodec<E extends Endpoint> = {
132
+ data: ResponseDataCodec<E["return"], E["meta"]>;
133
+ meta: ResponseMetadataCodec<E["meta"]>;
134
+ /** Occasionally, json responses require an extra transformation to separate the data from metadata */
135
+ transform?: {
136
+ toResponse: (data: unknown, meta: unknown) => unknown;
137
+ fromResponse: (resp: unknown) => {
138
+ data: E["return"];
139
+ } & (E["meta"] extends EmptyMeta ? {meta?: never} : {meta: E["meta"]});
140
+ };
141
+ /** Support ssz-only or json-only responses */
142
+ onlySupport?: WireFormat;
143
+ /** Indicator used to handle empty responses */
144
+ isEmpty?: true;
145
+ };
146
+
147
+ /**
148
+ * Top-level definition of a route used by both the client and server
149
+ * - url and method
150
+ * - request and response codec
151
+ * - request json schema
152
+ */
153
+ export type RouteDefinition<E extends Endpoint> = {
154
+ url: string;
155
+ method: E["method"];
156
+ req: RequestCodec<E>;
157
+ resp: ResponseCodec<E>;
158
+ init?: ExtraRequestInit;
159
+ };
160
+
161
+ export type RouteDefinitions<Es extends Record<string, Endpoint>> = {[K in keyof Es]: RouteDefinition<Es[K]>};