@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
366 lines (340 loc) • 10.7 kB
text/typescript
import {ContainerType, OptionalType, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {ArrayOf, StringType, fulu, ssz, stringType} from "@lodestar/types";
import {
EmptyArgs,
EmptyMeta,
EmptyMetaCodec,
EmptyRequest,
EmptyRequestCodec,
EmptyResponseCodec,
EmptyResponseData,
JsonOnlyResponseCodec,
} from "../../utils/codecs.js";
import {HttpStatusCode} from "../../utils/httpStatusCode.js";
import {Endpoint, RouteDefinitions, Schema} from "../../utils/index.js";
import {WireFormat} from "../../utils/wireFormat.js";
// TODO: Workaround for tsgo import-elision bug: ensure this is treated as a runtime value.
// https://github.com/microsoft/typescript-go/issues/2212
void HttpStatusCode;
export const NetworkIdentityType = new ContainerType(
{
/** Cryptographic hash of a peer’s public key. [Read more](https://docs.libp2p.io/concepts/peer-id/) */
peerId: stringType,
/** Ethereum node record. [Read more](https://eips.ethereum.org/EIPS/eip-778) */
enr: stringType,
p2pAddresses: ArrayOf(stringType),
discoveryAddresses: ArrayOf(stringType),
// TODO Fulu: replace with `ssz.fulu.Metadata` once `custody_group_count` is more widely supported
/** Based on Ethereum Consensus [Metadata object](https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md#metadata) */
metadata: ssz.altair.Metadata,
},
{jsonCase: "eth2"}
);
export const PeerCountType = new ContainerType(
{
disconnected: ssz.UintNum64,
connecting: ssz.UintNum64,
connected: ssz.UintNum64,
disconnecting: ssz.UintNum64,
},
{jsonCase: "eth2"}
);
export const SyncingStatusType = new ContainerType(
{
/** Head slot node is trying to reach */
headSlot: ssz.Slot,
/** How many slots node needs to process to reach head. 0 if synced. */
syncDistance: ssz.Slot,
/** Set to true if the node is syncing, false if the node is synced. */
isSyncing: ssz.Boolean,
/** Set to true if the node is optimistically tracking head. */
isOptimistic: ssz.Boolean,
/** Set to true if the connected el client is offline */
elOffline: ssz.Boolean,
},
{jsonCase: "eth2"}
);
export type NetworkIdentity = ValueOf<typeof NetworkIdentityType> & {
metadata: Partial<fulu.Metadata>;
};
export type PeerState = "disconnected" | "connecting" | "connected" | "disconnecting";
export type PeerDirection = "inbound" | "outbound";
export const NodePeerType = new ContainerType(
{
peerId: stringType,
enr: new OptionalType(stringType),
lastSeenP2pAddress: stringType,
state: new StringType<PeerState>(),
// the spec does not specify direction for a disconnected peer, lodestar uses null in that case
direction: new OptionalType(new StringType<PeerDirection>()),
},
{jsonCase: "eth2"}
);
export const NodePeersType = ArrayOf(NodePeerType);
export type NodePeer = ValueOf<typeof NodePeerType>;
export type NodePeers = ValueOf<typeof NodePeersType>;
export type PeersMeta = {count: number};
export type PeerCount = ValueOf<typeof PeerCountType>;
export type FilterGetPeers = {
state?: PeerState[];
direction?: PeerDirection[];
};
export type SyncingStatus = ValueOf<typeof SyncingStatusType>;
export enum NodeHealth {
READY = HttpStatusCode.OK,
SYNCING = HttpStatusCode.PARTIAL_CONTENT,
NOT_INITIALIZED_OR_ISSUES = HttpStatusCode.SERVICE_UNAVAILABLE,
}
/**
* Client code as defined in https://github.com/ethereum/execution-apis/blob/dc4dbca37ef8697d782f431af19120beaf5517f5/src/engine/identification.md#clientcode
* ClientCode.XX is dedicated to other clients which do not have their own code
*/
export enum ClientCode {
BU = "BU", // besu
EJ = "EJ", // ethereumJS
EG = "EG", // erigon
GE = "GE", // go-ethereum
GR = "GR", // grandine
LH = "LH", // lighthouse
LS = "LS", // lodestar
NM = "NM", // nethermind
NB = "NB", // nimbus
TE = "TE", // trin-execution
TK = "TK", // teku
PM = "PM", // prysm
RH = "RH", // reth
XX = "XX", // unknown
}
/**
* A structure which uniquely identifies a client implementation and its version.
* Mirrors the client version specification in the Engine API.
* https://github.com/ethereum/execution-apis/blob/dc4dbca37ef8697d782f431af19120beaf5517f5/src/engine/identification.md
*/
export type ClientVersion = {
code: ClientCode;
name: string;
version: string;
commit: string;
};
export type NodeVersionV2 = {
beaconNode: ClientVersion;
executionClient?: ClientVersion;
};
/**
* Read information about the beacon node.
*/
export type Endpoints = {
/**
* Get node network identity
* Retrieves data about the node's network presence
*/
getNetworkIdentity: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
NetworkIdentity,
EmptyMeta
>;
/**
* Get node network peers
* Retrieves data about the node's network peers. By default this returns all peers. Multiple query params are combined using AND conditions
*/
getPeers: Endpoint<
"GET",
FilterGetPeers,
{query: {state?: PeerState[]; direction?: PeerDirection[]}},
NodePeers,
PeersMeta
>;
/**
* Get peer
* Retrieves data about the given peer
*/
getPeer: Endpoint<
// ⏎
"GET",
{peerId: string},
{params: {peer_id: string}},
NodePeer,
EmptyMeta
>;
/**
* Get peer count
* Retrieves number of known peers.
*/
getPeerCount: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
PeerCount,
EmptyMeta
>;
/**
* Get version string of the running beacon node.
* Requests that the beacon node identify information about its implementation in a format similar to a [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3) field.
*/
getNodeVersion: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
{version: string},
EmptyMeta
>;
/**
* Get version information for the beacon node and execution client.
* Retrieves structured information about the version of the beacon node and its attached execution client
* in the same format as used on the
* [Engine API](https://github.com/ethereum/execution-apis/blob/dc4dbca37ef8697d782f431af19120beaf5517f5/src/engine/identification.md).
*
* Version information about the execution client may not be available at all times and is therefore optional.
*
* If the beacon node receives multiple values from `engine_getClientVersionV1`,
* the first value should be returned on this endpoint.
*/
getNodeVersionV2: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
NodeVersionV2,
EmptyMeta
>;
/**
* Get node syncing status
* Requests the beacon node to describe if it's currently syncing or not, and if it is, what block it is up to.
*/
getSyncingStatus: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
SyncingStatus,
EmptyMeta
>;
/**
* Get health check
* Returns node health status in http status codes. Useful for load balancers.
*/
getHealth: Endpoint<
// ⏎
"GET",
{syncingStatus?: number},
{query: {syncing_status?: number}},
EmptyResponseData,
EmptyMeta
>;
};
export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
getNetworkIdentity: {
url: "/eth/v1/node/identity",
method: "GET",
req: EmptyRequestCodec,
resp: {
onlySupport: WireFormat.json,
// TODO Fulu: clean this up
data: {
...JsonOnlyResponseCodec.data,
toJson: (data) => {
const json = NetworkIdentityType.toJson(data);
const {custodyGroupCount} = data.metadata;
(json as {metadata: {custody_group_count: string | undefined}}).metadata.custody_group_count =
custodyGroupCount !== undefined ? String(custodyGroupCount) : undefined;
return json;
},
fromJson: (json) => {
const data = NetworkIdentityType.fromJson(json);
const {
metadata: {custody_group_count},
} = json as {metadata: {custody_group_count: string | undefined}};
(data.metadata as Partial<fulu.Metadata>).custodyGroupCount =
custody_group_count !== undefined ? parseInt(custody_group_count) : undefined;
return data;
},
},
meta: EmptyMetaCodec,
},
},
getPeers: {
url: "/eth/v1/node/peers",
method: "GET",
req: {
writeReq: ({state, direction}) => ({query: {state, direction}}),
parseReq: ({query}) => ({state: query.state, direction: query.direction}),
schema: {query: {state: Schema.StringArray, direction: Schema.StringArray}},
},
resp: {
data: NodePeersType,
meta: {
toJson: (d) => d,
fromJson: (d) => ({count: (d as PeersMeta).count}),
toHeadersObject: () => ({}),
fromHeaders: () => ({}) as PeersMeta,
},
transform: {
toResponse: (data, meta) => ({data, meta}),
fromResponse: (resp) => resp as {data: NodePeer[]; meta: PeersMeta},
},
onlySupport: WireFormat.json,
},
},
getPeer: {
url: "/eth/v1/node/peers/{peer_id}",
method: "GET",
req: {
writeReq: ({peerId}) => ({params: {peer_id: peerId}}),
parseReq: ({params}) => ({peerId: params.peer_id}),
schema: {params: {peer_id: Schema.StringRequired}},
},
resp: {
data: NodePeerType,
meta: EmptyMetaCodec,
onlySupport: WireFormat.json,
},
},
getPeerCount: {
url: "/eth/v1/node/peer_count",
method: "GET",
req: EmptyRequestCodec,
resp: {
data: PeerCountType,
meta: EmptyMetaCodec,
},
},
getNodeVersion: {
url: "/eth/v1/node/version",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getNodeVersionV2: {
url: "/eth/v2/node/version",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
getSyncingStatus: {
url: "/eth/v1/node/syncing",
method: "GET",
req: EmptyRequestCodec,
resp: {
data: SyncingStatusType,
meta: EmptyMetaCodec,
},
},
getHealth: {
url: "/eth/v1/node/health",
method: "GET",
req: {
writeReq: ({syncingStatus}) => ({query: {syncing_status: syncingStatus}}),
parseReq: ({query}) => ({syncingStatus: query.syncing_status}),
schema: {query: {syncing_status: Schema.Uint}},
},
resp: EmptyResponseCodec,
},
};
}