Merge pull request #20 from hyle-team/feature/add-get-user-orders-pagination
feat: add get-user-orders pagination
This commit is contained in:
commit
bfae676c2e
11 changed files with 782 additions and 27 deletions
|
|
@ -2,11 +2,27 @@ import { Request, Response } from 'express';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
import CreateOrderRes, { CreateOrderErrorCode } from '@/interfaces/responses/orders/CreateOrderRes';
|
import CreateOrderRes, { CreateOrderErrorCode } from '@/interfaces/responses/orders/CreateOrderRes';
|
||||||
|
import GetUserOrdersRes, {
|
||||||
|
GetUserOrdersErrorCode,
|
||||||
|
GetUserOrdersResCurrency,
|
||||||
|
GetUserOrdersResOrderData,
|
||||||
|
} from '@/interfaces/responses/orders/GetUserOrdersRes';
|
||||||
|
import GetUserOrdersAllPairsBody from '@/interfaces/bodies/orders/GetUserOrdersAllPairsBody';
|
||||||
|
import GetUserOrdersAllPairsRes, {
|
||||||
|
GetUserOrdersAllPairsErrorCode,
|
||||||
|
GetUserOrdersAllPairsResPair,
|
||||||
|
} from '@/interfaces/responses/orders/GetUserOrdersAllPairsRes';
|
||||||
|
import CancelAllBody, { CancelAllBodyOrderType } from '@/interfaces/bodies/orders/CancelAllBody';
|
||||||
|
import sequelize from '@/sequelize';
|
||||||
|
import CancelAllRes, { CancelAllErrorCode } from '@/interfaces/responses/orders/CancelAllRes';
|
||||||
import candlesModel from '../models/Candles';
|
import candlesModel from '../models/Candles';
|
||||||
import ordersModel from '../models/Orders';
|
import ordersModel from '../models/Orders';
|
||||||
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody';
|
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody';
|
||||||
import GetUserOrdersPageBody from '../interfaces/bodies/orders/GetUserOrdersPageBody';
|
import GetUserOrdersPageBody from '../interfaces/bodies/orders/GetUserOrdersPageBody';
|
||||||
import GetUserOrdersBody from '../interfaces/bodies/orders/GetUserOrdersBody';
|
import GetUserOrdersBody, {
|
||||||
|
GetUserOrdersBodyStatus,
|
||||||
|
GetUserOrdersBodyType,
|
||||||
|
} from '../interfaces/bodies/orders/GetUserOrdersBody';
|
||||||
import CancelOrderBody from '../interfaces/bodies/orders/CancelOrderBody';
|
import CancelOrderBody from '../interfaces/bodies/orders/CancelOrderBody';
|
||||||
import GetCandlesBody from '../interfaces/bodies/orders/GetCandlesBody';
|
import GetCandlesBody from '../interfaces/bodies/orders/GetCandlesBody';
|
||||||
import GetChartOrdersBody from '../interfaces/bodies/orders/GetChartOrdersBody';
|
import GetChartOrdersBody from '../interfaces/bodies/orders/GetChartOrdersBody';
|
||||||
|
|
@ -157,21 +173,171 @@ class OrdersController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserOrders(req: Request, res: Response) {
|
private fromGetUserOrdersServiceToResCurrencyMapper(
|
||||||
|
currency: Currency,
|
||||||
|
): GetUserOrdersResCurrency {
|
||||||
|
return {
|
||||||
|
id: currency.id,
|
||||||
|
name: currency.name,
|
||||||
|
code: currency.code,
|
||||||
|
type: currency.type,
|
||||||
|
asset_id: currency.asset_id,
|
||||||
|
auto_parsed: currency.auto_parsed,
|
||||||
|
asset_info: currency.asset_info
|
||||||
|
? {
|
||||||
|
asset_id: currency.asset_info.asset_id,
|
||||||
|
logo: currency.asset_info.logo,
|
||||||
|
price_url: currency.asset_info.price_url,
|
||||||
|
ticker: currency.asset_info.ticker,
|
||||||
|
full_name: currency.asset_info.full_name,
|
||||||
|
total_max_supply: currency.asset_info.total_max_supply,
|
||||||
|
current_supply: currency.asset_info.current_supply,
|
||||||
|
decimal_point: currency.asset_info.decimal_point,
|
||||||
|
meta_info: currency.asset_info.meta_info,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
whitelisted: currency.whitelisted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
getUserOrders = async (req: Request, res: Response<GetUserOrdersRes>) => {
|
||||||
try {
|
try {
|
||||||
await userModel.resetExchangeNotificationsAmount(
|
const body = req.body as GetUserOrdersBody;
|
||||||
(req.body.userData as UserData).address,
|
const { userData, offset, limit, filterInfo } = body;
|
||||||
);
|
|
||||||
const result = await ordersModel.getUserOrders(req.body as GetUserOrdersBody);
|
|
||||||
|
|
||||||
if (result.data === 'Internal error') return res.status(500).send(result);
|
await userModel.resetExchangeNotificationsAmount(userData.address);
|
||||||
|
|
||||||
res.status(200).send(result);
|
const serviceOrderType: 'buy' | 'sell' | undefined = (() => {
|
||||||
|
if (filterInfo?.type === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterInfo.type === GetUserOrdersBodyType.BUY ? 'buy' : 'sell';
|
||||||
|
})();
|
||||||
|
|
||||||
|
const serviceOrderStatus: 'active' | 'finished' | undefined = (() => {
|
||||||
|
if (filterInfo?.status === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterInfo.status === GetUserOrdersBodyStatus.ACTIVE ? 'active' : 'finished';
|
||||||
|
})();
|
||||||
|
|
||||||
|
const result = await ordersModel.getUserOrders({
|
||||||
|
address: userData.address,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
filterInfo: {
|
||||||
|
pairId: filterInfo.pairId,
|
||||||
|
type: serviceOrderType,
|
||||||
|
status: serviceOrderStatus,
|
||||||
|
date:
|
||||||
|
filterInfo.date !== undefined
|
||||||
|
? {
|
||||||
|
from: filterInfo.date.from,
|
||||||
|
to: filterInfo.date.to,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.data === 'Internal error') {
|
||||||
|
throw new Error('ordersModel.getUserOrders returned Internal error');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { totalItemsCount } = result;
|
||||||
|
|
||||||
|
const userOrders = result.data.map((order) => {
|
||||||
|
const mappedOrder: GetUserOrdersResOrderData = {
|
||||||
|
id: order.id,
|
||||||
|
type: order.type,
|
||||||
|
timestamp: order.timestamp,
|
||||||
|
side: order.side,
|
||||||
|
price: order.price,
|
||||||
|
amount: order.amount,
|
||||||
|
total: order.total,
|
||||||
|
pair_id: order.pair_id,
|
||||||
|
user_id: order.user_id,
|
||||||
|
status: order.status,
|
||||||
|
left: order.left,
|
||||||
|
hasNotification: order.hasNotification,
|
||||||
|
pair: {
|
||||||
|
id: order.pair.id,
|
||||||
|
first_currency_id: order.pair.first_currency_id,
|
||||||
|
second_currency_id: order.pair.second_currency_id,
|
||||||
|
rate: order.pair.rate,
|
||||||
|
coefficient: order.pair.coefficient,
|
||||||
|
high: order.pair.high,
|
||||||
|
low: order.pair.low,
|
||||||
|
volume: order.pair.volume,
|
||||||
|
featured: order.pair.featured,
|
||||||
|
first_currency: this.fromGetUserOrdersServiceToResCurrencyMapper(
|
||||||
|
order.pair.first_currency,
|
||||||
|
),
|
||||||
|
second_currency: this.fromGetUserOrdersServiceToResCurrencyMapper(
|
||||||
|
order.pair.second_currency,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
first_currency: this.fromGetUserOrdersServiceToResCurrencyMapper(
|
||||||
|
order.first_currency,
|
||||||
|
),
|
||||||
|
second_currency: this.fromGetUserOrdersServiceToResCurrencyMapper(
|
||||||
|
order.second_currency,
|
||||||
|
),
|
||||||
|
isInstant: order.isInstant,
|
||||||
|
};
|
||||||
|
|
||||||
|
return mappedOrder;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
totalItemsCount,
|
||||||
|
data: userOrders,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
res.status(500).send({ success: false, data: 'Unhandled error' });
|
res.status(500).send({
|
||||||
|
success: false,
|
||||||
|
data: GetUserOrdersErrorCode.UNHANDLED_ERROR,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
getUserOrdersAllPairs = async (req: Request, res: Response<GetUserOrdersAllPairsRes>) => {
|
||||||
|
try {
|
||||||
|
const body = req.body as GetUserOrdersAllPairsBody;
|
||||||
|
const { userData } = body;
|
||||||
|
|
||||||
|
const getUserOrdersAllPairsResult = await ordersModel.getUserOrdersAllPairs(
|
||||||
|
userData.address,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pairs = getUserOrdersAllPairsResult.data;
|
||||||
|
|
||||||
|
const responsePairs: GetUserOrdersAllPairsResPair[] = pairs.map((pair) => ({
|
||||||
|
id: pair.id,
|
||||||
|
firstCurrency: {
|
||||||
|
id: pair.firstCurrency.id,
|
||||||
|
ticker: pair.firstCurrency.ticker,
|
||||||
|
},
|
||||||
|
secondCurrency: {
|
||||||
|
id: pair.secondCurrency.id,
|
||||||
|
ticker: pair.secondCurrency.ticker,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
success: true,
|
||||||
|
data: responsePairs,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
res.status(500).send({
|
||||||
|
success: false,
|
||||||
|
data: GetUserOrdersAllPairsErrorCode.UNHANDLED_ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async cancelOrder(req: Request, res: Response) {
|
async cancelOrder(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -289,6 +455,51 @@ class OrdersController {
|
||||||
res.status(500).send({ success: false, data: 'Unhandled error' });
|
res.status(500).send({ success: false, data: 'Unhandled error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelAll(req: Request, res: Response<CancelAllRes>) {
|
||||||
|
try {
|
||||||
|
const body = req.body as CancelAllBody;
|
||||||
|
const { userData, filterInfo } = body;
|
||||||
|
|
||||||
|
const filterType = (() => {
|
||||||
|
if (filterInfo.type === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterInfo.type === CancelAllBodyOrderType.BUY
|
||||||
|
? GetUserOrdersBodyType.BUY
|
||||||
|
: GetUserOrdersBodyType.SELL;
|
||||||
|
})();
|
||||||
|
|
||||||
|
await sequelize.transaction(async (transaction) => {
|
||||||
|
await ordersModel.cancelAll(
|
||||||
|
{
|
||||||
|
address: userData.address,
|
||||||
|
filterInfo: {
|
||||||
|
pairId: filterInfo.pairId,
|
||||||
|
type: filterType,
|
||||||
|
date:
|
||||||
|
filterInfo.date !== undefined
|
||||||
|
? {
|
||||||
|
from: filterInfo.date.from,
|
||||||
|
to: filterInfo.date.to,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ transaction },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
res.status(500).send({
|
||||||
|
success: false,
|
||||||
|
data: CancelAllErrorCode.UNHANDLED_ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ordersController = new OrdersController();
|
const ordersController = new OrdersController();
|
||||||
|
|
|
||||||
50
src/interfaces/bodies/orders/CancelAllBody.ts
Normal file
50
src/interfaces/bodies/orders/CancelAllBody.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import UserData from '@/interfaces/common/UserData';
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
|
||||||
|
export enum CancelAllBodyOrderType {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
BUY = 'buy',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
SELL = 'sell',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CancelAllBody {
|
||||||
|
userData: UserData;
|
||||||
|
|
||||||
|
filterInfo: {
|
||||||
|
pairId?: number;
|
||||||
|
type?: CancelAllBodyOrderType;
|
||||||
|
date?: {
|
||||||
|
// UNIX timestamps in milliseconds
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelAllValidator = [
|
||||||
|
body('filterInfo').isObject().withMessage('filterInfo must be an object'),
|
||||||
|
body('filterInfo.pairId')
|
||||||
|
.optional()
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage('filterInfo.pairId must be a non-negative integer'),
|
||||||
|
body('filterInfo.type')
|
||||||
|
.optional()
|
||||||
|
.isIn(Object.values(CancelAllBodyOrderType))
|
||||||
|
.withMessage(`Invalid filterInfo.type value`),
|
||||||
|
body('filterInfo.date').optional().isObject().withMessage('filterInfo.date must be an object'),
|
||||||
|
body('filterInfo.date.from')
|
||||||
|
.if(body('filterInfo.date').isObject())
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage(
|
||||||
|
'filterInfo.date.from must be a non-negative integer representing a UNIX timestamp in milliseconds',
|
||||||
|
),
|
||||||
|
body('filterInfo.date.to')
|
||||||
|
.if(body('filterInfo.date').isObject())
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage(
|
||||||
|
'filterInfo.date.to must be a non-negative integer representing a UNIX timestamp in milliseconds',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default CancelAllBody;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import UserData from '@/interfaces/common/UserData';
|
||||||
|
|
||||||
|
interface GetUserOrdersAllPairsBody {
|
||||||
|
userData: UserData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserOrdersAllPairsValidator = [];
|
||||||
|
|
||||||
|
export default GetUserOrdersAllPairsBody;
|
||||||
|
|
@ -1,7 +1,68 @@
|
||||||
import UserData from '../../common/UserData';
|
import UserData from '@/interfaces/common/UserData';
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
|
||||||
|
export enum GetUserOrdersBodyStatus {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
ACTIVE = 'active',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
FINISHED = 'finished',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GetUserOrdersBodyType {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
BUY = 'buy',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
SELL = 'sell',
|
||||||
|
}
|
||||||
|
|
||||||
interface GetUserOrdersBody {
|
interface GetUserOrdersBody {
|
||||||
userData: UserData;
|
userData: UserData;
|
||||||
|
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
filterInfo: {
|
||||||
|
pairId?: number;
|
||||||
|
status?: GetUserOrdersBodyStatus;
|
||||||
|
type?: GetUserOrdersBodyType;
|
||||||
|
date?: {
|
||||||
|
// UNIX timestamps in milliseconds
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUserOrdersValidator = [
|
||||||
|
body('limit')
|
||||||
|
.isInt({ min: 1, max: 1000 })
|
||||||
|
.withMessage('limit must be a positive integer within certain range'),
|
||||||
|
body('offset').isInt({ min: 0 }).withMessage('offset must be a non-negative integer'),
|
||||||
|
body('filterInfo').isObject().withMessage('filterInfo must be an object'),
|
||||||
|
body('filterInfo.pairId')
|
||||||
|
.optional()
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage('filterInfo.pairId must be a non-negative integer'),
|
||||||
|
body('filterInfo.status')
|
||||||
|
.optional()
|
||||||
|
.isIn(Object.values(GetUserOrdersBodyStatus))
|
||||||
|
.withMessage(`Invalid filterInfo.status value`),
|
||||||
|
body('filterInfo.type')
|
||||||
|
.optional()
|
||||||
|
.isIn(Object.values(GetUserOrdersBodyType))
|
||||||
|
.withMessage(`Invalid filterInfo.type value`),
|
||||||
|
body('filterInfo.date').optional().isObject().withMessage('filterInfo.date must be an object'),
|
||||||
|
body('filterInfo.date.from')
|
||||||
|
.if(body('filterInfo.date').isObject())
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage(
|
||||||
|
'filterInfo.date.from must be a non-negative integer representing a UNIX timestamp in milliseconds',
|
||||||
|
),
|
||||||
|
body('filterInfo.date.to')
|
||||||
|
.if(body('filterInfo.date').isObject())
|
||||||
|
.isInt({ min: 0 })
|
||||||
|
.withMessage(
|
||||||
|
'filterInfo.date.to must be a non-negative integer representing a UNIX timestamp in milliseconds',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
export default GetUserOrdersBody;
|
export default GetUserOrdersBody;
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,7 @@ export interface PairWithCurrencies extends Pair {
|
||||||
export interface OrderWithPairAndCurrencies extends Order {
|
export interface OrderWithPairAndCurrencies extends Order {
|
||||||
pair: PairWithCurrencies;
|
pair: PairWithCurrencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PairWithIdAndCurrencies extends PairWithCurrencies {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
|
||||||
17
src/interfaces/responses/orders/CancelAllRes.ts
Normal file
17
src/interfaces/responses/orders/CancelAllRes.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export type CancelAllSuccessRes = {
|
||||||
|
success: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum CancelAllErrorCode {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
UNHANDLED_ERROR = 'Unhandled error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CancelAllErrorRes = {
|
||||||
|
success: false;
|
||||||
|
data: CancelAllErrorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CancelAllRes = CancelAllSuccessRes | CancelAllErrorRes;
|
||||||
|
|
||||||
|
export default CancelAllRes;
|
||||||
30
src/interfaces/responses/orders/GetUserOrdersAllPairsRes.ts
Normal file
30
src/interfaces/responses/orders/GetUserOrdersAllPairsRes.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export type GetUserOrdersAllPairsResPair = {
|
||||||
|
id: number;
|
||||||
|
firstCurrency: {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
};
|
||||||
|
secondCurrency: {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetUserOrdersAllPairsSuccessRes = {
|
||||||
|
success: true;
|
||||||
|
data: GetUserOrdersAllPairsResPair[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum GetUserOrdersAllPairsErrorCode {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
UNHANDLED_ERROR = 'Unhandled error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetUserOrdersAllPairsErrorRes = {
|
||||||
|
success: false;
|
||||||
|
data: GetUserOrdersAllPairsErrorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetUserOrdersAllPairsRes = GetUserOrdersAllPairsSuccessRes | GetUserOrdersAllPairsErrorRes;
|
||||||
|
|
||||||
|
export default GetUserOrdersAllPairsRes;
|
||||||
74
src/interfaces/responses/orders/GetUserOrdersRes.ts
Normal file
74
src/interfaces/responses/orders/GetUserOrdersRes.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
export type GetUserOrdersResCurrency = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
type: string;
|
||||||
|
asset_id: string;
|
||||||
|
auto_parsed: boolean;
|
||||||
|
asset_info?: {
|
||||||
|
asset_id: string;
|
||||||
|
logo: string;
|
||||||
|
price_url: string;
|
||||||
|
ticker: string;
|
||||||
|
full_name: string;
|
||||||
|
total_max_supply: string;
|
||||||
|
current_supply: string;
|
||||||
|
decimal_point: number;
|
||||||
|
meta_info: string;
|
||||||
|
};
|
||||||
|
whitelisted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetUserOrdersResOrderData = {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
timestamp: number;
|
||||||
|
side: string;
|
||||||
|
price: string;
|
||||||
|
amount: string;
|
||||||
|
total: string;
|
||||||
|
pair_id: number;
|
||||||
|
user_id: number;
|
||||||
|
status: string;
|
||||||
|
left: string;
|
||||||
|
hasNotification: boolean;
|
||||||
|
|
||||||
|
pair: {
|
||||||
|
id: number;
|
||||||
|
first_currency_id: number;
|
||||||
|
second_currency_id: number;
|
||||||
|
rate?: number;
|
||||||
|
coefficient?: number;
|
||||||
|
high?: number;
|
||||||
|
low?: number;
|
||||||
|
volume: number;
|
||||||
|
featured: boolean;
|
||||||
|
|
||||||
|
first_currency: GetUserOrdersResCurrency;
|
||||||
|
second_currency: GetUserOrdersResCurrency;
|
||||||
|
};
|
||||||
|
|
||||||
|
first_currency: GetUserOrdersResCurrency;
|
||||||
|
second_currency: GetUserOrdersResCurrency;
|
||||||
|
isInstant: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetUserOrdersSuccessRes = {
|
||||||
|
success: true;
|
||||||
|
totalItemsCount: number;
|
||||||
|
data: GetUserOrdersResOrderData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum GetUserOrdersErrorCode {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
UNHANDLED_ERROR = 'Unhandled error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetUserOrdersErrorRes = {
|
||||||
|
success: false;
|
||||||
|
data: GetUserOrdersErrorCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetUserOrdersRes = GetUserOrdersSuccessRes | GetUserOrdersErrorRes;
|
||||||
|
|
||||||
|
export default GetUserOrdersRes;
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { Op } from 'sequelize';
|
import { Op, Transaction as SequelizeTransaction, WhereOptions } from 'sequelize';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
import TransactionWithOrders from '@/interfaces/common/Transaction.js';
|
import TransactionWithOrders from '@/interfaces/common/Transaction.js';
|
||||||
import Currency from '@/schemes/Currency.js';
|
import Currency from '@/schemes/Currency.js';
|
||||||
import {
|
import {
|
||||||
OrderWithAllTransactions,
|
|
||||||
OrderWithPair,
|
|
||||||
OrderWithPairAndCurrencies,
|
OrderWithPairAndCurrencies,
|
||||||
|
PairWithCurrencies,
|
||||||
|
PairWithIdAndCurrencies,
|
||||||
} from '@/interfaces/database/modifiedRequests.js';
|
} from '@/interfaces/database/modifiedRequests.js';
|
||||||
import configModel from './Config.js';
|
|
||||||
import dexModel from './Dex.js';
|
import dexModel from './Dex.js';
|
||||||
import userModel from './User.js';
|
import userModel from './User.js';
|
||||||
import exchangeModel from './ExchangeTransactions.js';
|
import exchangeModel from './ExchangeTransactions.js';
|
||||||
|
|
@ -22,10 +21,9 @@ import io from '../server.js';
|
||||||
import ApplyTip from '../interfaces/responses/orders/ApplyTip.js';
|
import ApplyTip from '../interfaces/responses/orders/ApplyTip.js';
|
||||||
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody.js';
|
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody.js';
|
||||||
import GetUserOrdersPageBody from '../interfaces/bodies/orders/GetUserOrdersPageBody.js';
|
import GetUserOrdersPageBody from '../interfaces/bodies/orders/GetUserOrdersPageBody.js';
|
||||||
import GetUserOrdersBody from '../interfaces/bodies/orders/GetUserOrdersBody.js';
|
|
||||||
import CancelOrderBody from '../interfaces/bodies/orders/CancelOrderBody.js';
|
import CancelOrderBody from '../interfaces/bodies/orders/CancelOrderBody.js';
|
||||||
import ApplyOrderBody from '../interfaces/bodies/orders/ApplyOrderBody.js';
|
import ApplyOrderBody from '../interfaces/bodies/orders/ApplyOrderBody.js';
|
||||||
import Order from '../schemes/Order';
|
import Order, { OrderStatus, OrderType } from '../schemes/Order';
|
||||||
import User from '../schemes/User';
|
import User from '../schemes/User';
|
||||||
import Transaction from '../schemes/Transaction';
|
import Transaction from '../schemes/Transaction';
|
||||||
import Pair from '../schemes/Pair';
|
import Pair from '../schemes/Pair';
|
||||||
|
|
@ -387,17 +385,85 @@ class OrdersModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserOrders(body: GetUserOrdersBody) {
|
async getUserOrders({
|
||||||
|
address,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
filterInfo: { pairId, status, type, date },
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
filterInfo: {
|
||||||
|
pairId?: number;
|
||||||
|
status?: 'active' | 'finished';
|
||||||
|
type?: 'buy' | 'sell';
|
||||||
|
date?: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}): Promise<
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
data: 'Internal error';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
totalItemsCount: number;
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
timestamp: number;
|
||||||
|
side: string;
|
||||||
|
price: string;
|
||||||
|
amount: string;
|
||||||
|
total: string;
|
||||||
|
pair_id: number;
|
||||||
|
user_id: number;
|
||||||
|
status: string;
|
||||||
|
left: string;
|
||||||
|
hasNotification: boolean;
|
||||||
|
|
||||||
|
pair: PairWithCurrencies;
|
||||||
|
|
||||||
|
first_currency: Currency;
|
||||||
|
second_currency: Currency;
|
||||||
|
isInstant: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
> {
|
||||||
try {
|
try {
|
||||||
const userRow = await userModel.getUserRow(body.userData.address);
|
const userRow = await userModel.getUserRow(address);
|
||||||
|
|
||||||
if (!userRow) throw new Error('Invalid address from token.');
|
if (!userRow) throw new Error('Invalid address from token.');
|
||||||
|
|
||||||
const orders = (await Order.findAll({
|
const ordersSelectWhereClause: WhereOptions = {
|
||||||
where: {
|
user_id: userRow.id,
|
||||||
user_id: userRow.id,
|
...(pairId !== undefined ? { pair_id: pairId } : {}),
|
||||||
},
|
...(status !== undefined
|
||||||
|
? {
|
||||||
|
status:
|
||||||
|
status === 'finished' ? OrderStatus.FINISHED : OrderStatus.ACTIVE,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(type !== undefined
|
||||||
|
? { type: type === 'buy' ? OrderType.BUY : OrderType.SELL }
|
||||||
|
: {}),
|
||||||
|
...(date !== undefined
|
||||||
|
? { timestamp: { [Op.between]: [date.from, date.to] } }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalItemsCount = await Order.count({
|
||||||
|
where: ordersSelectWhereClause,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ordersRows = (await Order.findAll({
|
||||||
|
where: ordersSelectWhereClause,
|
||||||
order: [['timestamp', 'DESC']],
|
order: [['timestamp', 'DESC']],
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Pair,
|
model: Pair,
|
||||||
|
|
@ -407,14 +473,32 @@ class OrdersModel {
|
||||||
],
|
],
|
||||||
})) as OrderWithPairAndCurrencies[];
|
})) as OrderWithPairAndCurrencies[];
|
||||||
|
|
||||||
const result = orders.map((e) => ({
|
const result = ordersRows.map((e) => ({
|
||||||
...e.toJSON(),
|
id: e.id,
|
||||||
|
type: e.type,
|
||||||
|
timestamp: e.timestamp,
|
||||||
|
side: e.side,
|
||||||
|
price: e.price,
|
||||||
|
amount: e.amount,
|
||||||
|
total: e.total,
|
||||||
|
pair_id: e.pair_id,
|
||||||
|
user_id: e.user_id,
|
||||||
|
status: e.status,
|
||||||
|
left: e.left,
|
||||||
|
hasNotification: e.hasNotification,
|
||||||
|
|
||||||
|
pair: e.pair,
|
||||||
|
|
||||||
first_currency: e.pair.first_currency,
|
first_currency: e.pair.first_currency,
|
||||||
second_currency: e.pair.second_currency,
|
second_currency: e.pair.second_currency,
|
||||||
isInstant: dexModel.isBotActive(e.id),
|
isInstant: dexModel.isBotActive(e.id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { success: true, data: result };
|
return {
|
||||||
|
success: true,
|
||||||
|
totalItemsCount,
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return { success: false, data: 'Internal error' };
|
return { success: false, data: 'Internal error' };
|
||||||
|
|
@ -728,6 +812,184 @@ class OrdersModel {
|
||||||
return { success: false, data: 'Internal error' };
|
return { success: false, data: 'Internal error' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GET_USER_ORDERS_ALL_PAIRS_USER_NOT_FOUND = 'No user found';
|
||||||
|
getUserOrdersAllPairs = async (
|
||||||
|
address: string,
|
||||||
|
): Promise<{
|
||||||
|
success: true;
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
firstCurrency: {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
};
|
||||||
|
secondCurrency: {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}> => {
|
||||||
|
const userRow = await userModel.getUserRow(address);
|
||||||
|
|
||||||
|
if (!userRow) {
|
||||||
|
throw new Error(OrdersModel.GET_USER_ORDERS_ALL_PAIRS_USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select distinct pair IDs for the user's orders, then fetch pairs
|
||||||
|
const distinctPairIdRows = (await Order.findAll({
|
||||||
|
attributes: [[sequelize.fn('DISTINCT', sequelize.col('pair_id')), 'pair_id']],
|
||||||
|
where: { user_id: userRow.id },
|
||||||
|
raw: true,
|
||||||
|
})) as { pair_id: number }[];
|
||||||
|
|
||||||
|
const pairIds = distinctPairIdRows.map((row) => row.pair_id);
|
||||||
|
|
||||||
|
const pairsSelection = (await Pair.findAll({
|
||||||
|
where: { id: pairIds },
|
||||||
|
include: [
|
||||||
|
{ model: Currency, as: 'first_currency' },
|
||||||
|
{ model: Currency, as: 'second_currency' },
|
||||||
|
],
|
||||||
|
attributes: ['id'],
|
||||||
|
})) as PairWithIdAndCurrencies[];
|
||||||
|
|
||||||
|
const pairs = pairsSelection.map((e) => {
|
||||||
|
const firstCurrencyTicker = e.first_currency.name;
|
||||||
|
const secondCurrencyTicker = e.second_currency.name;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: e.id,
|
||||||
|
firstCurrency: {
|
||||||
|
id: e.first_currency.id,
|
||||||
|
ticker: firstCurrencyTicker,
|
||||||
|
},
|
||||||
|
secondCurrency: {
|
||||||
|
id: e.second_currency.id,
|
||||||
|
ticker: secondCurrencyTicker,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: pairs,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
static CANCEL_ALL_USER_NOT_FOUND = 'No user found';
|
||||||
|
cancelAll = async (
|
||||||
|
{
|
||||||
|
address,
|
||||||
|
filterInfo: { pairId, type, date },
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
filterInfo: {
|
||||||
|
pairId?: number;
|
||||||
|
type?: 'buy' | 'sell';
|
||||||
|
date?: {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ transaction }: { transaction: SequelizeTransaction },
|
||||||
|
): Promise<{
|
||||||
|
success: true;
|
||||||
|
}> => {
|
||||||
|
const userRow = await userModel.getUserRow(address);
|
||||||
|
|
||||||
|
if (!userRow) {
|
||||||
|
throw new Error(OrdersModel.CANCEL_ALL_USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordersToCancelWhereClause: WhereOptions = {
|
||||||
|
user_id: userRow.id,
|
||||||
|
status: {
|
||||||
|
[Op.ne]: OrderStatus.FINISHED,
|
||||||
|
},
|
||||||
|
...(pairId !== undefined ? { pair_id: pairId } : {}),
|
||||||
|
...(type !== undefined
|
||||||
|
? { type: type === 'buy' ? OrderType.BUY : OrderType.SELL }
|
||||||
|
: {}),
|
||||||
|
...(date !== undefined ? { timestamp: { [Op.between]: [date.from, date.to] } } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ORDERS_PER_CANCEL_CHUNK = 10_000;
|
||||||
|
let lastOrderTimestamp: number | undefined;
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
while (!finished) {
|
||||||
|
const orderRows = await Order.findAll({
|
||||||
|
where: {
|
||||||
|
...ordersToCancelWhereClause,
|
||||||
|
...(lastOrderTimestamp !== undefined
|
||||||
|
? { timestamp: { [Op.gt]: lastOrderTimestamp } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
order: [['timestamp', 'ASC']],
|
||||||
|
limit: ORDERS_PER_CANCEL_CHUNK,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastOrderRow = orderRows.at(-1);
|
||||||
|
|
||||||
|
// if there are no more orders to cancel, finish the process
|
||||||
|
if (!lastOrderRow) {
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const orderRow of orderRows) {
|
||||||
|
await this.cancelOrderNotifications(orderRow, userRow);
|
||||||
|
|
||||||
|
const eps = new Decimal(1e-8);
|
||||||
|
const leftDecimal = new Decimal(orderRow.left);
|
||||||
|
const amountDecimal = new Decimal(orderRow.amount);
|
||||||
|
|
||||||
|
// if order was partially filled
|
||||||
|
if (leftDecimal.minus(amountDecimal).abs().greaterThan(eps)) {
|
||||||
|
const connectedTransactions = await Transaction.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ buy_order_id: orderRow.id },
|
||||||
|
{ sell_order_id: orderRow.id },
|
||||||
|
],
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const tx of connectedTransactions) {
|
||||||
|
await exchangeModel.returnTransactionAmount(tx.id, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Order.update(
|
||||||
|
{ status: OrderStatus.FINISHED },
|
||||||
|
{
|
||||||
|
where: { id: orderRow.id, user_id: userRow.id },
|
||||||
|
transaction,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await Order.destroy({
|
||||||
|
where: {
|
||||||
|
id: orderRow.id,
|
||||||
|
user_id: userRow.id,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.afterCommit(() => {
|
||||||
|
sendDeleteOrderMessage(io, orderRow.pair_id.toString(), orderRow.id.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastOrderRow) {
|
||||||
|
lastOrderTimestamp = lastOrderRow.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ordersModel = new OrdersModel();
|
const ordersModel = new OrdersModel();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import { createOrderValidator } from '@/interfaces/bodies/orders/CreateOrderBody.js';
|
import { createOrderValidator } from '@/interfaces/bodies/orders/CreateOrderBody.js';
|
||||||
|
import { getUserOrdersValidator } from '@/interfaces/bodies/orders/GetUserOrdersBody.js';
|
||||||
|
import { getUserOrdersAllPairsValidator } from '@/interfaces/bodies/orders/GetUserOrdersAllPairsBody.js';
|
||||||
|
import { cancelAllValidator } from '@/interfaces/bodies/orders/CancelAllBody.js';
|
||||||
import middleware from '../middleware/middleware.js';
|
import middleware from '../middleware/middleware.js';
|
||||||
import ordersController from '../controllers/orders.controller.js';
|
import ordersController from '../controllers/orders.controller.js';
|
||||||
|
|
||||||
|
|
@ -12,6 +16,8 @@ ordersRouter.use(
|
||||||
'/orders/get',
|
'/orders/get',
|
||||||
'/orders/cancel',
|
'/orders/cancel',
|
||||||
'/orders/apply-order',
|
'/orders/apply-order',
|
||||||
|
'/orders/get-user-orders-pairs',
|
||||||
|
'/orders/cancel-all',
|
||||||
],
|
],
|
||||||
middleware.verifyToken,
|
middleware.verifyToken,
|
||||||
);
|
);
|
||||||
|
|
@ -23,12 +29,26 @@ ordersRouter.post(
|
||||||
);
|
);
|
||||||
ordersRouter.post('/orders/get-page', ordersController.getOrdersPage);
|
ordersRouter.post('/orders/get-page', ordersController.getOrdersPage);
|
||||||
ordersRouter.post('/orders/get-user-page', ordersController.getUserOrdersPage);
|
ordersRouter.post('/orders/get-user-page', ordersController.getUserOrdersPage);
|
||||||
ordersRouter.post('/orders/get', ordersController.getUserOrders);
|
ordersRouter.patch(
|
||||||
|
'/orders/get',
|
||||||
|
middleware.expressValidator(getUserOrdersValidator),
|
||||||
|
ordersController.getUserOrders.bind(ordersController),
|
||||||
|
);
|
||||||
ordersRouter.post('/orders/cancel', ordersController.cancelOrder);
|
ordersRouter.post('/orders/cancel', ordersController.cancelOrder);
|
||||||
ordersRouter.post('/orders/get-candles', ordersController.getCandles);
|
ordersRouter.post('/orders/get-candles', ordersController.getCandles);
|
||||||
ordersRouter.post('/orders/get-chart-orders', ordersController.getChartOrders);
|
ordersRouter.post('/orders/get-chart-orders', ordersController.getChartOrders);
|
||||||
ordersRouter.post('/orders/get-pair-stats', ordersController.getPairStats);
|
ordersRouter.post('/orders/get-pair-stats', ordersController.getPairStats);
|
||||||
ordersRouter.post('/orders/apply-order', ordersController.applyOrder);
|
ordersRouter.post('/orders/apply-order', ordersController.applyOrder);
|
||||||
ordersRouter.post('/orders/get-trades', ordersController.getTrades);
|
ordersRouter.post('/orders/get-trades', ordersController.getTrades);
|
||||||
|
ordersRouter.patch(
|
||||||
|
'/orders/get-user-orders-pairs',
|
||||||
|
middleware.expressValidator(getUserOrdersAllPairsValidator),
|
||||||
|
ordersController.getUserOrdersAllPairs.bind(ordersController),
|
||||||
|
);
|
||||||
|
ordersRouter.patch(
|
||||||
|
'/orders/cancel-all',
|
||||||
|
middleware.expressValidator(cancelAllValidator),
|
||||||
|
ordersController.cancelAll.bind(ordersController),
|
||||||
|
);
|
||||||
|
|
||||||
export default ordersRouter;
|
export default ordersRouter;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
import { Model, DataTypes } from 'sequelize';
|
import { Model, DataTypes } from 'sequelize';
|
||||||
import sequelize from '../sequelize';
|
import sequelize from '../sequelize';
|
||||||
|
|
||||||
|
export enum OrderType {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
BUY = 'buy',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
SELL = 'sell',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrderStatus {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
ACTIVE = 'active',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
ZERO = 'zero',
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
FINISHED = 'finished',
|
||||||
|
}
|
||||||
|
|
||||||
class Order extends Model {
|
class Order extends Model {
|
||||||
declare readonly id: number;
|
declare readonly id: number;
|
||||||
|
|
||||||
|
|
@ -8,6 +24,7 @@ class Order extends Model {
|
||||||
|
|
||||||
declare timestamp: number;
|
declare timestamp: number;
|
||||||
|
|
||||||
|
// Currently not used
|
||||||
declare side: string;
|
declare side: string;
|
||||||
|
|
||||||
declare price: string;
|
declare price: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue