|
|
@@ -1,16 +1,17 @@
|
|
|
-import express, {Express} from "express";
|
|
|
+import express, { Express } from "express";
|
|
|
import cors from "cors";
|
|
|
import morgan from "morgan";
|
|
|
import responseTime from "response-time";
|
|
|
import { Request, Response, NextFunction } from "express";
|
|
|
-import { PriceFeedPriceInfo } from "./listen";
|
|
|
+import { PriceStore } from "./listen";
|
|
|
import { logger } from "./logging";
|
|
|
import { PromClient } from "./promClient";
|
|
|
import { DurationInMs, DurationInSec } from "./helpers";
|
|
|
import { StatusCodes } from "http-status-codes";
|
|
|
import { validate, ValidationError, Joi, schema } from "express-validation";
|
|
|
|
|
|
-const MORGAN_LOG_FORMAT = ':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
|
|
|
+const MORGAN_LOG_FORMAT =
|
|
|
+ ':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
|
|
|
' :status :res[content-length] :response-time ms ":referrer" ":user-agent"';
|
|
|
|
|
|
export class RestException extends Error {
|
|
|
@@ -23,20 +24,25 @@ export class RestException extends Error {
|
|
|
}
|
|
|
|
|
|
static PriceFeedIdNotFound(notFoundIds: string[]): RestException {
|
|
|
- return new RestException(StatusCodes.BAD_REQUEST, `Price Feeds with ids ${notFoundIds.join(', ')} not found`);
|
|
|
+ return new RestException(
|
|
|
+ StatusCodes.BAD_REQUEST,
|
|
|
+ `Price Feeds with ids ${notFoundIds.join(", ")} not found`
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export class RestAPI {
|
|
|
private port: number;
|
|
|
- private priceFeedVaaInfo: PriceFeedPriceInfo;
|
|
|
+ private priceFeedVaaInfo: PriceStore;
|
|
|
private isReady: (() => boolean) | undefined;
|
|
|
private promClient: PromClient | undefined;
|
|
|
|
|
|
- constructor(config: { port: number; },
|
|
|
- priceFeedVaaInfo: PriceFeedPriceInfo,
|
|
|
+ constructor(
|
|
|
+ config: { port: number },
|
|
|
+ priceFeedVaaInfo: PriceStore,
|
|
|
isReady?: () => boolean,
|
|
|
- promClient?: PromClient) {
|
|
|
+ promClient?: PromClient
|
|
|
+ ) {
|
|
|
this.port = config.port;
|
|
|
this.priceFeedVaaInfo = priceFeedVaaInfo;
|
|
|
this.isReady = isReady;
|
|
|
@@ -51,101 +57,128 @@ export class RestAPI {
|
|
|
const winstonStream = {
|
|
|
write: (text: string) => {
|
|
|
logger.info(text);
|
|
|
- }
|
|
|
+ },
|
|
|
};
|
|
|
|
|
|
app.use(morgan(MORGAN_LOG_FORMAT, { stream: winstonStream }));
|
|
|
|
|
|
- app.use(responseTime((req: Request, res: Response, time: DurationInMs) => {
|
|
|
- if (res.statusCode !== StatusCodes.NOT_FOUND) {
|
|
|
- this.promClient?.addResponseTime(req.path, res.statusCode, time);
|
|
|
- }
|
|
|
- }))
|
|
|
+ app.use(
|
|
|
+ responseTime((req: Request, res: Response, time: DurationInMs) => {
|
|
|
+ if (res.statusCode !== StatusCodes.NOT_FOUND) {
|
|
|
+ this.promClient?.addResponseTime(req.path, res.statusCode, time);
|
|
|
+ }
|
|
|
+ })
|
|
|
+ );
|
|
|
|
|
|
let endpoints: string[] = [];
|
|
|
-
|
|
|
+
|
|
|
const latestVaasInputSchema: schema = {
|
|
|
query: Joi.object({
|
|
|
- ids: Joi.array().items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/)).required()
|
|
|
- }).required()
|
|
|
- }
|
|
|
- app.get("/latest_vaas", validate(latestVaasInputSchema), (req: Request, res: Response) => {
|
|
|
- let priceIds = req.query.ids as string[];
|
|
|
-
|
|
|
- // Multiple price ids might share same vaa, we use sequence number as
|
|
|
- // key of a vaa and deduplicate using a map of seqnum to vaa bytes.
|
|
|
- let vaaMap = new Map<number, string>();
|
|
|
-
|
|
|
- let notFoundIds: string[] = [];
|
|
|
-
|
|
|
- for (let id of priceIds) {
|
|
|
- if (id.startsWith("0x")) {
|
|
|
- id = id.substring(2);
|
|
|
+ ids: Joi.array()
|
|
|
+ .items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/))
|
|
|
+ .required(),
|
|
|
+ }).required(),
|
|
|
+ };
|
|
|
+ app.get(
|
|
|
+ "/latest_vaas",
|
|
|
+ validate(latestVaasInputSchema),
|
|
|
+ (req: Request, res: Response) => {
|
|
|
+ let priceIds = req.query.ids as string[];
|
|
|
+
|
|
|
+ // Multiple price ids might share same vaa, we use sequence number as
|
|
|
+ // key of a vaa and deduplicate using a map of seqnum to vaa bytes.
|
|
|
+ let vaaMap = new Map<number, string>();
|
|
|
+
|
|
|
+ let notFoundIds: string[] = [];
|
|
|
+
|
|
|
+ for (let id of priceIds) {
|
|
|
+ if (id.startsWith("0x")) {
|
|
|
+ id = id.substring(2);
|
|
|
+ }
|
|
|
+
|
|
|
+ let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
|
|
|
+
|
|
|
+ if (latestPriceInfo === undefined) {
|
|
|
+ notFoundIds.push(id);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const freshness: DurationInSec =
|
|
|
+ new Date().getTime() / 1000 - latestPriceInfo.receiveTime;
|
|
|
+ this.promClient?.addApiRequestsPriceFreshness(
|
|
|
+ req.path,
|
|
|
+ id,
|
|
|
+ freshness
|
|
|
+ );
|
|
|
+
|
|
|
+ vaaMap.set(latestPriceInfo.seqNum, latestPriceInfo.vaaBytes);
|
|
|
}
|
|
|
|
|
|
- let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
|
|
|
-
|
|
|
- if (latestPriceInfo === undefined) {
|
|
|
- notFoundIds.push(id);
|
|
|
- continue;
|
|
|
+ if (notFoundIds.length > 0) {
|
|
|
+ throw RestException.PriceFeedIdNotFound(notFoundIds);
|
|
|
}
|
|
|
|
|
|
- const freshness: DurationInSec = (new Date).getTime() / 1000 - latestPriceInfo.receiveTime;
|
|
|
- this.promClient?.addApiRequestsPriceFreshness(req.path, id, freshness);
|
|
|
-
|
|
|
- vaaMap.set(latestPriceInfo.seqNum, latestPriceInfo.vaaBytes);
|
|
|
- }
|
|
|
+ const jsonResponse = Array.from(vaaMap.values(), (vaaBytes) =>
|
|
|
+ Buffer.from(vaaBytes, "binary").toString("base64")
|
|
|
+ );
|
|
|
|
|
|
- if (notFoundIds.length > 0) {
|
|
|
- throw RestException.PriceFeedIdNotFound(notFoundIds);
|
|
|
+ res.json(jsonResponse);
|
|
|
}
|
|
|
-
|
|
|
- const jsonResponse = Array.from(vaaMap.values(),
|
|
|
- vaaBytes => Buffer.from(vaaBytes, 'binary').toString('base64')
|
|
|
- );
|
|
|
-
|
|
|
- res.json(jsonResponse);
|
|
|
- });
|
|
|
- endpoints.push("latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..");
|
|
|
+ );
|
|
|
+ endpoints.push(
|
|
|
+ "latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&.."
|
|
|
+ );
|
|
|
|
|
|
const latestPriceFeedsInputSchema: schema = {
|
|
|
query: Joi.object({
|
|
|
- ids: Joi.array().items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/)).required()
|
|
|
- }).required()
|
|
|
- }
|
|
|
- app.get("/latest_price_feeds", validate(latestPriceFeedsInputSchema), (req: Request, res: Response) => {
|
|
|
- let priceIds = req.query.ids as string[];
|
|
|
+ ids: Joi.array()
|
|
|
+ .items(Joi.string().regex(/^(0x)?[a-f0-9]{64}$/))
|
|
|
+ .required(),
|
|
|
+ }).required(),
|
|
|
+ };
|
|
|
+ app.get(
|
|
|
+ "/latest_price_feeds",
|
|
|
+ validate(latestPriceFeedsInputSchema),
|
|
|
+ (req: Request, res: Response) => {
|
|
|
+ let priceIds = req.query.ids as string[];
|
|
|
|
|
|
- let responseJson = [];
|
|
|
+ let responseJson = [];
|
|
|
|
|
|
- let notFoundIds: string[] = [];
|
|
|
+ let notFoundIds: string[] = [];
|
|
|
|
|
|
- for (let id of priceIds) {
|
|
|
- if (id.startsWith("0x")) {
|
|
|
- id = id.substring(2);
|
|
|
- }
|
|
|
+ for (let id of priceIds) {
|
|
|
+ if (id.startsWith("0x")) {
|
|
|
+ id = id.substring(2);
|
|
|
+ }
|
|
|
|
|
|
- let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
|
|
|
+ let latestPriceInfo = this.priceFeedVaaInfo.getLatestPriceInfo(id);
|
|
|
|
|
|
- if (latestPriceInfo === undefined) {
|
|
|
- notFoundIds.push(id);
|
|
|
- continue;
|
|
|
- }
|
|
|
+ if (latestPriceInfo === undefined) {
|
|
|
+ notFoundIds.push(id);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- const freshness: DurationInSec = (new Date).getTime() / 1000 - latestPriceInfo.receiveTime;
|
|
|
- this.promClient?.addApiRequestsPriceFreshness(req.path, id, freshness);
|
|
|
+ const freshness: DurationInSec =
|
|
|
+ new Date().getTime() / 1000 - latestPriceInfo.receiveTime;
|
|
|
+ this.promClient?.addApiRequestsPriceFreshness(
|
|
|
+ req.path,
|
|
|
+ id,
|
|
|
+ freshness
|
|
|
+ );
|
|
|
|
|
|
- responseJson.push(latestPriceInfo.priceFeed.toJson());
|
|
|
- }
|
|
|
-
|
|
|
- if (notFoundIds.length > 0) {
|
|
|
- throw RestException.PriceFeedIdNotFound(notFoundIds);
|
|
|
- }
|
|
|
+ responseJson.push(latestPriceInfo.priceFeed.toJson());
|
|
|
+ }
|
|
|
|
|
|
- res.json(responseJson);
|
|
|
- });
|
|
|
- endpoints.push("latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..");
|
|
|
+ if (notFoundIds.length > 0) {
|
|
|
+ throw RestException.PriceFeedIdNotFound(notFoundIds);
|
|
|
+ }
|
|
|
|
|
|
+ res.json(responseJson);
|
|
|
+ }
|
|
|
+ );
|
|
|
+ endpoints.push(
|
|
|
+ "latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&.."
|
|
|
+ );
|
|
|
|
|
|
app.get("/ready", (_, res: Response) => {
|
|
|
if (this.isReady!()) {
|
|
|
@@ -154,19 +187,16 @@ export class RestAPI {
|
|
|
res.sendStatus(StatusCodes.SERVICE_UNAVAILABLE);
|
|
|
}
|
|
|
});
|
|
|
- endpoints.push('ready');
|
|
|
+ endpoints.push("ready");
|
|
|
|
|
|
app.get("/live", (_, res: Response) => {
|
|
|
res.sendStatus(StatusCodes.OK);
|
|
|
});
|
|
|
endpoints.push("live");
|
|
|
|
|
|
+ app.get("/", (_, res: Response) => res.json(endpoints));
|
|
|
|
|
|
- app.get("/", (_, res: Response) =>
|
|
|
- res.json(endpoints)
|
|
|
- );
|
|
|
-
|
|
|
- app.use(function(err: any, _: Request, res: Response, next: NextFunction) {
|
|
|
+ app.use(function (err: any, _: Request, res: Response, next: NextFunction) {
|
|
|
if (err instanceof ValidationError) {
|
|
|
return res.status(err.statusCode).json(err);
|
|
|
}
|
|
|
@@ -174,9 +204,9 @@ export class RestAPI {
|
|
|
if (err instanceof RestException) {
|
|
|
return res.status(err.statusCode).json(err);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return next(err);
|
|
|
- })
|
|
|
+ });
|
|
|
|
|
|
return app;
|
|
|
}
|