@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.
- package/lib/beacon/client/beacon.d.ts.map +1 -0
- package/lib/beacon/client/config.d.ts.map +1 -0
- package/lib/beacon/client/debug.d.ts.map +1 -0
- package/lib/beacon/client/events.d.ts.map +1 -0
- package/lib/beacon/client/index.d.ts.map +1 -0
- package/lib/beacon/client/index.js.map +1 -1
- package/lib/beacon/client/lightclient.d.ts.map +1 -0
- package/lib/beacon/client/lodestar.d.ts.map +1 -0
- package/lib/beacon/client/node.d.ts.map +1 -0
- package/lib/beacon/client/proof.d.ts.map +1 -0
- package/lib/beacon/client/validator.d.ts.map +1 -0
- package/lib/beacon/index.d.ts +1 -1
- package/lib/beacon/index.d.ts.map +1 -0
- package/lib/beacon/index.js.map +1 -1
- package/lib/beacon/routes/beacon/block.d.ts.map +1 -0
- package/lib/beacon/routes/beacon/index.d.ts +3 -3
- package/lib/beacon/routes/beacon/index.d.ts.map +1 -0
- package/lib/beacon/routes/beacon/index.js.map +1 -1
- package/lib/beacon/routes/beacon/pool.d.ts +1 -1
- package/lib/beacon/routes/beacon/pool.d.ts.map +1 -0
- package/lib/beacon/routes/beacon/rewards.d.ts.map +1 -0
- package/lib/beacon/routes/beacon/rewards.js.map +1 -1
- package/lib/beacon/routes/beacon/state.d.ts +2 -2
- package/lib/beacon/routes/beacon/state.d.ts.map +1 -0
- package/lib/beacon/routes/config.d.ts +1 -1
- package/lib/beacon/routes/config.d.ts.map +1 -0
- package/lib/beacon/routes/debug.d.ts.map +1 -0
- package/lib/beacon/routes/events.d.ts.map +1 -0
- package/lib/beacon/routes/events.js.map +1 -1
- package/lib/beacon/routes/index.d.ts.map +1 -0
- package/lib/beacon/routes/index.js.map +1 -1
- package/lib/beacon/routes/lightclient.d.ts.map +1 -0
- package/lib/beacon/routes/lodestar.d.ts.map +1 -0
- package/lib/beacon/routes/node.d.ts.map +1 -0
- package/lib/beacon/routes/proof.d.ts.map +1 -0
- package/lib/beacon/routes/validator.d.ts +1 -1
- package/lib/beacon/routes/validator.d.ts.map +1 -0
- package/lib/beacon/server/beacon.d.ts.map +1 -0
- package/lib/beacon/server/config.d.ts.map +1 -0
- package/lib/beacon/server/debug.d.ts.map +1 -0
- package/lib/beacon/server/events.d.ts.map +1 -0
- package/lib/beacon/server/index.d.ts +1 -1
- package/lib/beacon/server/index.d.ts.map +1 -0
- package/lib/beacon/server/index.js.map +1 -1
- package/lib/beacon/server/lightclient.d.ts.map +1 -0
- package/lib/beacon/server/lodestar.d.ts.map +1 -0
- package/lib/beacon/server/node.d.ts.map +1 -0
- package/lib/beacon/server/proof.d.ts.map +1 -0
- package/lib/beacon/server/validator.d.ts.map +1 -0
- package/lib/builder/client.d.ts.map +1 -0
- package/lib/builder/index.d.ts.map +1 -0
- package/lib/builder/index.js.map +1 -1
- package/lib/builder/routes.d.ts.map +1 -0
- package/lib/builder/routes.js.map +1 -1
- package/lib/builder/server/index.d.ts +1 -1
- package/lib/builder/server/index.d.ts.map +1 -0
- package/lib/index.d.ts +6 -6
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -3
- package/lib/index.js.map +1 -1
- package/lib/keymanager/client.d.ts.map +1 -0
- package/lib/keymanager/index.d.ts +2 -2
- package/lib/keymanager/index.d.ts.map +1 -0
- package/lib/keymanager/index.js +1 -2
- package/lib/keymanager/index.js.map +1 -1
- package/lib/keymanager/routes.d.ts.map +1 -0
- package/lib/keymanager/server/index.d.ts +1 -1
- package/lib/keymanager/server/index.d.ts.map +1 -0
- package/lib/server/index.d.ts.map +1 -0
- package/lib/utils/client/error.d.ts.map +1 -0
- package/lib/utils/client/error.js +2 -0
- package/lib/utils/client/error.js.map +1 -1
- package/lib/utils/client/eventSource.d.ts.map +1 -0
- package/lib/utils/client/format.d.ts.map +1 -0
- package/lib/utils/client/httpClient.d.ts +1 -2
- package/lib/utils/client/httpClient.d.ts.map +1 -0
- package/lib/utils/client/httpClient.js +13 -9
- package/lib/utils/client/httpClient.js.map +1 -1
- package/lib/utils/client/index.d.ts +1 -1
- package/lib/utils/client/index.d.ts.map +1 -0
- package/lib/utils/client/index.js +1 -1
- package/lib/utils/client/index.js.map +1 -1
- package/lib/utils/client/method.d.ts.map +1 -0
- package/lib/utils/client/metrics.d.ts.map +1 -0
- package/lib/utils/client/request.d.ts.map +1 -0
- package/lib/utils/client/response.d.ts.map +1 -0
- package/lib/utils/client/response.js +6 -0
- package/lib/utils/client/response.js.map +1 -1
- package/lib/utils/codecs.d.ts.map +1 -0
- package/lib/utils/fork.d.ts.map +1 -0
- package/lib/utils/headers.d.ts.map +1 -0
- package/lib/utils/httpStatusCode.d.ts.map +1 -0
- package/lib/utils/index.d.ts.map +1 -0
- package/lib/utils/metadata.d.ts.map +1 -0
- package/lib/utils/schema.d.ts.map +1 -0
- package/lib/utils/serdes.d.ts.map +1 -0
- package/lib/utils/server/error.d.ts.map +1 -0
- package/lib/utils/server/error.js +1 -0
- package/lib/utils/server/error.js.map +1 -1
- package/lib/utils/server/handler.d.ts.map +1 -0
- package/lib/utils/server/index.d.ts.map +1 -0
- package/lib/utils/server/method.d.ts.map +1 -0
- package/lib/utils/server/parser.d.ts.map +1 -0
- package/lib/utils/server/route.d.ts.map +1 -0
- package/lib/utils/server/route.js.map +1 -1
- package/lib/utils/types.d.ts.map +1 -0
- package/lib/utils/urlFormat.d.ts.map +1 -0
- package/lib/utils/wireFormat.d.ts.map +1 -0
- package/package.json +17 -11
- package/src/beacon/client/beacon.ts +12 -0
- package/src/beacon/client/config.ts +12 -0
- package/src/beacon/client/debug.ts +12 -0
- package/src/beacon/client/events.ts +69 -0
- package/src/beacon/client/index.ts +46 -0
- package/src/beacon/client/lightclient.ts +12 -0
- package/src/beacon/client/lodestar.ts +12 -0
- package/src/beacon/client/node.ts +12 -0
- package/src/beacon/client/proof.ts +12 -0
- package/src/beacon/client/validator.ts +12 -0
- package/src/beacon/index.ts +24 -0
- package/src/beacon/routes/beacon/block.ts +602 -0
- package/src/beacon/routes/beacon/index.ts +66 -0
- package/src/beacon/routes/beacon/pool.ts +503 -0
- package/src/beacon/routes/beacon/rewards.ts +216 -0
- package/src/beacon/routes/beacon/state.ts +588 -0
- package/src/beacon/routes/config.ts +114 -0
- package/src/beacon/routes/debug.ts +231 -0
- package/src/beacon/routes/events.ts +337 -0
- package/src/beacon/routes/index.ts +33 -0
- package/src/beacon/routes/lightclient.ts +241 -0
- package/src/beacon/routes/lodestar.ts +456 -0
- package/src/beacon/routes/node.ts +286 -0
- package/src/beacon/routes/proof.ts +79 -0
- package/src/beacon/routes/validator.ts +1014 -0
- package/src/beacon/server/beacon.ts +7 -0
- package/src/beacon/server/config.ts +7 -0
- package/src/beacon/server/debug.ts +7 -0
- package/src/beacon/server/events.ts +73 -0
- package/src/beacon/server/index.ts +55 -0
- package/src/beacon/server/lightclient.ts +7 -0
- package/src/beacon/server/lodestar.ts +7 -0
- package/src/beacon/server/node.ts +7 -0
- package/src/beacon/server/proof.ts +7 -0
- package/src/beacon/server/validator.ts +7 -0
- package/src/builder/client.ts +9 -0
- package/src/builder/index.ts +26 -0
- package/src/builder/routes.ts +227 -0
- package/src/builder/server/index.ts +19 -0
- package/src/index.ts +19 -0
- package/src/keymanager/client.ts +9 -0
- package/src/keymanager/index.ts +39 -0
- package/src/keymanager/routes.ts +699 -0
- package/src/keymanager/server/index.ts +19 -0
- package/src/server/index.ts +2 -0
- package/src/utils/client/error.ts +10 -0
- package/src/utils/client/eventSource.ts +7 -0
- package/src/utils/client/format.ts +22 -0
- package/src/utils/client/httpClient.ts +444 -0
- package/src/utils/client/index.ts +6 -0
- package/src/utils/client/method.ts +50 -0
- package/src/utils/client/metrics.ts +9 -0
- package/src/utils/client/request.ts +113 -0
- package/src/utils/client/response.ts +205 -0
- package/src/utils/codecs.ts +143 -0
- package/src/utils/fork.ts +44 -0
- package/src/utils/headers.ts +173 -0
- package/src/utils/httpStatusCode.ts +392 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/metadata.ts +170 -0
- package/src/utils/schema.ts +141 -0
- package/src/utils/serdes.ts +120 -0
- package/src/utils/server/error.ts +9 -0
- package/src/utils/server/handler.ts +149 -0
- package/src/utils/server/index.ts +5 -0
- package/src/utils/server/method.ts +38 -0
- package/src/utils/server/parser.ts +15 -0
- package/src/utils/server/route.ts +45 -0
- package/src/utils/types.ts +161 -0
- package/src/utils/urlFormat.ts +112 -0
- 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,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,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]>};
|