Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: Ticker Historical prices route #5

Merged
merged 4 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
][]
}
Loading