From fda6b4b9a5a527b5108dc1c188f59d4feffa2ca8 Mon Sep 17 00:00:00 2001 From: Filip Pajic <42151917+pajicf@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:48:43 +0100 Subject: [PATCH] core: Onchain Price observation (#4) * onchain price state initialisation * onchain price update listener added * added ticker routes to api * linter fixes * socket support added * added price listeners on app state init --- package.json | 4 +- src/api/v1/definitions/README.md | 31 ++++++ src/api/v1/definitions/tickers.route.d.ts | 48 +++++++++ src/api/v1/index.ts | 8 ++ src/api/v1/middlewares/error.middleware.ts | 31 ++++++ src/api/v1/middlewares/validate.middleware.ts | 19 ++++ src/api/v1/routes/tickers.route.ts | 41 ++++++++ src/api/v1/validators/tickers.validator.ts | 19 ++++ src/entities/ticker.entity.ts | 22 +++++ src/index.ts | 2 + src/jobs/app.jobs.ts | 6 +- src/jobs/price.jobs.ts | 37 ++++++- src/jobs/ticker.jobs.ts | 8 +- src/services/oracle/oracle.service.ts | 44 ++++++++- src/services/oracle/oracle.service.types.ts | 3 + src/services/socket.service.ts | 33 +++++++ src/types/util.types.ts | 12 ++- src/utils/errors.util.ts | 19 +++- yarn.lock | 99 +++++++++++++++++-- 19 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 src/api/v1/definitions/README.md create mode 100644 src/api/v1/definitions/tickers.route.d.ts create mode 100644 src/api/v1/middlewares/error.middleware.ts create mode 100644 src/api/v1/middlewares/validate.middleware.ts create mode 100644 src/api/v1/routes/tickers.route.ts create mode 100644 src/api/v1/validators/tickers.validator.ts create mode 100644 src/entities/ticker.entity.ts create mode 100644 src/services/oracle/oracle.service.types.ts create mode 100644 src/services/socket.service.ts diff --git a/package.json b/package.json index 8ed259c..813a550 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ "dotenv": "^16.3.1", "ethers": "^6.9.0", "express": "^4.18.2", + "express-validator": "^7.0.1", "log4js": "^6.9.1", "node-cron": "^3.0.3", - "redux": "^5.0.0" + "redux": "^5.0.0", + "socket.io": "^4.7.2" } } diff --git a/src/api/v1/definitions/README.md b/src/api/v1/definitions/README.md new file mode 100644 index 0000000..be53a5b --- /dev/null +++ b/src/api/v1/definitions/README.md @@ -0,0 +1,31 @@ +# Route definition declarations guide + +In order to better and/or more strictly type the codebase, every new route +will have to follow these rules: + +1. Every route in /routes folder should have corresponding file inside /definitions + named the same only with .d (definitions) before the .ts file type ending + +2. Inside the routes definitions file, there should be a namespace named the + same as the class inside the routes folder which is being typed. The same + namespace is the default export of that file. + There should also be an enum in which every route/method is defined + +3. Every namespace should implement next generics: + + `ResponseBody` - Returns specified routes response body + + `RequestBody` - Returns specified routes request body + + `RequestQueries` - Returns specified routes query params + + `RequestParams` - Returns specified routes request params + + `Response` - Returns expressjs response object + + `Request` - Returns expressjs request object + + `RouteMethod` - Returns typization for next function + + Every generic should return default empty object if route wasn't implemented + +4. The naming convention is PascalCase/UpperCamelCase and it should look something like this: + - For namespace name + + `{ROUTE_NAME}Definitions` - AuthRouteDefinitions + - For enum name + + `E{ROUTE_NAME}` - EAuthRoute + - For enum values + + `{HTTP_METHOD}{ROUTE_NAME}` - GetMe or PostAuthGithub diff --git a/src/api/v1/definitions/tickers.route.d.ts b/src/api/v1/definitions/tickers.route.d.ts new file mode 100644 index 0000000..2abb469 --- /dev/null +++ b/src/api/v1/definitions/tickers.route.d.ts @@ -0,0 +1,48 @@ +import { EmptyObject, ParamsDictionary } from "../../../types/util.types"; +import { TickerEntity } from "../../../entities/ticker.entity"; +import { IResponseSuccess } from "../../../utils/response.util"; +import { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express"; + + +export enum ETickersRoute { + GetTickerList = "GetTickerList", + GetTicker = "GetTicker", + GetTickerHistory = "GetTickerHistory" +} + +declare namespace TickersRouteDefinitions { + type ResponseBody = + // GET /tickers/[ticker] + T extends ETickersRoute.GetTicker ? TickerEntity : + // GET /tickers + T extends ETickersRoute.GetTickerList ? TickerEntity[] : + // GET /tickers/[ticker]/history + T extends ETickersRoute.GetTickerHistory ? EmptyObject : + EmptyObject + + type RequestBody = // eslint-disable-line @typescript-eslint/no-unused-vars + EmptyObject; + + type RequestQueries = // eslint-disable-line @typescript-eslint/no-unused-vars + EmptyObject + + type RequestParams = + // GET /tickers/[ticker] + T extends ETickersRoute.GetTicker ? TickerParams : + // GET /tickers/[ticker]/history + T extends ETickersRoute.GetTickerHistory ? TickerParams : + EmptyObject; + + type Response = ExpressResponse>> + + type Request = ExpressRequest & ParamsDictionary, IResponseSuccess>, RequestBody, RequestQueries> + + type RouteMethod = (request: Request, response: Response, next: NextFunction) => Promise; + + // PARAMS + type TickerParams = { + tickerSymbol: string; + } +} + +export default TickersRouteDefinitions; \ No newline at end of file diff --git a/src/api/v1/index.ts b/src/api/v1/index.ts index 55651da..2b61343 100644 --- a/src/api/v1/index.ts +++ b/src/api/v1/index.ts @@ -1,8 +1,16 @@ import { Router } from "express"; +import { error } from "./middlewares/error.middleware"; import StatusRoute from "./routes/status.route"; +import TickersRoute from "./routes/tickers.route"; +import TickersValidator from "./validators/tickers.validator"; const v1 = Router(); +v1.get("/tickers/", TickersRoute.getTickerList); +v1.get("/tickers/:tickerSymbol", TickersValidator.validateGetTicker, TickersRoute.getTicker); + v1.get("/status", StatusRoute.getStatus); +v1.use(error); + export default v1; \ No newline at end of file diff --git a/src/api/v1/middlewares/error.middleware.ts b/src/api/v1/middlewares/error.middleware.ts new file mode 100644 index 0000000..57b3781 --- /dev/null +++ b/src/api/v1/middlewares/error.middleware.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from "express"; +import { + CustomValidationError, NotFoundError, InvalidRequestError +} from "../../../utils/errors.util"; +import { APIResponse } from "../../../utils/response.util"; +import logger from "../../../utils/logger.util"; + +export async function error(error: Error, request: Request, response: Response, next: NextFunction) { // eslint-disable-line @typescript-eslint/no-unused-vars + logger.error(error); + + let status: number; + let message: string; + let errors: any | undefined; + + if (error instanceof NotFoundError) { + status = 404; + message = error.message || "Not found"; + } else if (error instanceof InvalidRequestError) { + status = 400; + message = error.message || "Invalid request"; + } else if (error instanceof CustomValidationError) { + status = 422; + message = error.message || "Validation error"; + errors = error.err; + } else { + status = 500; + message = "Server error"; + } + + return response.status(status).json(APIResponse.error(status, message, errors)); +} diff --git a/src/api/v1/middlewares/validate.middleware.ts b/src/api/v1/middlewares/validate.middleware.ts new file mode 100644 index 0000000..06b6f73 --- /dev/null +++ b/src/api/v1/middlewares/validate.middleware.ts @@ -0,0 +1,19 @@ +import { ValidationChain, validationResult } from "express-validator"; +import { NextFunction, RequestHandler } from "express"; +import { CustomValidationError } from "../../../utils/errors.util"; + +async function validateRequest(request?: Request, response?: Response, next?: NextFunction) { + const result = validationResult(request); + const errors = result.mapped(); + + if (!result.isEmpty()) { + return next(new CustomValidationError(errors)); + } else { + return next(); + } +} + +export function val(validationChain: ValidationChain[]): RequestHandler[] { + // @ts-expect-error Type mismatch for validationChain, but actually is fitting + return [...validationChain, validateRequest]; +} diff --git a/src/api/v1/routes/tickers.route.ts b/src/api/v1/routes/tickers.route.ts new file mode 100644 index 0000000..ed9e1fe --- /dev/null +++ b/src/api/v1/routes/tickers.route.ts @@ -0,0 +1,41 @@ +import TickersRouteDefinitions from "../definitions/tickers.route"; +import { ETickersRoute } from "../definitions/tickers.route"; +import { tickerEntityFromReduxState } from "../../../entities/ticker.entity"; +import store from "../../../redux/store"; +import { NotFoundError } from "../../../utils/errors.util"; +import { APIResponse } from "../../../utils/response.util"; + +class TickersRoute { + public static getTicker: TickersRouteDefinitions.RouteMethod = async (request, response, next) => { + try { + const { + tickerSymbol + } = request.params; + + const tickerData = tickerEntityFromReduxState(tickerSymbol, store.getState()); + + if (!tickerData) { + throw new NotFoundError(); + } + + return response.status(200).json(APIResponse.success(tickerData)); + } catch (error) { + next(error); + } + }; + + public static getTickerList: TickersRouteDefinitions.RouteMethod = async (request, response, next) => { + try { + const rootState = store.getState(); + const symbols = rootState.tickers.symbols; + + const tickerList = symbols.map(symbol => tickerEntityFromReduxState(symbol, rootState)); + + return response.status(200).json(APIResponse.success(tickerList)); + } catch (error) { + next(error); + } + }; +} + +export default TickersRoute; \ No newline at end of file diff --git a/src/api/v1/validators/tickers.validator.ts b/src/api/v1/validators/tickers.validator.ts new file mode 100644 index 0000000..014fc59 --- /dev/null +++ b/src/api/v1/validators/tickers.validator.ts @@ -0,0 +1,19 @@ +import TickersRouteDefinitions from "../definitions/tickers.route"; +import { val } from "../middlewares/validate.middleware"; +import { checkSchema } from "express-validator"; +import { ValidatorFields } from "../../../types/util.types"; + +type GetTickerFields = + (keyof TickersRouteDefinitions.TickerParams) + +const getTickerSchema: ValidatorFields = { + tickerSymbol: { + in: ["params"], + errorMessage: "Ticker must be a string", + isString: true + } +}; + +export default class TickersValidator { + public static validateGetTicker = val(checkSchema(getTickerSchema)); +} \ No newline at end of file diff --git a/src/entities/ticker.entity.ts b/src/entities/ticker.entity.ts new file mode 100644 index 0000000..293a9af --- /dev/null +++ b/src/entities/ticker.entity.ts @@ -0,0 +1,22 @@ +import { RootState } from "../redux/redux.types"; +import { Nullable } from "../types/util.types"; + +export type TickerEntity = { + symbol: string; + onchainPrice?: number; + offchainPrice?: number; +} + +export function tickerEntityFromReduxState(tickerSymbol: string, state: RootState): Nullable { + const tickerPrices = state.prices.current[tickerSymbol]; + + if (tickerPrices) { + return { + symbol: tickerSymbol, + onchainPrice: tickerPrices.onchain, + offchainPrice: tickerPrices.offchain + }; + } else { + return undefined; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3069ff1..d82526d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ import router from "./app"; import { createServer, Server } from "http"; import logger from "./utils/logger.util"; +import SocketService from "./services/socket.service"; const server: Server = createServer(router); +export const RootSocket = new SocketService(server); server.listen(router.get("port"), router.get("host"), async () => { process.on("SIGINT", async () => { diff --git a/src/jobs/app.jobs.ts b/src/jobs/app.jobs.ts index 2d26178..e7b3be7 100644 --- a/src/jobs/app.jobs.ts +++ b/src/jobs/app.jobs.ts @@ -1,10 +1,14 @@ import { initTickerState, setupTickerFetchingJob } from "./ticker.jobs"; -import { setupOffchainPriceFetchingJob } from "./price.jobs"; +import { initOnchainPriceState, setupOffchainPriceFetchingJob } from "./price.jobs"; import logger from "../utils/logger.util"; +import store from "../redux/store"; const initAppState = async () => { logger.log("Initialising the app state"); await initTickerState(); + + const symbols = store.getState().tickers.symbols; + await initOnchainPriceState(symbols); }; export const initApp = async () => { diff --git a/src/jobs/price.jobs.ts b/src/jobs/price.jobs.ts index 2c5baf9..e382847 100644 --- a/src/jobs/price.jobs.ts +++ b/src/jobs/price.jobs.ts @@ -2,13 +2,15 @@ import CoinGeckoService from "../services/coingecko/coingecko.service"; import CronService from "../services/cron.service"; import store from "../redux/store"; import { getCoingeckoIdByChainlinkTicker } from "../constants/coingecko"; -import { setCurrentOffchainPrice } from "../redux/prices/prices.redux.actions"; +import { setCurrentOffchainPrice, setCurrentOnchainPrice } from "../redux/prices/prices.redux.actions"; import OracleService from "../services/oracle/oracle.service"; +import logger from "../utils/logger.util"; +import { RootSocket } from "../index"; -export const setupOffchainPriceFetchingJob = async () => { - const coinGecko = new CoinGeckoService(); - const oracleService = new OracleService(); +const coinGecko = new CoinGeckoService(); +const oracleService = new OracleService(); +export const setupOffchainPriceFetchingJob = async () => { CronService.scheduleRecurringJob(async () => { const tickerSymbols = store.getState().tickers.symbols; const tickerCoinGeckoIds = tickerSymbols.map(getCoingeckoIdByChainlinkTicker); @@ -27,4 +29,31 @@ export const setupOffchainPriceFetchingJob = async () => { } catch (err) { console.log(err);} } }); +}; + +const updateReduxTickerOnchainPrice = (tickerSymbol: string, tickerPrice: number) => { + store.dispatch(setCurrentOnchainPrice(tickerSymbol, tickerPrice)); +}; + +export const initOnchainPriceState = async (symbols: string[]) => { + for (let i = 0; i < symbols.length; i++) { + const tickerSymbol = symbols[i]; + const newPrice = await oracleService.getOnchainPrice(tickerSymbol); + updateReduxTickerOnchainPrice(tickerSymbol, newPrice); + } +}; + +const isJobActive: Map = new Map(); +export const setupOnchainPriceFetchingJobFor = async (tickerSymbol: string) => { + if (isJobActive.get(tickerSymbol)) { + return; + } else { + isJobActive.set(tickerSymbol, true); + } + + logger.log("Setting up the onchain Price fetching observer for ", tickerSymbol); + await oracleService.listenForOnchainPriceUpdates(tickerSymbol, (tickerPriceData) => { + updateReduxTickerOnchainPrice(tickerSymbol, tickerPriceData.newPrice); + RootSocket.emitEvent("TickerPriceUpdated", [tickerSymbol, tickerPriceData.newPrice]); + }); }; \ No newline at end of file diff --git a/src/jobs/ticker.jobs.ts b/src/jobs/ticker.jobs.ts index 793a89a..8a66a12 100644 --- a/src/jobs/ticker.jobs.ts +++ b/src/jobs/ticker.jobs.ts @@ -3,6 +3,7 @@ import store from "../redux/store"; import { addTicker } from "../redux/tickers/tickers.redux.actions"; import { TickerRegistryData } from "../services/tickers/tickers.service.types"; import logger from "../utils/logger.util"; +import { setupOnchainPriceFetchingJobFor } from "./price.jobs"; const tickersService = new TickersService(); @@ -17,11 +18,16 @@ export const initTickerState = async () => { tickers.forEach(tickerData => { updateReduxTickerState(tickerData); symbols.push(tickerData.tickerSymbol); + setupOnchainPriceFetchingJobFor(tickerData.tickerSymbol); }); + logger.log("Tickers found: ", symbols); }; export const setupTickerFetchingJob = async () => { logger.log("Setting up the Ticker Fetching observer"); - tickersService.listenForTickerUpdates(updateReduxTickerState).then(); + tickersService.listenForTickerUpdates(tickerData => { + updateReduxTickerState(tickerData); + setupOnchainPriceFetchingJobFor(tickerData.tickerSymbol); + }).then(); }; \ No newline at end of file diff --git a/src/services/oracle/oracle.service.ts b/src/services/oracle/oracle.service.ts index 654c272..171e33a 100644 --- a/src/services/oracle/oracle.service.ts +++ b/src/services/oracle/oracle.service.ts @@ -1,18 +1,20 @@ import Web3Service from "../web3.service"; import { CONFIG } from "../../config"; import logger from "../../utils/logger.util"; +import { TypedEventLog } from "../../contracts/common"; +import { TickerPriceData } from "./oracle.service.types"; class OracleService { private _tickerPriceContract; + private readonly _numberOfChainlinkDecimals = 8; constructor() { this._tickerPriceContract = Web3Service.getTickerPriceStorageContract(CONFIG.TICKER_PRICE_STORAGE, true); } public async updateOnchainPrice(ticker: string, newPrice: number) { - const numberOfChainlinkDecimals = 8; // @TODO Update to use BigInt - const parsedPrice = Math.trunc(newPrice * 10**numberOfChainlinkDecimals); + const parsedPrice = Math.trunc(newPrice * 10**this._numberOfChainlinkDecimals); logger.log("Trying to update onchain price of ", ticker, " to ", newPrice); const willRevert = await this.isPriceUpdateGoingToRevert(ticker, parsedPrice); @@ -36,6 +38,44 @@ class OracleService { return true; } } + + public async getOnchainPrice(tickerSymbol: string): Promise { + const eventFilter = this._tickerPriceContract.filters.TickerPriceUpdated(tickerSymbol); + const results = await this._tickerPriceContract.queryFilter(eventFilter); + + if (results.length > 0 ) { + const latestEvent = this.parseTickerPriceUpdatedEvent(results[results.length-1]); + return latestEvent.newPrice; + } else { + return 0; + } + } + + public async listenForOnchainPriceUpdates(tickerSymbol: string, onPriceUpdate: (tickerPriceData: TickerPriceData) => void) { + const startBlockNumber = await Web3Service.provider.getBlockNumber(); + const eventFilter = this._tickerPriceContract.filters.TickerPriceUpdated(tickerSymbol); + + await this._tickerPriceContract.on(eventFilter, (data) => { + if (data.log.blockNumber <= startBlockNumber) { + return; + } + + const parsedEvent = this.parseTickerPriceUpdatedEvent(data); + logger.log(`${tickerSymbol} price updated on-chain to ${parsedEvent.newPrice}`); + + onPriceUpdate(parsedEvent); + }); + } + + private parseTickerPriceUpdatedEvent(event: TypedEventLog): TickerPriceData { + const args = event.args as [string, number]; + const newPrice = args[1]; + const parsedPrice = (Number(newPrice) / (10**this._numberOfChainlinkDecimals)); + + return { + newPrice: parsedPrice + }; + } } export default OracleService; \ No newline at end of file diff --git a/src/services/oracle/oracle.service.types.ts b/src/services/oracle/oracle.service.types.ts new file mode 100644 index 0000000..e182eda --- /dev/null +++ b/src/services/oracle/oracle.service.types.ts @@ -0,0 +1,3 @@ +export type TickerPriceData = { + newPrice: number; +} \ No newline at end of file diff --git a/src/services/socket.service.ts b/src/services/socket.service.ts new file mode 100644 index 0000000..900e768 --- /dev/null +++ b/src/services/socket.service.ts @@ -0,0 +1,33 @@ +import { Server } from "socket.io"; +import { Server as ServerType } from "http"; +import logger from "../utils/logger.util"; + +class SocketService { + private _socketio: Server; + + constructor(httpServer_: ServerType) { + this._socketio = new Server(httpServer_, { + cors: { + origin: "*" + } + }); + + this.setupGreetingListeners(); + } + + private setupGreetingListeners() { + this._socketio.on("connection", (socket) => { + logger.log(`New connection on socket, with id ${socket.id}`); + + socket.on("disconnect", () => { + logger.log(`Connection with id ${socket.id} disconnected`); + }); + }); + } + + public emitEvent(eventName: string, ...eventArgs: any) { + this._socketio.emit("PriceUpdated", eventArgs); + } +} + +export default SocketService; \ No newline at end of file diff --git a/src/types/util.types.ts b/src/types/util.types.ts index 6ce9562..97cbd58 100644 --- a/src/types/util.types.ts +++ b/src/types/util.types.ts @@ -1,4 +1,6 @@ // Used for typing some dynamic object, first param defines the value type, second the key type +import { ParamSchema } from "express-validator"; + export type DynamicObject< Value = any, Key extends (string | number) = string, @@ -7,4 +9,12 @@ export type DynamicObject< {[K in Key]: Value} : {[K in Key]?: Value}; -export type AllKeysRequired = true; \ No newline at end of file +export type AllKeysRequired = true; + +export type EmptyObject = object; + +export type ParamsDictionary = {[key: string]: string}; + +export type Nullable = T | undefined | null; + +export type ValidatorFields = DynamicObject; diff --git a/src/utils/errors.util.ts b/src/utils/errors.util.ts index 0296856..5bb19b7 100644 --- a/src/utils/errors.util.ts +++ b/src/utils/errors.util.ts @@ -1,3 +1,20 @@ +export class NotFoundError extends Error {} + +export class InvalidRequestError extends Error {} + export class AuthenticationError extends Error {} -export class AuthorizationError extends Error {} \ No newline at end of file +export class AuthorizationError extends Error {} + +export class CustomValidationError extends Error { + private readonly _err: any; + + constructor(errors: any) { + super(); + this._err = errors; + } + + get err(): any { + return this._err; + } +} diff --git a/yarn.lock b/yarn.lock index 8b55430..56e7a26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,6 +121,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" + integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -164,7 +169,12 @@ dependencies: "@types/node" "*" -"@types/cors@^2.8.17": +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + +"@types/cors@^2.8.12", "@types/cors@^2.8.17": version "2.8.17" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== @@ -216,7 +226,7 @@ resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg== -"@types/node@*": +"@types/node@*", "@types/node@>=10.0.0": version "20.10.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.4.tgz#b246fd84d55d5b1b71bf51f964bd514409347198" integrity sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg== @@ -360,7 +370,7 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -474,6 +484,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64id@2.0.0, base64id@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -659,7 +674,12 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cors@^2.8.5: +cookie@~0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cors@^2.8.5, cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -693,7 +713,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -768,6 +788,27 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + +engine.io@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc" + integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.11.0" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -891,6 +932,14 @@ ethers@^6.9.0: tslib "2.4.0" ws "8.5.0" +express-validator@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-7.0.1.tgz#435fac6b5fa838763f78eca05d2206317b92106e" + integrity sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA== + dependencies: + lodash "^4.17.21" + validator "^13.9.0" + express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -1351,7 +1400,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1798,6 +1847,34 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +socket.io-adapter@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12" + integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== + dependencies: + ws "~8.11.0" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + +socket.io@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.2.tgz#22557d76c3f3ca48f82e73d68b7add36a22df002" + integrity sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + cors "~2.8.5" + debug "~4.3.2" + engine.io "~6.5.2" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -2014,6 +2091,11 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +validator@^13.9.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -2044,6 +2126,11 @@ ws@8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +ws@~8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"