├── .dockerignore ├── src ├── config │ ├── test-setup.ts │ ├── config.ts │ └── constants.ts ├── lib │ ├── interfaces │ │ ├── logs.ts │ │ ├── details.ts │ │ └── status.ts │ ├── db │ │ ├── models │ │ │ └── details-model.ts │ │ ├── mongoose.ts │ │ └── repositories │ │ │ └── details-repository.ts │ ├── middlewares │ │ ├── validation.middleware.ts │ │ └── auth.middleware.ts │ └── rabbitmq │ │ └── rabbitmq.ts ├── handlers │ ├── interfaces │ │ └── healthCheckResponse.ts │ ├── logs │ │ ├── validator.ts │ │ └── controller.ts │ ├── healthCheck │ │ └── controller.ts │ └── details │ │ ├── validator.ts │ │ └── controller.ts ├── helpers │ ├── common.ts │ ├── cronJob.ts │ ├── logger.ts │ ├── security.ts │ └── details.ts ├── tests │ ├── helpers │ │ ├── cronJob.spec.ts │ │ ├── common.spec.ts │ │ ├── security.spec.ts │ │ └── details.spec.ts │ ├── integrations │ │ └── defilama-service.spec.ts │ ├── fixtures │ │ └── mock-data.ts │ ├── handlers │ │ ├── healthcheck.spec.ts │ │ └── details.spec.ts │ ├── services │ │ └── details-service.spec.ts │ └── middleware │ │ └── auth.middleware.spec.ts ├── routes │ ├── health-routes.ts │ ├── log-routes.ts │ ├── index.ts │ └── details-routes.ts ├── server.ts ├── integrations │ └── defilama-service.ts ├── app.ts └── services │ └── details-service.ts ├── .prettierignore ├── .prettierrc ├── jest.config.ts ├── tsconfig.json ├── tslint.json ├── README.md ├── package.json ├── .gitignore └── CODE_OF_CONDUCT.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /src/config/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import 'jest-ts-auto-mock'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # misc 2 | package.json 3 | # folders 4 | node_modules/ 5 | files/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 80, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/interfaces/logs.ts: -------------------------------------------------------------------------------- 1 | export interface ILogs { 2 | logType: string; 3 | message: string; 4 | dateTime: Date; 5 | } 6 | -------------------------------------------------------------------------------- /src/handlers/interfaces/healthCheckResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IHealthcheckResponse { 2 | uptime: number; 3 | responsetime: number[]; 4 | message: string; 5 | timestamp: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/interfaces/details.ts: -------------------------------------------------------------------------------- 1 | export interface IDetail { 2 | symbol: string; 3 | address: string; 4 | category: string; 5 | tvl: number; 6 | change_1h: number; 7 | change_1d: number; 8 | change_7d: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary waits a specified number of seconds 3 | * @param {number} s - seconds to wait 4 | * @returns {Promise} - returned value 5 | */ 6 | export const waitSeconds = (s: number): Promise => { 7 | return new Promise((resolve) => setTimeout(resolve, s * 1000)); 8 | }; 9 | -------------------------------------------------------------------------------- /src/tests/helpers/cronJob.spec.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@config/config'; 2 | import { initCronJob } from '@helpers/cronJob'; 3 | 4 | jest.useFakeTimers(); 5 | 6 | describe('helpers/cronjob', () => { 7 | test('the schedule of cron job every hour', () => { 8 | initCronJob(config.cronJob.schedule); 9 | expect(config.cronJob.schedule).toBe('0 * * * *'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/routes/health-routes.ts: -------------------------------------------------------------------------------- 1 | import { authorizehRead } from '@lib/middlewares/auth.middleware'; 2 | import { healthRouter } from '@config/constants'; 3 | import * as HealthHandler from '@handlers/healthCheck/controller'; 4 | import { verifyErrors } from '@lib/middlewares/validation.middleware'; 5 | 6 | healthRouter.get('/', authorizehRead, verifyErrors, HealthHandler.healthCheck); 7 | 8 | export default healthRouter; 9 | -------------------------------------------------------------------------------- /src/tests/helpers/common.spec.ts: -------------------------------------------------------------------------------- 1 | import { waitSeconds } from '@helpers/common'; 2 | 3 | jest.useFakeTimers(); 4 | jest.spyOn(global, 'setTimeout'); 5 | 6 | describe('helpers/common', () => { 7 | it('should wait for 1 second', () => { 8 | waitSeconds(1); 9 | expect(setTimeout).toHaveBeenCalledTimes(1); 10 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/tests/integrations/defilama-service.spec.ts: -------------------------------------------------------------------------------- 1 | import * as defilamaService from '@integrations/defilama-service'; 2 | 3 | jest.useFakeTimers(); 4 | jest.setTimeout(15000); 5 | 6 | describe('integrations/getAllProtocols', () => { 7 | test('calls the getAllProtocols function', async () => { 8 | const value = await jest.mocked(defilamaService.getAllProtocols()); 9 | expect(value).toBeCalled; 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/helpers/cronJob.ts: -------------------------------------------------------------------------------- 1 | import nodeCron from 'node-cron'; 2 | import { bulkUpdateDetails } from '@helpers/details'; 3 | 4 | /** 5 | * @summary initates hourly cronjob 6 | * @param {string} schedule - schedule parameter 7 | * @returns {nodeCron.ScheduledTask} - returned value 8 | */ 9 | export const initCronJob = (schedule: string): nodeCron.ScheduledTask => { 10 | return nodeCron.schedule(schedule, bulkUpdateDetails); 11 | }; 12 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@src/helpers/logger'; 2 | import app, { onInit } from '@src/app'; 3 | import { config } from '@config/config'; 4 | 5 | app.listen(config.base.port, async (): Promise => { 6 | try { 7 | await onInit(); 8 | logger.info(`Service up && running on ${config.base.port}!`); 9 | } catch (e) { 10 | logger.info(`[src/server] - ${e.message}`); 11 | process.exit(0); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/handlers/logs/validator.ts: -------------------------------------------------------------------------------- 1 | import { body, ValidationChain } from 'express-validator'; 2 | 3 | export const logBody: ValidationChain[] = [ 4 | body('logType', 'logType should be a string') 5 | .optional({ checkFalsy: true }) 6 | .isString(), 7 | body('message', 'message should be a string') 8 | .optional({ checkFalsy: true }) 9 | .isString(), 10 | ]; 11 | 12 | export const createLog: ValidationChain[] = logBody; 13 | -------------------------------------------------------------------------------- /src/tests/helpers/security.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | authenticateApiKey, 3 | generateApiKey, 4 | generateApiSecret, 5 | } from '@helpers/security'; 6 | 7 | describe('helpers/security', () => { 8 | it('should generate an api Key, secret and then authenticate', () => { 9 | const key = generateApiKey(); 10 | const secret = generateApiSecret(key); 11 | const result = authenticateApiKey(secret, key); 12 | expect(result).toBeTruthy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/lib/interfaces/status.ts: -------------------------------------------------------------------------------- 1 | export enum PROMISE_STATUSES { 2 | FULFILLED = 'fulfilled', 3 | REJECTED = 'rejected', 4 | } 5 | 6 | export enum HTTP_CODES { 7 | OK = 200, 8 | CREATED = 201, 9 | NO_CONTENT = 204, 10 | BAD_REQUEST = 400, 11 | UNAUTHORIZED = 401, 12 | NOT_FOUND = 404, 13 | UNPROCESSABLE_ENTITY = 422, 14 | INTERNAL_SERVER_ERROR = 500, 15 | SERVICE_UNAVAILABLE = 503, 16 | } 17 | 18 | export interface VerificationErrorModel { 19 | [key: string]: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/routes/log-routes.ts: -------------------------------------------------------------------------------- 1 | import { logRouter } from '@config/constants'; 2 | import * as Controller from '@handlers/logs/controller'; 3 | import { authorizehWrite } from '@lib/middlewares/auth.middleware'; 4 | import * as Validator from '@handlers/logs/validator'; 5 | import { verifyErrors } from '@lib/middlewares/validation.middleware'; 6 | 7 | logRouter.post( 8 | '/', 9 | authorizehWrite, 10 | Validator.createLog, 11 | verifyErrors, 12 | Controller.createLog 13 | ); 14 | 15 | export default logRouter; 16 | -------------------------------------------------------------------------------- /src/tests/fixtures/mock-data.ts: -------------------------------------------------------------------------------- 1 | import { IDetail } from '@lib/interfaces/details'; 2 | 3 | export const mockData: IDetail = { 4 | symbol: 'mock-symbol', 5 | address: 'mock-address', 6 | category: 'mock-cat', 7 | tvl: 0, 8 | change_1h: 0, 9 | change_1d: 0, 10 | change_7d: 0, 11 | }; 12 | 13 | export const mockDataList: IDetail[] = [ 14 | { 15 | symbol: 'mock-symbol', 16 | address: 'mock-address', 17 | category: 'mock-cat', 18 | tvl: 0, 19 | change_1h: 0, 20 | change_1d: 0, 21 | change_7d: 0, 22 | }, 23 | ]; 24 | 25 | export const mockSymbol: string = 'mock-symbol'; 26 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import dappRoutes from '@routes/details-routes'; 2 | import healthRoutes from '@routes/health-routes'; 3 | import logRoutes from '@routes/log-routes'; 4 | import * as express from 'express'; 5 | 6 | /** 7 | * @summary registers the routes for the service 8 | * @param {express.Application} app - express application 9 | * @returns {void} - returned value 10 | */ 11 | export const register = (app: express.Application): void => { 12 | app.get('/'); 13 | app.use('(/api)?/v1/details-service', dappRoutes); 14 | app.use('/healthcheck', healthRoutes); 15 | app.use('/sendLog', logRoutes); 16 | }; 17 | -------------------------------------------------------------------------------- /src/tests/handlers/healthcheck.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMockReq, getMockRes } from '@jest-mock/express'; 2 | import { healthCheck } from '@handlers/healthCheck/controller'; 3 | import { HTTP_CODES } from '@lib/interfaces/status'; 4 | import { SUCCESS } from '@config/constants'; 5 | 6 | describe('handlers/healthcheck', () => { 7 | const req = getMockReq(); 8 | const { res } = getMockRes(); 9 | 10 | it('should send a response with 200 Ok', async () => { 11 | await healthCheck(req, res); 12 | expect(res.json).toHaveBeenCalledWith( 13 | expect.objectContaining({ 14 | status: SUCCESS, 15 | }) 16 | ); 17 | expect(res.status).toHaveBeenLastCalledWith(HTTP_CODES.OK); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/lib/db/models/details-model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | export const detailSchema = new Schema( 6 | { 7 | symbol: { 8 | type: String, 9 | unique: true, 10 | lowercase: true, 11 | }, 12 | address: { 13 | type: String, 14 | }, 15 | category: { 16 | type: String, 17 | }, 18 | tvl: { 19 | type: Number, 20 | }, 21 | change_1h: { 22 | type: Number, 23 | }, 24 | change_1d: { 25 | type: Number, 26 | }, 27 | change_7d: { 28 | type: Number, 29 | }, 30 | }, 31 | { 32 | timestamps: true, 33 | } 34 | ); 35 | 36 | export const Detail = mongoose.model('detail', detailSchema); 37 | -------------------------------------------------------------------------------- /src/routes/details-routes.ts: -------------------------------------------------------------------------------- 1 | import { detailsRouter } from '@config/constants'; 2 | import * as Controller from '@handlers/details/controller'; 3 | import { 4 | authorizehRead, 5 | authorizehWrite, 6 | } from '@lib/middlewares/auth.middleware'; 7 | import * as Validator from '@handlers/details/validator'; 8 | import { verifyErrors } from '@lib/middlewares/validation.middleware'; 9 | 10 | detailsRouter.get( 11 | '/details', 12 | authorizehRead, 13 | Validator.getAllDetails, 14 | verifyErrors, 15 | Controller.getAllDetails 16 | ); 17 | 18 | detailsRouter.post( 19 | '/details', 20 | authorizehWrite, 21 | Validator.createDetails, 22 | verifyErrors, 23 | Controller.createDetails 24 | ); 25 | 26 | export default detailsRouter; 27 | -------------------------------------------------------------------------------- /src/lib/db/mongoose.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@src/helpers/logger'; 2 | import mongoose from 'mongoose'; 3 | 4 | /** 5 | * @summary connects to MongoDB 6 | * @param {string} uri - mongodb uri 7 | * @returns {Promise} - returned value 8 | */ 9 | export const mongooseConnect = async (uri: string): Promise => { 10 | try { 11 | mongoose.connect(uri); 12 | } catch (e) { 13 | logger.info(`[lib/db/mongoose] - ${e.message}`); 14 | process.exit(1); 15 | } 16 | }; 17 | 18 | /** 19 | * @summary disconnects from MongoDB 20 | * @returns {Promise} - returned value 21 | */ 22 | export const mongooseDisconnect = async (): Promise => { 23 | try { 24 | mongoose.disconnect(); 25 | } catch (e) { 26 | logger.info(`[lib/db/mongoose] - ${e.message}`); 27 | process.exit(1); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import winston, { createLogger, format, transports } from 'winston'; 2 | import { IS_PROD_ENV } from '@config/constants'; 3 | 4 | const enumerateErrorFormat: winston.Logform.FormatWrap = format( 5 | (info: winston.Logform.TransformableInfo) => { 6 | if (info instanceof Error) { 7 | Object.assign(info, { message: info.stack }); 8 | } 9 | return info; 10 | } 11 | ); 12 | 13 | export const logger: winston.Logger = createLogger({ 14 | level: IS_PROD_ENV ? 'info' : 'debug', 15 | format: format.combine( 16 | enumerateErrorFormat(), 17 | IS_PROD_ENV ? format.colorize() : format.uncolorize(), 18 | format.splat(), 19 | format.printf(({ level, message }) => `${level}: ${message}`) 20 | ), 21 | transports: [ 22 | new transports.Console({ 23 | stderrLevels: ['error'], 24 | }), 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /src/integrations/defilama-service.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@src/helpers/logger'; 2 | import axios, { AxiosInstance, AxiosResponse } from 'axios'; 3 | import { DEFILAMA_BASE_API } from '@config/constants'; 4 | import { IDetail } from '@lib/interfaces/details'; 5 | 6 | export const defiLameInstancen: AxiosInstance = axios.create({ 7 | baseURL: DEFILAMA_BASE_API, 8 | }); 9 | 10 | /** 11 | * @summary returns coingecko coins 12 | * @returns {Promise<{ data: IDefiLamaResponse }>} - returned value 13 | */ 14 | export const getAllProtocols = async (): Promise => { 15 | const url = '/protocols'; 16 | try { 17 | const coins: AxiosResponse = 18 | await defiLameInstancen.get(url); 19 | return coins.data; 20 | } catch (e) { 21 | logger.error(`[defilama/getAllProtocols] - ${e.message}`); 22 | return null; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/tests/helpers/details.spec.ts: -------------------------------------------------------------------------------- 1 | import * as helper from '@helpers/details'; 2 | import * as defilamaService from '@integrations/defilama-service'; 3 | 4 | jest.useFakeTimers(); 5 | jest.setTimeout(15000); 6 | 7 | describe('helpers/common', () => { 8 | test('calls the getDetailsAndParse function', async () => { 9 | const defalamaMock = await jest.mocked(defilamaService.getAllProtocols()); 10 | const value = jest.mocked(helper.getDetailsAndParse()); 11 | expect(value).toBeCalled; 12 | expect(defalamaMock).toBeCalled; 13 | }); 14 | 15 | test('calls the bulkUpdateDetails function', async () => { 16 | const getDetailsAndParseMock = await jest.mocked( 17 | helper.getDetailsAndParse() 18 | ); 19 | const value = jest.mocked(helper.bulkUpdateDetails()); 20 | expect(value).toBeCalled; 21 | expect(getDetailsAndParseMock).toBeCalled; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { 3 | BASE_ACCESS_URL, 4 | DEFAULT_ENV, 5 | HOST, 6 | IS_DEV_ENV, 7 | IS_PROD_ENV, 8 | MONGODB_DEFAULT, 9 | MONGODB_DEV, 10 | MONGODB_PROD, 11 | MONGODB_TEST, 12 | PORT, 13 | } from '@config/constants'; 14 | 15 | dotenv.config(); 16 | 17 | export const config = { 18 | base: { 19 | protocol: 'http', 20 | host: HOST, 21 | port: PORT, 22 | accessUrl: BASE_ACCESS_URL, 23 | }, 24 | service: { 25 | prod: IS_PROD_ENV, 26 | dev: IS_DEV_ENV, 27 | default: DEFAULT_ENV, 28 | }, 29 | rabbitMQ: { 30 | url: 'amqp://localhost:5672', 31 | exchange: 'detailsExchange', 32 | }, 33 | db: { 34 | prod: MONGODB_PROD, 35 | dev: MONGODB_DEV, 36 | test: MONGODB_TEST, 37 | default: MONGODB_DEFAULT, 38 | }, 39 | cronJob: { 40 | schedule: '0 * * * *', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/tests/services/details-service.spec.ts: -------------------------------------------------------------------------------- 1 | import * as detailsService from '@services/details-service'; 2 | import { 3 | mockData, 4 | mockDataList, 5 | mockSymbol, 6 | } from '@src/tests/fixtures/mock-data'; 7 | 8 | jest.useFakeTimers(); 9 | jest.setTimeout(15000); 10 | 11 | describe('services/details-service', () => { 12 | test('calls the getAllDetails function', () => { 13 | const value = jest.mocked(detailsService.getAllDetails()); 14 | expect(value).toBeCalled; 15 | }); 16 | 17 | test('calls the updateDetails function', () => { 18 | const value = jest.mocked( 19 | detailsService.updateDetails(mockSymbol, mockData) 20 | ); 21 | expect(value).toBeCalled; 22 | }); 23 | 24 | test('calls the bulkUpdateDetails function', () => { 25 | const value = jest.mocked(detailsService.bulkUpdateDetails(mockDataList)); 26 | expect(value).toBeCalled; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFiles: ['dotenv/config'], 5 | moduleNameMapper: { 6 | '^@src/(.*)$': '/src/$1', 7 | '^@config/(.*)$': '/src/config/$1', 8 | '^@helpers/(.*)$': '/src/helpers/$1', 9 | '^@handlers/(.*)$': '/src/handlers/$1', 10 | '^@integrations/(.*)$': '/src/integrations/$1', 11 | '^@lib/(.*)$': '/src/lib/$1', 12 | '^@routes/(.*)$': '/src/routes/$1', 13 | '^@services/(.*)$': '/src/services/$1', 14 | }, 15 | coveragePathIgnorePatterns: [ 16 | 'node_modules', 17 | 'test-config', 18 | 'interfaces', 19 | 'jestGlobalMocks.ts', 20 | '/src/config/logger.ts', 21 | '.module.ts', 22 | '/src/app.ts', 23 | '.mock.ts', 24 | ], 25 | modulePathIgnorePatterns: ['/src/config/'], 26 | }; 27 | -------------------------------------------------------------------------------- /src/handlers/logs/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { SUCCESS } from '@config/constants'; 3 | import { HTTP_CODES } from '@lib/interfaces/status'; 4 | import * as producer from '@lib/rabbitmq/rabbitmq'; 5 | 6 | /** 7 | * @summary calls createDetails service 8 | * @param {Request} req - request object 9 | * @param {Response} res - response object 10 | * @returns {Promise} - returned value 11 | */ 12 | export const createLog = async ( 13 | req: Request, 14 | res: Response 15 | ): Promise => { 16 | try { 17 | const log: void = await producer.publishMessage( 18 | req.body.logType, 19 | req.body.message 20 | ); 21 | return res.status(HTTP_CODES.OK).json({ status: SUCCESS, data: log }); 22 | } catch (e) { 23 | return res 24 | .status(HTTP_CODES.SERVICE_UNAVAILABLE) 25 | .json({ error: e.message }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as routes from '@routes/index'; 2 | import * as bodyParser from 'body-parser'; 3 | import express from 'express'; 4 | import { mongooseConnect } from '@lib/db/mongoose'; 5 | import { logger } from '@src/helpers/logger'; 6 | import { initCronJob } from '@helpers/cronJob'; 7 | import { config } from '@config/config'; 8 | 9 | const app = express(); 10 | app.use(bodyParser.json({ limit: '10mb' })); 11 | app.use(bodyParser.urlencoded({ extended: false })); 12 | 13 | /* routing setup */ 14 | routes.register(app); 15 | 16 | /** 17 | * @summary initiates app 18 | * @returns {Promise} - returned value 19 | */ 20 | export const onInit = async (): Promise => { 21 | /* init cronjob */ 22 | initCronJob(config.cronJob.schedule); 23 | 24 | try { 25 | await mongooseConnect(config.db.dev); 26 | } catch (e) { 27 | logger.info(`[src/app] - ${e.message}`); 28 | process.exit(0); 29 | } 30 | }; 31 | 32 | export default app; 33 | -------------------------------------------------------------------------------- /src/handlers/healthCheck/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { IHealthcheckResponse } from '@handlers/interfaces/healthCheckResponse'; 3 | import { MESSAGE_OK, SUCCESS } from '@config/constants'; 4 | import { HTTP_CODES } from '@lib/interfaces/status'; 5 | 6 | /** 7 | * @summary checks the API health 8 | * @param {Request} _req - request object 9 | * @param {Response} res - response object 10 | * @returns {Promise} - returned value 11 | */ 12 | export const healthCheck = async ( 13 | _req: Request, 14 | res: Response 15 | ): Promise => { 16 | const healthcheck: IHealthcheckResponse = { 17 | uptime: process.uptime(), 18 | responsetime: process.hrtime(), 19 | message: MESSAGE_OK, 20 | timestamp: Date.now(), 21 | }; 22 | try { 23 | return res 24 | .status(HTTP_CODES.OK) 25 | .json({ status: SUCCESS, data: healthcheck }); 26 | } catch (e) { 27 | return res 28 | .status(HTTP_CODES.SERVICE_UNAVAILABLE) 29 | .json({ error: e.message }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/middlewares/validation.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { Result, ValidationError, validationResult } from 'express-validator'; 3 | import { HTTP_CODES, VerificationErrorModel } from '@lib/interfaces/status'; 4 | 5 | /** 6 | * @summary checks for express validator errors 7 | * @param {Request} req - request value 8 | * @param {Response} res - response value 9 | * @param {NextFunction} next - next function 10 | * @returns {void | Response} - returned value 11 | */ 12 | export const verifyErrors = ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ): void | Response => { 17 | const errors: Result = validationResult(req); 18 | 19 | if (errors.isEmpty()) { 20 | return next(); 21 | } 22 | 23 | const extractedErrors: VerificationErrorModel[] = []; 24 | 25 | errors 26 | .array() 27 | .map((e: ValidationError) => extractedErrors.push({ [e.param]: e.msg })); 28 | 29 | return res.status(HTTP_CODES.UNPROCESSABLE_ENTITY).json({ 30 | errors: extractedErrors, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/rabbitmq/rabbitmq.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@config/config'; 2 | import { logger } from '@helpers/logger'; 3 | import amqplib, { Channel, Connection } from 'amqplib'; 4 | import { ILogs } from '@lib/interfaces/logs'; 5 | 6 | /** 7 | * @summary publishes queye message 8 | * @returns {Promise} - returned value 9 | */ 10 | export const publishMessage = async ( 11 | routingKey: string, 12 | message: string 13 | ): Promise => { 14 | try { 15 | const connection: Connection = await amqplib.connect(config.rabbitMQ.url); 16 | const channel: Channel = await connection.createChannel(); 17 | const exchange: string = config.rabbitMQ.exchange; 18 | await channel.assertExchange(exchange, 'direct'); 19 | 20 | const logDetails: ILogs = { 21 | logType: routingKey, 22 | message: message, 23 | dateTime: new Date(), 24 | }; 25 | 26 | channel.publish( 27 | exchange, 28 | routingKey, 29 | Buffer.from(JSON.stringify(logDetails)) 30 | ); 31 | } catch (e) { 32 | logger.info(`[lib/rabbitmq] - ${e.message}`); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "lib": ["es5", "es6", "dom", "dom.iterable"], 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "rootDir": "./src", 11 | "outDir": "./dist", 12 | "baseUrl": ".", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "paths": { 16 | "*": ["node_modules/*"], 17 | "@src/*": ["src/*"], 18 | "@lib/*": ["src/lib/*"], 19 | "@config/*": ["src/config/*"], 20 | "@helpers/*": ["src/helpers/*"], 21 | "@routes/*": ["src/routes/*"], 22 | "@schemes/*": ["src/schemes/*"], 23 | "@handlers/*": ["src/handlers/*"], 24 | "@services/*": ["src/services/*"], 25 | "@integrations/*": ["src/integrations/*"], 26 | "@repositories/*": ["src/lib/db/repositories/*"] 27 | }, 28 | "plugins": [ 29 | { 30 | "transform": "ts-auto-mock/transformer", 31 | "cacheBetweenTests": false 32 | } 33 | ] 34 | }, 35 | "include": ["src"] 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/security.ts: -------------------------------------------------------------------------------- 1 | import crypto, { scryptSync, randomBytes, timingSafeEqual } from 'crypto'; 2 | 3 | /** 4 | * @summary generates API key 5 | * @returns {string} - returned value 6 | */ 7 | export const generateApiKey = (): string => { 8 | const buffer: Buffer = crypto.randomBytes(32); 9 | return buffer.toString('base64'); 10 | }; 11 | 12 | /** 13 | * @summary generates API secret 14 | * @param {crypto.BinaryLike} key - API key 15 | * @returns {string} - returned value 16 | */ 17 | export const generateApiSecret = (key: crypto.BinaryLike): string => { 18 | const salt: string = randomBytes(8).toString('hex'); 19 | const buffer: Buffer = scryptSync(key, salt, 64); 20 | return `${buffer.toString('hex')}.${salt}`; 21 | }; 22 | 23 | /** 24 | * @summary gauthenticates API key 25 | * @param {crypto.BinaryLike} storedKey - secret key 26 | * @param {crypto.BinaryLike} suppliedKey - API key 27 | * @returns {boolean} - returned value 28 | */ 29 | export const authenticateApiKey = ( 30 | storedKey: string, 31 | suppliedKey: crypto.BinaryLike 32 | ): boolean => { 33 | const [hashedKey, salt]: string[] = storedKey.split('.'); 34 | const buffer: Buffer = scryptSync(suppliedKey, salt, 64); 35 | return timingSafeEqual(Buffer.from(hashedKey, 'hex'), buffer); 36 | }; 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "trailing-comma": [false], 7 | "no-empty-interface": false, 8 | "curly": true, 9 | "ordered-imports": [ 10 | true, 11 | { 12 | "grouped-imports": true, 13 | "groups": [ 14 | { 15 | "name": "business logic", 16 | "match": "^(?!(@cronos))(@|\\.\\/)(repositor|service|integration|client|handler|job|lib|src\\/(helper|middleware)|helper)((?!(interfaces|models|constants)).)+$", 17 | "order": 2 18 | }, 19 | { 20 | "name": "models, interfaces, schemes", 21 | "match": "^(?!(@cronos)).*(interface|model|scheme).*$", 22 | "order": 3 23 | }, 24 | { 25 | "name": "constants, exceptions", 26 | "match": "^(?!(@cronos)).*(constant|exception).*$", 27 | "order": 4 28 | }, 29 | { 30 | "name": "relative paths", 31 | "match": "^[.].*", 32 | "order": 5 33 | }, 34 | { 35 | "name": "node_modules", 36 | "match": ".*", 37 | "order": 1 38 | } 39 | ], 40 | "alphabetize": { "order": "asc", "caseInsensitive": true }, 41 | "newlines-between": "always" 42 | } 43 | ] 44 | }, 45 | "rulesDirectory": [] 46 | } 47 | -------------------------------------------------------------------------------- /src/handlers/details/validator.ts: -------------------------------------------------------------------------------- 1 | import { body, query, ValidationChain } from 'express-validator'; 2 | 3 | export const detailsBody: ValidationChain[] = [ 4 | body('symbol', 'symbol should be a string') 5 | .optional({ checkFalsy: true }) 6 | .isString(), 7 | body('address', 'address should be a string') 8 | .optional({ checkFalsy: true }) 9 | .isString(), 10 | body('category', 'category should be a string') 11 | .optional({ checkFalsy: true }) 12 | .isString(), 13 | body('tvl', 'tvl should be a number') 14 | .optional({ checkFalsy: true }) 15 | .isNumeric(), 16 | body('change_1h', 'change_1h should be a number') 17 | .optional({ checkFalsy: true }) 18 | .isNumeric(), 19 | body('change_1d', 'change_1d should be a number') 20 | .optional({ checkFalsy: true }) 21 | .isNumeric(), 22 | body('change_7d', 'change_7d should be a number') 23 | .optional({ checkFalsy: true }) 24 | .isNumeric(), 25 | ]; 26 | 27 | export const logBody: ValidationChain[] = [ 28 | body('logType', 'logType should be a string') 29 | .optional({ checkFalsy: true }) 30 | .isString(), 31 | body('message', 'message should be a string') 32 | .optional({ checkFalsy: true }) 33 | .isString(), 34 | ]; 35 | 36 | export const getAllDetails: ValidationChain[] = [ 37 | query('query', 'query should be a string') 38 | .optional({ checkFalsy: true }) 39 | .isString(), 40 | ]; 41 | 42 | export const createDetails: ValidationChain[] = detailsBody; 43 | -------------------------------------------------------------------------------- /src/helpers/details.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@src/helpers/logger'; 2 | import * as detailService from '@src/services/details-service'; 3 | import { getAllProtocols } from '@src/integrations/defilama-service'; 4 | import { IDetail } from '@src/lib/interfaces/details'; 5 | import { CURRENT_DATE, FAILED, SUCCESS } from '@src/config/constants'; 6 | import mongodb from 'mongodb'; 7 | 8 | /** 9 | * @summary gets details and parses data 10 | * @returns {Promise<{ status: string, results: IDetail[] }>} - returned value 11 | */ 12 | export const getDetailsAndParse = async (): Promise<{ 13 | status: string; 14 | results: IDetail[]; 15 | }> => { 16 | try { 17 | const body: IDetail = await getAllProtocols(); 18 | const data: IDetail[] = JSON.parse(JSON.stringify(body)); 19 | return { status: SUCCESS, results: data }; 20 | } catch (e) { 21 | logger.error(`[helpers/getDetailsAndParse] - ${e.message}`); 22 | return { status: FAILED, results: null }; 23 | } 24 | }; 25 | 26 | /** 27 | * @summary updates data through loop 28 | * @returns {Promise<{ status: string, timeStamp: Date, operation: BulkWriteResult}>} - returned value 29 | */ 30 | export const bulkUpdateDetails = async (): Promise<{ 31 | status: string; 32 | timeStamp: Date; 33 | result: mongodb.BulkWriteResult; 34 | }> => { 35 | try { 36 | const body: { 37 | status: string; 38 | results: IDetail[]; 39 | } = await getDetailsAndParse(); 40 | const result: IDetail[] = body.results; 41 | const data: mongodb.BulkWriteResult = await detailService.bulkUpdateDetails( 42 | result 43 | ); 44 | 45 | return { status: SUCCESS, timeStamp: CURRENT_DATE, result: data }; 46 | } catch (e) { 47 | logger.error(`[helpers/updateDetails] - ${e.message}`); 48 | return { status: FAILED, timeStamp: CURRENT_DATE, result: null }; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/lib/db/repositories/details-repository.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { UpdateQuery } from 'mongoose'; 2 | import { IDetail } from '@lib/interfaces/details'; 3 | import { Detail } from '@lib/db/models/details-model'; 4 | import mongodb from 'mongodb'; 5 | 6 | /** 7 | * @summary gets all aggregated dapps 8 | * @returns {Promise} - returned value 9 | */ 10 | export const getAllDetails = async (): Promise => { 11 | return await Detail.find({}); 12 | }; 13 | 14 | /** 15 | * @summary creates details 16 | * @param {IDetail} data - body parameter 17 | * @returns {Promise} - returned value 18 | */ 19 | export const createDetails = async ( 20 | data: IDetail 21 | ): Promise => { 22 | return await Detail.create(data); 23 | }; 24 | 25 | /** 26 | * @summary updates price stats by symbol 27 | * @param {string} symbol - symbol parameter 28 | * @param {UpdateQuery} data - body parameter 29 | * @returns {Promise} - returned value 30 | */ 31 | export const updateDetails = async ( 32 | symbol: string, 33 | data: UpdateQuery 34 | ): Promise => { 35 | return await Detail.updateMany({ symbol: symbol }, data, { 36 | upsert: true, 37 | }); 38 | }; 39 | 40 | /** 41 | * @summary updates price stats by symbol 42 | * @param {UpdateQuery} data - body parameter 43 | * @returns {Promise} - returned value 44 | */ 45 | export const bulkUpdateDetails = async ( 46 | data: IDetail[] 47 | ): Promise => { 48 | return await Detail.bulkWrite([ 49 | { 50 | updateMany: { 51 | filter: { symbol: data.map((details: IDetail) => details.symbol) }, 52 | update: { data }, 53 | upsert: true, 54 | }, 55 | }, 56 | ]); 57 | }; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protocol Example Service 2 | 3 | ![Coverage Badge](https://img.shields.io/badge/Coverage-83-green) 4 | ![Version Badge](https://img.shields.io/badge/Version-v1.0.1-blue) 5 | 6 | This repository contains a protocol example service. 7 | 8 | ### Local setup 9 | 10 | - This local setup is provided as is 11 | - In order to run the formatter with `vscode`, the prettier extension needs to be installed 12 | - After installing the extension, add the following `json` to your `.vscode/settings.json` file: 13 | 14 | ``` 15 | { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "editor.formatOnSave": true, 18 | "jest.jestCommandLine": "npm run test --", 19 | "jest.autoRun": "off" 20 | } 21 | ``` 22 | 23 | - You need to generate an API key and a Secret key by using the helper functions provided in the example. 24 | 25 | - In order to have the local environment variables set up, add the following `.env` file in the root of the project: 26 | 27 | ``` 28 | DEFILAMA_BASE_API=https://api.llama.fi 29 | MONGODB_DEV=YOUR_MONGO_DB_URI 30 | READ_API_KEY=YOUR_READ_API_KEY 31 | READ_API_SECRET=YOUR_READ_API_SECRET 32 | WRITE_API_KEY=YOUR_CRONOS_DAPP_WRITE_API_KEY/68zAyBQ= 33 | WRITE_API_SECRET=YPUR_WRITE_API_SECRET 34 | JEST_TEST_API_KEY=YOUR_JEST_TEST_API_KEY 35 | JEST_TEST_SECRET_KEY=YOUR_JEST_TEST_API_KEY 36 | HOST=localhost 37 | ``` 38 | 39 | - Finally, to install all the dependencies, run the following: 40 | 41 | ```bash 42 | npm install 43 | ``` 44 | 45 | ### Database setup 46 | 47 | install mongodb community edition and start the database locally. 48 | 49 | ### Running the service 50 | 51 | - In order to run the service locally, after all the steps, run the following command: 52 | 53 | ```bash 54 | npm run dev 55 | ``` 56 | 57 | ### Links of interest 58 | 59 | - [Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protocol-example-service", 3 | "version": "1.0.0", 4 | "description": "protocol-example-service endpoint integrating the defilama api", 5 | "main": "server.ts", 6 | "scripts": { 7 | "prebuild": "tslint -c tslint.json -p tsconfig.json --fix", 8 | "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", 9 | "dev": "ts-node -r tsconfig-paths/register ./src/server.ts", 10 | "start": "node ./dist/server.js", 11 | "prod": "npm run build && npm run start", 12 | "pretty": "prettier --write \"./**/*.{ts,js,json}\"", 13 | "test": "jest --coverage --detectOpenHandles", 14 | "test:watch": "jest --watch" 15 | }, 16 | "author": "rarcifa", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@types/express": "4.17.15", 20 | "amqplib": "0.10.3", 21 | "axios": "0.25.0", 22 | "bcryptjs": "2.4.3", 23 | "body-parser": "1.20.1", 24 | "dotenv": "16.0.3", 25 | "express": "4.18.2", 26 | "express-validator": "6.14.2", 27 | "jsonwebtoken": "9.0.0", 28 | "module-alias": "2.2.2", 29 | "mongoose": "6.8.1", 30 | "node-cron": "3.0.2", 31 | "nodemon": "2.0.20", 32 | "randombytes": "2.1.0", 33 | "swagger-jsdoc": "6.2.7", 34 | "swagger-ui-express": "4.6.0", 35 | "ts-jest": "29.0.5", 36 | "tsc-alias": "1.8.2", 37 | "winston": "3.8.2" 38 | }, 39 | "devDependencies": { 40 | "@shelf/jest-mongodb": "^4.1.6", 41 | "@jest-mock/express": "^2.0.1", 42 | "@types/amqplib": "^0.10.1", 43 | "@types/jest": "^29.4.0", 44 | "@types/jsonwebtoken": "^9.0.0", 45 | "@types/module-alias": "^2.0.1", 46 | "@types/node-cron": "^3.0.7", 47 | "@types/swagger-jsdoc": "^6.0.1", 48 | "@types/swagger-ui-express": "^4.1.3", 49 | "@typescript-eslint/eslint-plugin": "^5.48.0", 50 | "jest": "^29.4.1", 51 | "mockingoose": "^2.16.2", 52 | "node-mocks-http": "^1.12.1", 53 | "prettier": "^2.5.1", 54 | "supertest": "^6.3.3", 55 | "ts-node": "^10.9.1", 56 | "tsconfig-paths": "^4.1.2", 57 | "tslint": "^6.1.3", 58 | "tslint-config-prettier": "^1.18.0", 59 | "typescript": "^4.9.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tests/handlers/details.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMockReq, getMockRes } from '@jest-mock/express'; 2 | import { 3 | getAllDetails, 4 | updatePriceStats, 5 | } from '@src/handlers/details/controller'; 6 | import { mongooseConnect, mongooseDisconnect } from '@src/lib/db/mongoose'; 7 | import * as detailsRepository from '@src/lib/db/repositories/details-repository'; 8 | import * as detailsService from '@src/services/details-service'; 9 | import { mockData } from '@src/tests/fixtures/mock-data'; 10 | import { Detail } from '@lib/db/models/details-model'; 11 | import { HTTP_CODES } from '@lib/interfaces/status'; 12 | import { SUCCESS } from '@src/config/constants'; 13 | import { config } from '@src/config/config'; 14 | 15 | jest.setTimeout(15000); 16 | 17 | beforeAll(async () => { 18 | await mongooseConnect(config.db.test); 19 | }); 20 | 21 | afterAll(async () => { 22 | await mongooseDisconnect(); 23 | }); 24 | 25 | describe('handlers/details', () => { 26 | const req = getMockReq(); 27 | const { res } = getMockRes(); 28 | 29 | beforeEach(async () => { 30 | await Detail.deleteMany({}); 31 | }); 32 | 33 | it('should call getAllDetails service and respond with 200 Ok', async () => { 34 | const service = jest.spyOn(detailsService, 'getAllDetails'); 35 | await getAllDetails(req, res); 36 | expect(service).toHaveBeenCalled(); 37 | expect(res.json).toHaveBeenCalledWith( 38 | expect.objectContaining({ 39 | status: SUCCESS, 40 | }) 41 | ); 42 | expect(res.status).toHaveBeenLastCalledWith(HTTP_CODES.OK); 43 | }); 44 | 45 | it('should call updateDetails service and respond with 200 Ok', async () => { 46 | const service = jest.spyOn(detailsService, 'updateDetails'); 47 | await detailsRepository.createDetails(mockData); 48 | const req = getMockReq({ 49 | params: { symbol: 'mock-symbol' }, 50 | body: mockData, 51 | }); 52 | await updatePriceStats(req, res); 53 | expect(service).toHaveBeenLastCalledWith('mock-symbol', mockData); 54 | expect(res.json).toHaveBeenCalledWith( 55 | expect.objectContaining({ 56 | status: SUCCESS, 57 | }) 58 | ); 59 | expect(res.status).toHaveBeenLastCalledWith(HTTP_CODES.OK); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | lerna-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | *.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules 38 | jspm_packages 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # TypeScript cache 44 | *.tsbuildinfo 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Microbundle cache 53 | .rpt2_cache/ 54 | .rts2_cache_cjs/ 55 | .rts2_cache_es/ 56 | .rts2_cache_umd/ 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # Next.js build output 75 | .next 76 | 77 | # Nuxt.js build / generate output 78 | .nuxt 79 | dist 80 | 81 | # Gatsby files 82 | .cache/ 83 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 84 | # https://nextjs.org/blog/next-9-1#public-directory-support 85 | # public 86 | 87 | # vuepress build output 88 | .vuepress/dist 89 | 90 | # Serverless directories 91 | .serverless/ 92 | 93 | # FuseBox cache 94 | .fusebox/ 95 | 96 | # DynamoDB Local files 97 | .dynamodb/ 98 | 99 | # TernJS port file 100 | .tern-port 101 | 102 | # Vscode 103 | .vscode 104 | 105 | # Msc 106 | .DS_Store 107 | .idea 108 | private.key 109 | public.key 110 | 111 | # Temp csv files 112 | files/ 113 | 114 | # sops 115 | **/**.dec.yaml -------------------------------------------------------------------------------- /src/handlers/details/controller.ts: -------------------------------------------------------------------------------- 1 | import * as detailService from '@services/details-service'; 2 | import { Request, Response } from 'express'; 3 | import mongoose, { UpdateWriteOpResult } from 'mongoose'; 4 | import { IDetail } from '@lib/interfaces/details'; 5 | import { getAllProtocols } from '@integrations/defilama-service'; 6 | import { SUCCESS } from '@config/constants'; 7 | import { HTTP_CODES } from '@lib/interfaces/status'; 8 | 9 | /** 10 | * @summary calls getAllDapps service 11 | * @param {Request} _req - request object 12 | * @param {Response} res - response object 13 | * @returns {Promise} - returned value 14 | */ 15 | export const getAllDetails = async ( 16 | _req: Request, 17 | res: Response 18 | ): Promise => { 19 | try { 20 | const details: IDetail[] = await detailService.getAllDetails(); 21 | return res.status(HTTP_CODES.OK).json({ status: SUCCESS, data: details }); 22 | } catch (e) { 23 | return res 24 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 25 | .json({ error: e.message }); 26 | } 27 | }; 28 | 29 | /** 30 | * @summary calls createDetails service 31 | * @param {Request} _req - request object 32 | * @param {Response} res - response object 33 | * @returns {Promise} - returned value 34 | */ 35 | export const createDetails = async ( 36 | _req: Request, 37 | res: Response 38 | ): Promise => { 39 | try { 40 | const body: IDetail = await getAllProtocols(); 41 | const details: mongoose.Document = await detailService.createDetails(body); 42 | return res.status(HTTP_CODES.OK).json({ status: SUCCESS, data: details }); 43 | } catch (e) { 44 | return res 45 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 46 | .json({ error: e.message }); 47 | } 48 | }; 49 | 50 | /** 51 | * @summary calls updatePriceStats service 52 | * @param {Request} req - request object 53 | * @param {Response} res - response object 54 | * @returns {Promise} - returned value 55 | */ 56 | export const updatePriceStats = async ( 57 | req: Request, 58 | res: Response 59 | ): Promise => { 60 | try { 61 | const details: UpdateWriteOpResult = await detailService.updateDetails( 62 | req.params.symbol, 63 | req.body 64 | ); 65 | return res.status(HTTP_CODES.OK).json({ status: SUCCESS, data: details }); 66 | } catch (e) { 67 | return res 68 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 69 | .json({ error: e.message }); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/services/details-service.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { UpdateQuery, UpdateWriteOpResult } from 'mongoose'; 2 | import { IDetail } from '@lib/interfaces/details'; 3 | import * as detailsRepository from '@lib/db/repositories/details-repository'; 4 | import { logger } from '@src/helpers/logger'; 5 | import mongodb from 'mongodb'; 6 | 7 | /** 8 | * @summary gets all aggregated dapps 9 | * @returns {Promise} - returned value 10 | */ 11 | export const getAllDetails = async (): Promise => { 12 | try { 13 | const details: IDetail[] = await detailsRepository.getAllDetails(); 14 | return details; 15 | } catch (e) { 16 | logger.error(`[services/getAllDetails] - ${e.message}`); 17 | return null; 18 | } 19 | }; 20 | 21 | /** 22 | * @summary creates details 23 | * @param {IDetail} data - body parameter 24 | * @returns {Promise} - returned value 25 | */ 26 | export const createDetails = async ( 27 | data: IDetail 28 | ): Promise => { 29 | try { 30 | const details: mongoose.Document = await detailsRepository.createDetails( 31 | data 32 | ); 33 | return details; 34 | } catch (e) { 35 | logger.error(`[services/createDetails] - ${e.message}`); 36 | return null; 37 | } 38 | }; 39 | 40 | /** 41 | * @summary updates price stats by symbol 42 | * @param {string} symbol - symbol parameter 43 | * @param {UpdateQuery} data - body parameter 44 | * @returns {Promise} - returned value 45 | */ 46 | export const updateDetails = async ( 47 | symbol: string, 48 | data: UpdateQuery 49 | ): Promise => { 50 | try { 51 | const details: UpdateWriteOpResult = await detailsRepository.updateDetails( 52 | symbol, 53 | data 54 | ); 55 | return details; 56 | } catch (e) { 57 | logger.error(`[services/updateDetails] - ${e.message}`); 58 | return null; 59 | } 60 | }; 61 | 62 | /** 63 | * @summary updates price stats by symbol 64 | * @param {IDetail} data - body parameter 65 | * @returns {Promise} - returned value 66 | */ 67 | export const bulkUpdateDetails = async ( 68 | data: IDetail[] 69 | ): Promise => { 70 | try { 71 | const details: mongodb.BulkWriteResult = 72 | await detailsRepository.bulkUpdateDetails(data); 73 | return details; 74 | } catch (e) { 75 | logger.error(`[services/bulkUpdateDetails] - ${e.message}`); 76 | return null; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/tests/middleware/auth.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | import { getMockRes } from '@jest-mock/express'; 2 | import { createRequest } from 'node-mocks-http'; 3 | import { 4 | authorizehRead, 5 | authorizehWrite, 6 | } from '@lib/middlewares/auth.middleware'; 7 | import { HTTP_CODES } from '@lib/interfaces/status'; 8 | import { 9 | INVALID_API_KEY, 10 | READ_API_KEY, 11 | WRITE_API_KEY, 12 | AUTHORIZATION_FAILED, 13 | } from '@config/constants'; 14 | 15 | describe('lib/middleware', () => { 16 | const { res, next } = getMockRes(); 17 | 18 | test('read with invalid key', async () => { 19 | try { 20 | const req = createRequest({ headers: { 'x-api-key': 'INVALID_KEY' } }); 21 | await authorizehRead(req, res, next); 22 | expect(res.json).toBeCalledWith({ message: INVALID_API_KEY }); 23 | expect(res.status).toBeCalledWith(HTTP_CODES.UNAUTHORIZED); 24 | return next(); 25 | } catch (e) { 26 | return res 27 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 28 | .json({ message: AUTHORIZATION_FAILED }); 29 | } 30 | }); 31 | 32 | test('read with valid key', async () => { 33 | try { 34 | const req = createRequest({ 35 | headers: { 'x-api-key': READ_API_KEY }, 36 | }); 37 | await authorizehRead(req, res, next); 38 | expect(next).toBeCalled(); 39 | return next(); 40 | } catch (e) { 41 | return res 42 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 43 | .json({ message: AUTHORIZATION_FAILED }); 44 | } 45 | }); 46 | 47 | test('write with invalid key', async () => { 48 | try { 49 | const req = createRequest({ headers: { 'x-api-key': 'INVALID_KEY' } }); 50 | await authorizehWrite(req, res, next); 51 | expect(res.json).toBeCalledWith({ message: INVALID_API_KEY }); 52 | expect(res.status).toBeCalledWith(HTTP_CODES.UNAUTHORIZED); 53 | return next(); 54 | } catch (e) { 55 | return res 56 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 57 | .json({ message: AUTHORIZATION_FAILED }); 58 | } 59 | }); 60 | 61 | test('write with valid key', async () => { 62 | try { 63 | const req = createRequest({ 64 | headers: { 'x-api-key': WRITE_API_KEY }, 65 | }); 66 | await authorizehWrite(req, res, next); 67 | expect(next).toBeCalled(); 68 | return next(); 69 | } catch (e) { 70 | return res 71 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 72 | .json({ message: AUTHORIZATION_FAILED }); 73 | } 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import * as dotenv from 'dotenv'; 3 | import mongoose from 'mongoose'; 4 | import express, { Router } from 'express'; 5 | 6 | dotenv.config(); 7 | 8 | // env 9 | process.env.NODE_ENV = 'production'; 10 | process.env.NODE_ENV = 'development'; 11 | 12 | // integrations 13 | export const DEFILAMA_BASE_API: string = process.env.DEFILAMA_BASE_API; 14 | 15 | // request status 16 | export const SUCCESS: string = 'success'; 17 | export const FAILED: string = 'failed'; 18 | export const INVALID_API_KEY: string = 'Invalid API key'; 19 | export const EXPIRED_TOKEN: string = 'Expired token'; 20 | export const AUTHORIZATION_FAILED: string = 'Failed to authorize endpoint'; 21 | export const MESSAGE_OK: string = 'OK'; 22 | 23 | // utils 24 | export const JWT_PAYLOAD: string = crypto.randomBytes(20).toString('hex'); 25 | export const IS_STRICT_QUERY: typeof mongoose = mongoose.set( 26 | 'strictQuery', 27 | false 28 | ); 29 | export const CURRENT_DATE: Date = new Date(); 30 | 31 | // service config 32 | export const IS_PROD_ENV: boolean = process.env.NODE_ENV === 'production'; 33 | export const IS_DEV_ENV: boolean = process.env.NODE_ENV === 'development'; 34 | export const DEFAULT_ENV: string = IS_DEV_ENV ? 'production' : 'development'; 35 | export const HOST: string = process.env.HOST; 36 | export const PORT: string = process.env.PORT; 37 | export const BASE_ACCESS_URL: string = process.env.BASE_ACCESS_URL; 38 | 39 | // db config 40 | export const MONGODB_PROD: string = process.env.MONGODB_PROD; 41 | export const MONGODB_DEV: string = process.env.MONGODB_DEV; 42 | export const MONGODB_TEST: string = process.env.MONGODB_TEST; 43 | export const MONGODB_DEFAULT: string = IS_DEV_ENV 44 | ? process.env.MONGODB_DEV 45 | : process.env.MONGODB_PROD; 46 | 47 | // jest config 48 | export const TEST_API_KEY: string = process.env.TEST_API_KEY; 49 | export const TEST_SECRET_KEY: string = process.env.TEST_SECRET_KEY; 50 | 51 | // keys 52 | export const READ_API_KEY: string = IS_DEV_ENV 53 | ? TEST_API_KEY 54 | : process.env.READ_API_KEY; 55 | export const READ_API_SECRET: string = IS_DEV_ENV 56 | ? TEST_SECRET_KEY 57 | : process.env.READ_API_SECRET; 58 | export const WRITE_API_KEY: string = IS_DEV_ENV 59 | ? TEST_SECRET_KEY 60 | : process.env.WRITE_API_KEY; 61 | export const WRITE_API_SECRET: string = IS_DEV_ENV 62 | ? TEST_SECRET_KEY 63 | : process.env.WRITE_API_SECRET; 64 | 65 | // router 66 | export const detailsRouter: Router = express.Router(); 67 | export const healthRouter: Router = express.Router(); 68 | export const logRouter: Router = express.Router(); 69 | -------------------------------------------------------------------------------- /src/lib/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | import { 3 | AUTHORIZATION_FAILED, 4 | READ_API_KEY, 5 | READ_API_SECRET, 6 | WRITE_API_KEY, 7 | WRITE_API_SECRET, 8 | EXPIRED_TOKEN, 9 | INVALID_API_KEY, 10 | } from '@config/constants'; 11 | import { authenticateApiKey } from '@helpers/security'; 12 | import { HTTP_CODES } from '@lib/interfaces/status'; 13 | 14 | /** 15 | * @summary checks for endpoint read rights 16 | * @param {Request} req - request value 17 | * @param {Response} res - response value 18 | * @param {NextFunction} next - next function 19 | * @returns {Promise } - returned value 20 | */ 21 | export const authorizehRead = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ): Promise => { 26 | try { 27 | const apiKey: string = req.header('x-api-key') || READ_API_KEY; 28 | const isValidApiKey: boolean = authenticateApiKey(READ_API_SECRET, apiKey); 29 | 30 | if (!isValidApiKey) { 31 | return res 32 | .status(HTTP_CODES.UNAUTHORIZED) 33 | .json({ message: INVALID_API_KEY }); 34 | } 35 | 36 | return next(); 37 | } catch (e) { 38 | if (e.name === 'TokenExpiredError') { 39 | return res 40 | .status(HTTP_CODES.UNAUTHORIZED) 41 | .json({ message: EXPIRED_TOKEN }); 42 | } 43 | 44 | return res 45 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 46 | .json({ message: AUTHORIZATION_FAILED }); 47 | } 48 | }; 49 | 50 | /** 51 | * @summary checks for endpoint write rights 52 | * @param {Request} req - request value 53 | * @param {Response} res - response value 54 | * @param {NextFunction} next - next function 55 | * @returns {Promise } - returned value 56 | */ 57 | export const authorizehWrite = async ( 58 | req: Request, 59 | res: Response, 60 | next: NextFunction 61 | ): Promise => { 62 | try { 63 | const apiKey: string = req.header('x-api-key') || WRITE_API_KEY; 64 | const isValidApiKey: boolean = authenticateApiKey(WRITE_API_SECRET, apiKey); 65 | 66 | if (!isValidApiKey) { 67 | return res 68 | .status(HTTP_CODES.UNAUTHORIZED) 69 | .json({ message: INVALID_API_KEY }); 70 | } 71 | 72 | return next(); 73 | } catch (e) { 74 | if (e.name === 'TokenExpiredError') { 75 | return res 76 | .status(HTTP_CODES.UNAUTHORIZED) 77 | .json({ message: EXPIRED_TOKEN }); 78 | } 79 | 80 | return res 81 | .status(HTTP_CODES.INTERNAL_SERVER_ERROR) 82 | .json({ message: AUTHORIZATION_FAILED }); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | rarcifa@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | --------------------------------------------------------------------------------