diff --git a/src/api/v1/definitions/tickers.route.d.ts b/src/api/v1/definitions/tickers.route.d.ts index 2abb469..4e55929 100644 --- a/src/api/v1/definitions/tickers.route.d.ts +++ b/src/api/v1/definitions/tickers.route.d.ts @@ -1,5 +1,5 @@ import { EmptyObject, ParamsDictionary } from "../../../types/util.types"; -import { TickerEntity } from "../../../entities/ticker.entity"; +import { TickerEntity, TickerHistoryEntity } from "../../../entities/ticker.entity"; import { IResponseSuccess } from "../../../utils/response.util"; import { NextFunction, Request as ExpressRequest, Response as ExpressResponse } from "express"; @@ -17,14 +17,15 @@ declare namespace TickersRouteDefinitions { // GET /tickers T extends ETickersRoute.GetTickerList ? TickerEntity[] : // GET /tickers/[ticker]/history - T extends ETickersRoute.GetTickerHistory ? EmptyObject : + T extends ETickersRoute.GetTickerHistory ? TickerHistoryEntity : EmptyObject type RequestBody = // eslint-disable-line @typescript-eslint/no-unused-vars EmptyObject; - type RequestQueries = // eslint-disable-line @typescript-eslint/no-unused-vars - EmptyObject + type RequestQueries = + T extends ETickersRoute.GetTickerHistory ? TickerPriceHistoryQueries : + EmptyObject; type RequestParams = // GET /tickers/[ticker] @@ -43,6 +44,12 @@ declare namespace TickersRouteDefinitions { type TickerParams = { tickerSymbol: string; } + + // QUERY + type TickerPriceHistoryQueries = { + from: number, + to: number + } } export default TickersRouteDefinitions; \ No newline at end of file diff --git a/src/api/v1/index.ts b/src/api/v1/index.ts index 2b61343..b59c252 100644 --- a/src/api/v1/index.ts +++ b/src/api/v1/index.ts @@ -8,6 +8,7 @@ const v1 = Router(); v1.get("/tickers/", TickersRoute.getTickerList); v1.get("/tickers/:tickerSymbol", TickersValidator.validateGetTicker, TickersRoute.getTicker); +v1.get("/tickers/:tickerSymbol/history", TickersValidator.validateGetTickerPriceHistory, TickersRoute.getTickerPriceHistory); v1.get("/status", StatusRoute.getStatus); diff --git a/src/api/v1/middlewares/error.middleware.ts b/src/api/v1/middlewares/error.middleware.ts index 57b3781..88a5c62 100644 --- a/src/api/v1/middlewares/error.middleware.ts +++ b/src/api/v1/middlewares/error.middleware.ts @@ -6,8 +6,6 @@ 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; @@ -23,6 +21,7 @@ export async function error(error: Error, request: Request, response: Response, message = error.message || "Validation error"; errors = error.err; } else { + logger.error(error); status = 500; message = "Server error"; } diff --git a/src/api/v1/routes/tickers.route.ts b/src/api/v1/routes/tickers.route.ts index ed9e1fe..a2409b1 100644 --- a/src/api/v1/routes/tickers.route.ts +++ b/src/api/v1/routes/tickers.route.ts @@ -4,6 +4,8 @@ import { tickerEntityFromReduxState } from "../../../entities/ticker.entity"; import store from "../../../redux/store"; import { NotFoundError } from "../../../utils/errors.util"; import { APIResponse } from "../../../utils/response.util"; +import CoingeckoService from "../../../services/coingecko/coingecko.service"; +import { getCoingeckoIdByChainlinkTicker } from "../../../constants/coingecko"; class TickersRoute { public static getTicker: TickersRouteDefinitions.RouteMethod = async (request, response, next) => { @@ -36,6 +38,29 @@ class TickersRoute { next(error); } }; + + public static getTickerPriceHistory: TickersRouteDefinitions.RouteMethod = async (request, response, next) => { + try { + const { tickerSymbol } = request.params; + const { from, to } = request.query; + + const coingecko = new CoingeckoService(); + const coingeckoId = getCoingeckoIdByChainlinkTicker(tickerSymbol); + + if (!coingeckoId) { + throw new NotFoundError(); + } + + const priceHistory = await coingecko.getPriceHistory(coingeckoId, from, to); + + return response.status(200).json(APIResponse.success({ + symbol: tickerSymbol, + prices: priceHistory.prices + })); + } 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 index 014fc59..a3004a2 100644 --- a/src/api/v1/validators/tickers.validator.ts +++ b/src/api/v1/validators/tickers.validator.ts @@ -3,17 +3,59 @@ import { val } from "../middlewares/validate.middleware"; import { checkSchema } from "express-validator"; import { ValidatorFields } from "../../../types/util.types"; -type GetTickerFields = - (keyof TickersRouteDefinitions.TickerParams) +type GetTickerFields = keyof ( + TickersRouteDefinitions.TickerParams +) const getTickerSchema: ValidatorFields = { tickerSymbol: { in: ["params"], errorMessage: "Ticker must be a string", - isString: true + isString: true, + isLength: { + errorMessage: "Ticker must be less than 3 characters", + options: { max: 3 } + } + } +}; + +type GetTickerPriceHistoryFields = keyof ( + TickersRouteDefinitions.TickerParams & + TickersRouteDefinitions.TickerPriceHistoryQueries +) + +const getTickerPriceHistorySchema: ValidatorFields = { + tickerSymbol: { + in: ["params"], + errorMessage: "Ticker must be a string", + isString: true, + isLength: { + errorMessage: "Ticker must be less than 3 characters", + options: { max: 3 } + } + }, + from: { + errorMessage: "Must be a valid UNIX timestamp", + in: ["query"], + isInt: { + options: { + min: 0 + } + } + }, + to: { + errorMessage: "Must be a valid UNIX timestamp", + in: ["query"], + isInt: { + options: { + min: 0 + } + } } }; export default class TickersValidator { public static validateGetTicker = val(checkSchema(getTickerSchema)); + + public static validateGetTickerPriceHistory = val(checkSchema(getTickerPriceHistorySchema)); } \ No newline at end of file diff --git a/src/entities/ticker.entity.ts b/src/entities/ticker.entity.ts index 293a9af..ffc6cdd 100644 --- a/src/entities/ticker.entity.ts +++ b/src/entities/ticker.entity.ts @@ -19,4 +19,11 @@ export function tickerEntityFromReduxState(tickerSymbol: string, state: RootStat } else { return undefined; } +} +export type TickerHistoryEntity = { + symbol: string; + prices: [ + timestamp: number, + price: number + ][]; } \ No newline at end of file diff --git a/src/services/coingecko/coingecko.service.ts b/src/services/coingecko/coingecko.service.ts index 17e5364..ab5b5b8 100644 --- a/src/services/coingecko/coingecko.service.ts +++ b/src/services/coingecko/coingecko.service.ts @@ -3,7 +3,7 @@ import { CONFIG } from "../../config"; import { EAuthenticationType } from "../../types/auth.types"; import { arrayToString } from "../../utils/common.util"; import { CoinGeckoFiatCurrencies } from "../../constants/coingecko"; -import { CoinGeckoSimplePriceResponse } from "./coingecko.service.types"; +import { CoinGeckoMarketChartRangeResponse, CoinGeckoSimplePriceResponse } from "./coingecko.service.types"; class CoinGeckoService extends RestService { constructor() { @@ -30,6 +30,21 @@ class CoinGeckoService extends RestService { return response.data; } + + public async getPriceHistory(id: string, from: number, to: number): Promise { + const response = await this.get({ + url: `/coins/${id}/market_chart/range`, + config: { + params: { + vs_currency: CoinGeckoFiatCurrencies.USD, + from: from, + to: to + } + } + }); + + return response.data; + } } export default CoinGeckoService; \ No newline at end of file diff --git a/src/services/coingecko/coingecko.service.types.ts b/src/services/coingecko/coingecko.service.types.ts index ff30efc..bd6914a 100644 --- a/src/services/coingecko/coingecko.service.types.ts +++ b/src/services/coingecko/coingecko.service.types.ts @@ -3,4 +3,19 @@ import { DynamicObject } from "../../types/util.types"; export type CoinGeckoSimplePriceResponse = { [ticker: string]: DynamicObject +} + +export type CoinGeckoMarketChartRangeResponse = { + prices: [ + timestamp: number, + price: number + ][], + market_caps: [ + timestamp: number, + marketCap: number + ][], + total_volumes: [ + timestamp: number, + totalVolume: number + ][] } \ No newline at end of file