Merge pull request #19 from hyle-team/feature/refactor-create-order-endpoint
feat: refactor create-order endpoint
This commit is contained in:
commit
ef703da2de
6 changed files with 207 additions and 52 deletions
1
shared/constants.ts
Normal file
1
shared/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/;
|
||||
|
|
@ -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<CreateOrderRes>) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
36
src/interfaces/responses/orders/CreateOrderRes.ts
Normal file
36
src/interfaces/responses/orders/CreateOrderRes.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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' };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue