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 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 ordersModel from '../models/Orders';
|
||||
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody';
|
||||
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 GetCandlesBody from '../interfaces/bodies/orders/GetCandlesBody';
|
||||
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 {
|
||||
await userModel.resetExchangeNotificationsAmount(
|
||||
(req.body.userData as UserData).address,
|
||||
);
|
||||
const result = await ordersModel.getUserOrders(req.body as GetUserOrdersBody);
|
||||
const body = req.body as GetUserOrdersBody;
|
||||
const { userData, offset, limit, filterInfo } = body;
|
||||
|
||||
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) {
|
||||
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) {
|
||||
try {
|
||||
|
|
@ -289,6 +455,51 @@ class OrdersController {
|
|||
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();
|
||||
|
|
|
|||
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 {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -32,3 +32,7 @@ export interface PairWithCurrencies extends Pair {
|
|||
export interface OrderWithPairAndCurrencies extends Order {
|
||||
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 TransactionWithOrders from '@/interfaces/common/Transaction.js';
|
||||
import Currency from '@/schemes/Currency.js';
|
||||
import {
|
||||
OrderWithAllTransactions,
|
||||
OrderWithPair,
|
||||
OrderWithPairAndCurrencies,
|
||||
PairWithCurrencies,
|
||||
PairWithIdAndCurrencies,
|
||||
} from '@/interfaces/database/modifiedRequests.js';
|
||||
import configModel from './Config.js';
|
||||
import dexModel from './Dex.js';
|
||||
import userModel from './User.js';
|
||||
import exchangeModel from './ExchangeTransactions.js';
|
||||
|
|
@ -22,10 +21,9 @@ import io from '../server.js';
|
|||
import ApplyTip from '../interfaces/responses/orders/ApplyTip.js';
|
||||
import CreateOrderBody from '../interfaces/bodies/orders/CreateOrderBody.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 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 Transaction from '../schemes/Transaction';
|
||||
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 {
|
||||
const userRow = await userModel.getUserRow(body.userData.address);
|
||||
const userRow = await userModel.getUserRow(address);
|
||||
|
||||
if (!userRow) throw new Error('Invalid address from token.');
|
||||
|
||||
const orders = (await Order.findAll({
|
||||
where: {
|
||||
const ordersSelectWhereClause: WhereOptions = {
|
||||
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']],
|
||||
limit,
|
||||
offset,
|
||||
include: [
|
||||
{
|
||||
model: Pair,
|
||||
|
|
@ -407,14 +473,32 @@ class OrdersModel {
|
|||
],
|
||||
})) as OrderWithPairAndCurrencies[];
|
||||
|
||||
const result = orders.map((e) => ({
|
||||
...e.toJSON(),
|
||||
const result = ordersRows.map((e) => ({
|
||||
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,
|
||||
second_currency: e.pair.second_currency,
|
||||
isInstant: dexModel.isBotActive(e.id),
|
||||
}));
|
||||
|
||||
return { success: true, data: result };
|
||||
return {
|
||||
success: true,
|
||||
totalItemsCount,
|
||||
data: result,
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return { success: false, data: 'Internal error' };
|
||||
|
|
@ -728,6 +812,184 @@ class OrdersModel {
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import express from 'express';
|
||||
|
||||
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 ordersController from '../controllers/orders.controller.js';
|
||||
|
||||
|
|
@ -12,6 +16,8 @@ ordersRouter.use(
|
|||
'/orders/get',
|
||||
'/orders/cancel',
|
||||
'/orders/apply-order',
|
||||
'/orders/get-user-orders-pairs',
|
||||
'/orders/cancel-all',
|
||||
],
|
||||
middleware.verifyToken,
|
||||
);
|
||||
|
|
@ -23,12 +29,26 @@ ordersRouter.post(
|
|||
);
|
||||
ordersRouter.post('/orders/get-page', ordersController.getOrdersPage);
|
||||
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/get-candles', ordersController.getCandles);
|
||||
ordersRouter.post('/orders/get-chart-orders', ordersController.getChartOrders);
|
||||
ordersRouter.post('/orders/get-pair-stats', ordersController.getPairStats);
|
||||
ordersRouter.post('/orders/apply-order', ordersController.applyOrder);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,22 @@
|
|||
import { Model, DataTypes } 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 {
|
||||
declare readonly id: number;
|
||||
|
||||
|
|
@ -8,6 +24,7 @@ class Order extends Model {
|
|||
|
||||
declare timestamp: number;
|
||||
|
||||
// Currently not used
|
||||
declare side: string;
|
||||
|
||||
declare price: string;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue