Skip to content

Commit

Permalink
api: Ticker Historical prices route (#5)
Browse files Browse the repository at this point in the history
* added price history query to the coingecko service

* added price history api route

* added 404 error on tickers history route

* lint fixes
  • Loading branch information
pajicf authored Dec 15, 2023
1 parent fda6b4b commit fdbdd0a
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 10 deletions.
15 changes: 11 additions & 4 deletions src/api/v1/definitions/tickers.route.d.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<T extends ETickersRoute> = // eslint-disable-line @typescript-eslint/no-unused-vars
EmptyObject;

type RequestQueries<T extends ETickersRoute> = // eslint-disable-line @typescript-eslint/no-unused-vars
EmptyObject
type RequestQueries<T extends ETickersRoute> =
T extends ETickersRoute.GetTickerHistory ? TickerPriceHistoryQueries :
EmptyObject;

type RequestParams<T extends ETickersRoute> =
// GET /tickers/[ticker]
Expand All @@ -43,6 +44,12 @@ declare namespace TickersRouteDefinitions {
type TickerParams = {
tickerSymbol: string;
}

// QUERY
type TickerPriceHistoryQueries = {
from: number,
to: number
}
}

export default TickersRouteDefinitions;
1 change: 1 addition & 0 deletions src/api/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 1 addition & 2 deletions src/api/v1/middlewares/error.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
}
Expand Down
25 changes: 25 additions & 0 deletions src/api/v1/routes/tickers.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ETickersRoute.GetTicker> = async (request, response, next) => {
Expand Down Expand Up @@ -36,6 +38,29 @@ class TickersRoute {
next(error);
}
};

public static getTickerPriceHistory: TickersRouteDefinitions.RouteMethod<ETickersRoute.GetTickerHistory> = 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;
48 changes: 45 additions & 3 deletions src/api/v1/validators/tickers.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetTickerFields> = {
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<GetTickerPriceHistoryFields> = {
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));
}
7 changes: 7 additions & 0 deletions src/entities/ticker.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ export function tickerEntityFromReduxState(tickerSymbol: string, state: RootStat
} else {
return undefined;
}
}
export type TickerHistoryEntity = {
symbol: string;
prices: [
timestamp: number,
price: number
][];
}
17 changes: 16 additions & 1 deletion src/services/coingecko/coingecko.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -30,6 +30,21 @@ class CoinGeckoService extends RestService {

return response.data;
}

public async getPriceHistory(id: string, from: number, to: number): Promise<CoinGeckoMarketChartRangeResponse> {
const response = await this.get<CoinGeckoMarketChartRangeResponse>({
url: `/coins/${id}/market_chart/range`,
config: {
params: {
vs_currency: CoinGeckoFiatCurrencies.USD,
from: from,
to: to
}
}
});

return response.data;
}
}

export default CoinGeckoService;
15 changes: 15 additions & 0 deletions src/services/coingecko/coingecko.service.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,19 @@ import { DynamicObject } from "../../types/util.types";

export type CoinGeckoSimplePriceResponse = {
[ticker: string]: DynamicObject<number, CoinGeckoFiatCurrencies>
}

export type CoinGeckoMarketChartRangeResponse = {
prices: [
timestamp: number,
price: number
][],
market_caps: [
timestamp: number,
marketCap: number
][],
total_volumes: [
timestamp: number,
totalVolume: number
][]
}

0 comments on commit fdbdd0a

Please sign in to comment.