diff --git a/shared/constants.ts b/shared/constants.ts new file mode 100644 index 0000000..204f551 --- /dev/null +++ b/shared/constants.ts @@ -0,0 +1 @@ +export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/; diff --git a/src/controllers/orders.controller.ts b/src/controllers/orders.controller.ts index 61bc154..e3d6643 100644 --- a/src/controllers/orders.controller.ts +++ b/src/controllers/orders.controller.ts @@ -1,5 +1,7 @@ import { Request, Response } from 'express'; import Decimal from 'decimal.js'; + +import CreateOrderRes, { CreateOrderErrorCode } from '@/interfaces/responses/orders/CreateOrderRes'; import candlesModel from '../models/Candles'; import ordersModel from '../models/Orders'; import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody'; @@ -16,70 +18,97 @@ import Currency from '../schemes/Currency'; import { validateTokensInput } from '../../shared/utils'; class OrdersController { - async createOrder(req: Request, res: Response) { + static CURRENCY_DECIMAL_POINT_NOT_FOUND_ERROR_MSG = 'CURRENCY_DECIMAL_POINT_MISSING'; + async createOrder(req: Request, res: Response) { try { - const { orderData } = req.body as CreateOrderBody; + const body = req.body as CreateOrderBody; + const { orderData } = body; + const { price, amount, pairId } = orderData; - const isFull = - orderData && - orderData?.type && - orderData?.side && - orderData?.price && - orderData?.amount && - orderData?.pairId; + const priceDecimal = new Decimal(price); + const amountDecimal = new Decimal(amount); - const priceDecimal = new Decimal(orderData?.price || 0); - const amountDecimal = new Decimal(orderData?.amount || 0); - - const pair = await Pair.findByPk(orderData?.pairId); + const pair = await Pair.findByPk(pairId); const firstCurrency = await Currency.findByPk(pair?.first_currency_id); const secondCurrency = await Currency.findByPk(pair?.second_currency_id); if (!pair || !firstCurrency || !secondCurrency) { - return res.status(400).send({ success: false, data: 'Invalid pair data' }); + return res.status(400).send({ + success: false, + data: CreateOrderErrorCode.INVALID_ORDER_DATA, + }); } - const firstCurrencyDecimalPoint = firstCurrency?.asset_info?.decimal_point || 12; - const secondCurrencyDecimalPoint = secondCurrency?.asset_info?.decimal_point || 12; + const firstCurrencyDP = firstCurrency.asset_info?.decimal_point; + const secondCurrencyDP = secondCurrency.asset_info?.decimal_point; - const rangeCorrect = (() => { - const priceCorrect = validateTokensInput( - orderData?.price, - secondCurrencyDecimalPoint, - ).valid; - const amountCorrect = validateTokensInput( - orderData?.amount, - firstCurrencyDecimalPoint, - ).valid; - - return priceCorrect && amountCorrect; - })(); - - const priceDecimalPointCorrect = priceDecimal.toString().replace('.', '').length <= 20; - const amountDecimalPointCorrect = - amountDecimal.toString().replace('.', '').length <= 18; - - if (!priceDecimalPointCorrect || !amountDecimalPointCorrect) { - return res.status(400).send({ success: false, data: 'Invalid pair data' }); + if (firstCurrencyDP === undefined || secondCurrencyDP === undefined) { + throw new Error(OrdersController.CURRENCY_DECIMAL_POINT_NOT_FOUND_ERROR_MSG); } - if (!isFull || !rangeCorrect) - return res.status(400).send({ success: false, data: 'Invalid order data' }); + const totalDecimal = priceDecimal.mul(amountDecimal); + const total = totalDecimal.toFixed(); + + const isPriceValid = validateTokensInput(price, secondCurrencyDP).valid; + const isAmountValid = validateTokensInput(amount, firstCurrencyDP).valid; + const isTotalValid = validateTokensInput(total, secondCurrencyDP).valid; + + const areAmountsValid = isPriceValid && isAmountValid && isTotalValid; + + if (!areAmountsValid) { + return res.status(400).send({ + success: false, + data: CreateOrderErrorCode.INVALID_ORDER_DATA, + }); + } const result = await ordersModel.createOrder(req.body); - if (result.data === 'Invalid order data') return res.status(400).send(result); + if (result.data === 'Invalid order data') + return res.status(400).send({ + success: false, + data: CreateOrderErrorCode.INVALID_ORDER_DATA, + }); - if (result.data === 'Same order') return res.status(400).send(result); + if (result.data === 'Same order') + return res.status(400).send({ + success: false, + data: CreateOrderErrorCode.SAME_ORDER, + }); - if (result.data === 'Internal error') return res.status(500).send(result); + if (result.data === 'Internal error') { + throw new Error('orderModel.createOrder returned Internal error'); + } - res.status(200).send(result); + if (typeof result.data === 'string') { + throw new Error('Invalid orderModel.createOrder result'); + } + + const createdOrder = result.data; + + res.status(200).send({ + success: true, + data: { + id: createdOrder.id, + type: createdOrder.type, + timestamp: createdOrder.timestamp, + side: createdOrder.side, + price: createdOrder.price, + amount: createdOrder.amount, + total: createdOrder.total, + pair_id: createdOrder.pairId, + user_id: createdOrder.userId, + status: createdOrder.status, + left: createdOrder.left, + hasNotification: createdOrder.hasNotification, + immediateMatch: createdOrder.immediateMatch, + }, + }); } catch (err) { console.log(err); - res.status(500).send({ success: false, data: 'Unhandled error' }); + res.status(500).send({ success: false, data: CreateOrderErrorCode.UNHANDLED_ERROR }); } } diff --git a/src/interfaces/bodies/orders/CreateOrderBody.ts b/src/interfaces/bodies/orders/CreateOrderBody.ts index 877596e..3edc652 100644 --- a/src/interfaces/bodies/orders/CreateOrderBody.ts +++ b/src/interfaces/bodies/orders/CreateOrderBody.ts @@ -1,10 +1,24 @@ -import OfferType from '../../common/OfferType'; -import Side from '../../common/Side'; +import { body } from 'express-validator'; +import { NON_NEGATIVE_REAL_NUMBER_REGEX } from 'shared/constants'; import UserData from '../../common/UserData'; -interface OrderData { - type: OfferType; - side: Side; +export enum CreateOrderType { + // eslint-disable-next-line no-unused-vars + BUY = 'buy', + // eslint-disable-next-line no-unused-vars + SELL = 'sell', +} + +export enum CreateOrderSide { + // eslint-disable-next-line no-unused-vars + LIMIT = 'limit', + // eslint-disable-next-line no-unused-vars + MARKET = 'market', +} + +interface CreateOrderData { + type: CreateOrderType; + side: CreateOrderSide; price: string; amount: string; pairId: string; @@ -12,7 +26,26 @@ interface OrderData { interface CreateOrderBody { userData: UserData; - orderData: OrderData; + orderData: CreateOrderData; } +export const createOrderValidator = [ + body('orderData').isObject().withMessage('orderData must be an object'), + body('orderData.type') + .isIn(Object.values(CreateOrderType)) + .withMessage(`Invalid orderData.type value`), + body('orderData.side') + .isIn(Object.values(CreateOrderSide)) + .withMessage(`Invalid orderData.side value`), + body('orderData.price') + .isString() + .matches(NON_NEGATIVE_REAL_NUMBER_REGEX) + .withMessage('orderData.price must be a positive decimal string'), + body('orderData.amount') + .isString() + .matches(NON_NEGATIVE_REAL_NUMBER_REGEX) + .withMessage('orderData.amount must be a positive decimal string'), + body('orderData.pairId').isString().withMessage('orderData.pairId must be a string'), +]; + export default CreateOrderBody; diff --git a/src/interfaces/responses/orders/CreateOrderRes.ts b/src/interfaces/responses/orders/CreateOrderRes.ts new file mode 100644 index 0000000..34f1796 --- /dev/null +++ b/src/interfaces/responses/orders/CreateOrderRes.ts @@ -0,0 +1,36 @@ +export type CreateOrderSuccessRes = { + success: true; + data: { + hasNotification: boolean; + id: number; + type: string; + timestamp: number; + side: string; + price: string; + amount: string; + total: string; + pair_id: number; + user_id: number; + status: string; + left: string; + immediateMatch?: true; + }; +}; + +export enum CreateOrderErrorCode { + // eslint-disable-next-line no-unused-vars + INVALID_ORDER_DATA = 'Invalid order data', + // eslint-disable-next-line no-unused-vars + SAME_ORDER = 'Same order', + // eslint-disable-next-line no-unused-vars + UNHANDLED_ERROR = 'Unhandled error', +} + +export type CreateOrderErrorRes = { + success: false; + data: CreateOrderErrorCode; +}; + +type CreateOrderRes = CreateOrderSuccessRes | CreateOrderErrorRes; + +export default CreateOrderRes; diff --git a/src/models/Orders.ts b/src/models/Orders.ts index 3f58bc3..89320e7 100644 --- a/src/models/Orders.ts +++ b/src/models/Orders.ts @@ -68,7 +68,30 @@ class OrdersModel { return matchedOrders; } - async createOrder(body: CreateOrderBody) { + async createOrder(body: CreateOrderBody): Promise< + | { + success: false; + data: string; + } + | { + success: true; + data: { + id: number; + type: string; + timestamp: number; + side: string; + price: string; + amount: string; + total: string; + pairId: number; + userId: number; + status: string; + left: string; + hasNotification: boolean; + immediateMatch?: true; + }; + } + > { try { const { orderData } = body; const { userData } = body; @@ -173,13 +196,41 @@ class OrdersModel { return { success: true, data: { - ...newOrder.toJSON(), + id: newOrder.id, + type: newOrder.type, + timestamp: newOrder.timestamp, + side: newOrder.side, + price: newOrder.price, + amount: newOrder.amount, + total: newOrder.total, + pairId: newOrder.pair_id, + userId: newOrder.user_id, + status: newOrder.status, + left: newOrder.left, + hasNotification: newOrder.hasNotification, + immediateMatch: true, }, }; } - return { success: true, data: newOrder.toJSON() }; + return { + success: true, + data: { + id: newOrder.id, + type: newOrder.type, + timestamp: newOrder.timestamp, + side: newOrder.side, + price: newOrder.price, + amount: newOrder.amount, + total: newOrder.total, + pairId: newOrder.pair_id, + userId: newOrder.user_id, + status: newOrder.status, + left: newOrder.left, + hasNotification: newOrder.hasNotification, + }, + }; } catch (err) { console.log(err); return { success: false, data: 'Internal error' }; diff --git a/src/routes/orders.router.ts b/src/routes/orders.router.ts index 479aaca..712b106 100644 --- a/src/routes/orders.router.ts +++ b/src/routes/orders.router.ts @@ -1,4 +1,5 @@ import express from 'express'; +import { createOrderValidator } from '@/interfaces/bodies/orders/CreateOrderBody.js'; import middleware from '../middleware/middleware.js'; import ordersController from '../controllers/orders.controller.js'; @@ -15,7 +16,11 @@ ordersRouter.use( middleware.verifyToken, ); -ordersRouter.post('/orders/create', ordersController.createOrder); +ordersRouter.post( + '/orders/create', + middleware.expressValidator(createOrderValidator), + ordersController.createOrder, +); ordersRouter.post('/orders/get-page', ordersController.getOrdersPage); ordersRouter.post('/orders/get-user-page', ordersController.getUserOrdersPage); ordersRouter.post('/orders/get', ordersController.getUserOrders);