add: add orders cancel-all endpoint

This commit is contained in:
Andrew Besedin 2026-02-18 02:37:27 +03:00
parent ac430e0d17
commit afe5ce2fa3
5 changed files with 230 additions and 1 deletions

View file

@ -12,6 +12,9 @@ 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';
@ -452,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();

View 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;

View 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;

View file

@ -1,4 +1,4 @@
import { Op, WhereOptions } 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';
@ -876,6 +876,114 @@ class OrdersModel {
data: pairs,
};
};
static CANCEL_ALL_USER_NOT_FOUND = 'No user found';
static CANCEL_ALL_ORDER_NOT_FOUND = 'Order not found during cancel all process';
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 ordersToCancelCount = await Order.count({
where: ordersToCancelWhereClause,
});
const cancelPromises = [];
for (let offset = 0; offset < ordersToCancelCount; offset += 1) {
cancelPromises.push(async () => {
const orderRow = await Order.findOne({
where: ordersToCancelWhereClause,
order: [['timestamp', 'ASC']],
offset,
limit: 1,
});
if (!orderRow) {
throw new Error(OrdersModel.CANCEL_ALL_ORDER_NOT_FOUND);
}
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());
});
});
}
await Promise.all(cancelPromises);
return { success: true };
};
}
const ordersModel = new OrdersModel();

View file

@ -3,6 +3,7 @@ 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';
@ -43,5 +44,10 @@ ordersRouter.patch(
middleware.expressValidator(getUserOrdersAllPairsValidator),
ordersController.getUserOrdersAllPairs.bind(ordersController),
);
ordersRouter.patch(
'/orders/cancel-all',
middleware.expressValidator(cancelAllValidator),
ordersController.cancelAll.bind(ordersController),
);
export default ordersRouter;