Merge pull request #23 from hyle-team/dev

This commit is contained in:
Dmitrii Kolpakov 2026-02-25 11:48:02 +01:00 committed by GitHub
commit e4bd5e65df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1481 additions and 170 deletions

14
package-lock.json generated
View file

@ -15,6 +15,7 @@
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^8.2.1",
"express-validator": "^7.3.1",
"jimp": "^0.22.8",
"jsonwebtoken": "^9.0.0",
"nanoid": "^5.1.5",
@ -4280,6 +4281,19 @@
"express": ">= 4.11"
}
},
"node_modules/express-validator": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz",
"integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"validator": "~13.15.23"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View file

@ -24,6 +24,7 @@
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^8.2.1",
"express-validator": "^7.3.1",
"jimp": "^0.22.8",
"jsonwebtoken": "^9.0.0",
"nanoid": "^5.1.5",

3
shared/constants.ts Normal file
View file

@ -0,0 +1,3 @@
export const NON_NEGATIVE_REAL_NUMBER_REGEX = /^\d+(\.\d+)?$/;
export const AUTH_MESSAGE_EXPIRATION_TIME_MS = 5 * 60 * 1000; // 5 minutes

View file

@ -1,13 +1,39 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import { Request, Response } from 'express';
import crypto from 'crypto';
import AuthData from '@/interfaces/bodies/user/AuthData.js';
import RequestAuthBody from '@/interfaces/bodies/auth/RequestAuthBody.js';
import authMessagesModel from '@/models/AuthMessages.js';
import { AUTH_MESSAGE_EXPIRATION_TIME_MS } from 'shared/constants.js';
import RequestAuthRes from '@/interfaces/responses/auth/RequestAuthRes.js';
import sequelize from '@/sequelize.js';
import validateWallet from '../methods/validateWallet.js';
import userModel from '../models/User.js';
dotenv.config();
class AuthController {
requestAuth = async (req: Request, res: Response<RequestAuthRes>) => {
const { address, alias } = req.body as RequestAuthBody;
const message = crypto.randomUUID();
const expiresAt = new Date(Date.now() + AUTH_MESSAGE_EXPIRATION_TIME_MS);
const authMessageRow = await authMessagesModel.create({
address,
alias,
message,
expiresAt,
});
return res.status(200).send({
success: true,
data: authMessageRow.message,
});
};
async auth(req: Request, res: Response) {
try {
const userData: AuthData = req.body.data;
@ -18,6 +44,19 @@ class AuthController {
return res.status(400).send({ success: false, data: 'Invalid auth data' });
}
const authMessageRow = await authMessagesModel.findOne({
address,
alias,
message,
});
const isAuthMessageValid =
!!authMessageRow && authMessageRow.expires_at.getTime() > Date.now();
if (!isAuthMessageValid) {
return res.status(400).send({ success: false, data: 'Invalid auth message' });
}
const dataValid = !!(
userData &&
userData.address &&
@ -28,17 +67,33 @@ class AuthController {
return res.status(400).send({ success: false, data: 'Invalid auth data' });
}
const success = await userModel.add(userData);
let token: string | undefined;
if (success) {
const token = jwt.sign(
{ ...userData },
process.env.JWT_SECRET || '',
neverExpires ? undefined : { expiresIn: '24h' },
);
res.status(200).send({ success, data: token });
await sequelize.transaction(async (transaction) => {
const success = await userModel.add(userData, { transaction });
if (success) {
await authMessagesModel.deleteOne(
{
address,
alias,
message,
},
{ transaction },
);
token = jwt.sign(
{ ...userData },
process.env.JWT_SECRET || '',
neverExpires ? undefined : { expiresIn: '24h' },
);
}
});
if (token !== undefined) {
res.status(200).send({ success: true, data: token });
} else {
res.status(500).send({ success, data: 'Internal error' });
res.status(500).send({ success: false, data: 'Internal error' });
}
} catch (err) {
console.log(err);

View file

@ -3,6 +3,10 @@ import UserData from '@/interfaces/common/UserData.js';
import Currency from '@/schemes/Currency.js';
import Pair from '@/schemes/Pair.js';
import { Op } from 'sequelize';
import GetAssetsPriceRatesBody from '@/interfaces/bodies/dex/GetAssetsPriceRatesBody.js';
import GetAssetsPriceRatesRes, {
GetAssetsPriceRatesResPriceRate,
} from '@/interfaces/responses/dex/GetAssetsPriceRatesRes.js';
import User from '../schemes/User.js';
import ordersModel from '../models/Orders.js';
import dexModel from '../models/Dex.js';
@ -104,10 +108,10 @@ class DexController {
return res.status(200).send(result);
}
async getAssetsPriceRates(req: Request, res: Response) {
const { assetsIds } = req.body;
getAssetsPriceRates = async (req: Request, res: Response<GetAssetsPriceRatesRes>) => {
const { assetsIds } = req.body as GetAssetsPriceRatesBody;
const currencysRows = await Currency.findAll({
const currenciesRows = await Currency.findAll({
where: {
asset_id: {
[Op.in]: assetsIds,
@ -115,61 +119,42 @@ class DexController {
},
});
if (!currencysRows) {
return res.status(200).send({
success: false,
data: 'Assets with this id doesn`t exists',
});
}
const currencyIds = currenciesRows.map((currency) => currency.id);
const currencyIds = currencysRows.map((currency) => currency.id);
const pairsRows = (
(await Pair.findAll({
where: {
first_currency_id: {
[Op.in]: currencyIds,
},
const pairsRows = (await Pair.findAll({
where: {
first_currency_id: {
[Op.in]: currencyIds,
},
include: [
{
model: Currency,
as: 'first_currency',
required: true,
attributes: ['asset_id'],
},
],
})) || []
).map((pair) => ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
asset_id: pair?.first_currency?.asset_id,
rate: pair.rate,
}));
},
include: [
{
model: Currency,
as: 'first_currency',
required: true,
attributes: ['asset_id'],
},
],
})) as (Pair & { first_currency: Currency })[];
if (!pairsRows || pairsRows.length === 0) {
return res.status(200).send({
success: false,
data: 'Assets with this id doesn`t exists',
});
}
const priceRates: GetAssetsPriceRatesResPriceRate[] = pairsRows.map((pairRow) => {
const assetId = pairRow.first_currency.asset_id;
// const priceRates = await Promise.all(pairsRows.map(async (pair) => {
// const currency = await Currency.findOne({ where: {
// id: pair.first_currency_id
// }})
// return {
// asset_id: currency?.asset_id,
// rate: pair.rate
// }
// }))
return {
asset_id: assetId,
rate: pairRow?.rate ?? null,
day_change: pairRow?.coefficient ?? null,
day_volume: pairRow?.volume ?? null,
day_high: pairRow?.high ?? null,
day_low: pairRow?.low ?? null,
};
});
return res.status(200).send({
success: true,
priceRates: pairsRows,
priceRates,
});
}
};
async findPairID(req: Request, res: Response) {
const { first, second } = req.body;

View file

@ -1,10 +1,28 @@
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';
@ -16,70 +34,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 });
}
}
@ -128,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 {
@ -260,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,13 @@
import { body } from 'express-validator';
interface RequestAuthBody {
address: string;
alias: string;
}
export const requestAuthBodyValidator = [
body('address').isString().notEmpty(),
body('alias').isString().notEmpty(),
];
export default RequestAuthBody;

View file

@ -0,0 +1,14 @@
import { body } from 'express-validator';
interface GetAssetsPriceRatesBody {
assetsIds: string[];
}
export const getAssetsPriceRatesValidator = [
body('assetsIds')
.isArray({ min: 1 })
.withMessage('assetsIds must be a non-empty array of strings'),
body('assetsIds.*').isString().withMessage('Each assetId must be a string'),
];
export default GetAssetsPriceRatesBody;

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

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

View file

@ -0,0 +1,9 @@
import UserData from '@/interfaces/common/UserData';
interface GetUserOrdersAllPairsBody {
userData: UserData;
}
export const getUserOrdersAllPairsValidator = [];
export default GetUserOrdersAllPairsBody;

View file

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

View file

@ -32,3 +32,7 @@ export interface PairWithCurrencies extends Pair {
export interface OrderWithPairAndCurrencies extends Order {
pair: PairWithCurrencies;
}
export interface PairWithIdAndCurrencies extends PairWithCurrencies {
id: number;
}

View file

@ -0,0 +1,7 @@
interface RequestAuthRes {
success: true;
// Auth message
data: string;
}
export default RequestAuthRes;

View file

@ -0,0 +1,24 @@
export type GetAssetsPriceRatesResPriceRate = {
asset_id: string;
rate: number | null;
day_change: number | null;
day_volume: number | null;
day_high: number | null;
day_low: number | null;
};
export type GetAssetsPriceRatesSuccessRes = {
success: true;
priceRates: GetAssetsPriceRatesResPriceRate[];
};
export enum GetAssetsPriceRatesErrorCode {}
export type GetAssetsPriceRatesErrorRes = {
success: false;
data: GetAssetsPriceRatesErrorCode;
};
type GetAssetsPriceRatesRes = GetAssetsPriceRatesSuccessRes | GetAssetsPriceRatesErrorRes;
export default GetAssetsPriceRatesRes;

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

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

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

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

View file

@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express';
import { ValidationChain, validationResult } from 'express-validator';
import { rateLimit } from 'express-rate-limit';
import jwt from 'jsonwebtoken';
import User from '@/schemes/User';
@ -49,6 +50,49 @@ class Middleware {
defaultRateLimit = async (req: Request, res: Response, next: NextFunction) =>
defaultRateLimitMiddleware(req, res, next);
expressValidator(validators: ValidationChain[]) {
return [
...validators,
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(500).send({
success: false,
data: 'Internal error',
});
return;
}
next();
},
];
}
expressJSONErrorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
const isExpressJSONError =
err instanceof SyntaxError && 'status' in err && err.status === 400 && 'body' in err;
if (isExpressJSONError) {
res.status(500).send({
success: false,
data: 'Internal error',
});
} else {
next();
}
};
globalErrorHandler = (err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error('Global error handler:');
console.error(err);
res.status(500).send({
success: false,
data: 'Internal error',
});
};
resultGlobalErrorHandler = [this.expressJSONErrorHandler, this.globalErrorHandler];
}
const middleware = new Middleware();

View file

@ -0,0 +1,89 @@
import { Op, Transaction } from 'sequelize';
import AuthMessage from '@/schemes/AuthMessage';
class AuthMessagesModel {
create = async (
{
address,
alias,
message,
expiresAt,
}: {
address: string;
alias: string;
message: string;
expiresAt: Date;
},
{ transaction }: { transaction?: Transaction } = {},
): Promise<AuthMessage> => {
const authMessage = await AuthMessage.create(
{
address,
alias,
message,
expires_at: expiresAt,
},
{ transaction },
);
return authMessage;
};
findOne = async ({
address,
alias,
message,
}: {
address: string;
alias: string;
message: string;
}): Promise<AuthMessage | null> =>
AuthMessage.findOne({
where: {
address,
alias,
message,
},
});
deleteOne = async (
{
address,
alias,
message,
}: {
address: string;
alias: string;
message: string;
},
{ transaction }: { transaction?: Transaction } = {},
): Promise<void> => {
await AuthMessage.destroy({
where: {
address,
alias,
message,
},
transaction,
});
};
deleteAllExpired = async (
{ now }: { now: Date },
{ transaction }: { transaction?: Transaction } = {},
): Promise<void> => {
await AuthMessage.destroy({
where: {
expires_at: {
[Op.lt]: now,
},
},
transaction,
});
};
}
const authMessagesModel = new AuthMessagesModel();
export default authMessagesModel;

View file

@ -38,8 +38,58 @@ class DexModel {
}
private getPairsSearchCondition(searchText: string, whitelistedOnly: boolean) {
const tickerRegexp = /^[A-Za-z0-9]{1,14}$/;
const fullNameRegexp = /^[A-Za-z0-9.,:!?\-() ]*$/;
const firstFullNameExpr = Sequelize.literal(`"first_currency"."asset_info"->>'full_name'`);
const secondFullNameExpr = Sequelize.literal(
`"second_currency"."asset_info"->>'full_name'`,
);
const searchCondition: WhereOptions = {
[Op.and]: [
Sequelize.where(Sequelize.col('first_currency.name'), {
[Op.regexp]: tickerRegexp.source,
}),
{
[Op.or]: [
Sequelize.where(Sequelize.col('first_currency.asset_info'), {
[Op.is]: null,
}),
Sequelize.where(firstFullNameExpr, { [Op.is]: null }),
{
[Op.and]: [
Sequelize.where(firstFullNameExpr, {
[Op.regexp]: fullNameRegexp.source,
}),
Sequelize.where(Sequelize.fn('char_length', firstFullNameExpr), {
[Op.lte]: 400,
}),
],
},
],
},
Sequelize.where(Sequelize.col('second_currency.name'), {
[Op.regexp]: tickerRegexp.source,
}),
{
[Op.or]: [
Sequelize.where(Sequelize.col('second_currency.asset_info'), {
[Op.is]: null,
}),
Sequelize.where(secondFullNameExpr, { [Op.is]: null }),
{
[Op.and]: [
Sequelize.where(secondFullNameExpr, {
[Op.regexp]: fullNameRegexp.source,
}),
Sequelize.where(Sequelize.fn('char_length', secondFullNameExpr), {
[Op.lte]: 400,
}),
],
},
],
},
{
[Op.or]: [
Sequelize.where(
@ -70,7 +120,20 @@ class DexModel {
],
};
return searchCondition;
const includeCondition = [
{
model: Currency,
as: 'first_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type', 'whitelisted'],
},
{
model: Currency,
as: 'second_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type', 'whitelisted'],
},
];
return { searchCondition, includeCondition };
}
async getPairRow(id: number) {
@ -88,7 +151,10 @@ class DexModel {
sortOption: PairSortOption,
) {
try {
const searchCondition = this.getPairsSearchCondition(searchText, whitelistedOnly);
const { searchCondition, includeCondition } = this.getPairsSearchCondition(
searchText,
whitelistedOnly,
);
const volumeSortDirection =
sortOption === PairSortOption.VOLUME_LOW_TO_HIGH ? 'ASC' : 'DESC';
@ -105,18 +171,7 @@ class DexModel {
'volume',
'featured',
],
include: [
{
model: Currency,
as: 'first_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type', 'whitelisted'],
},
{
model: Currency,
as: 'second_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type', 'whitelisted'],
},
],
include: includeCondition,
where: searchCondition,
order: [
['volume', volumeSortDirection],
@ -152,21 +207,13 @@ class DexModel {
async getPairsPagesAmount(searchText: string, whitelistedOnly: boolean) {
try {
const searchCondition = this.getPairsSearchCondition(searchText, whitelistedOnly);
const { searchCondition, includeCondition } = this.getPairsSearchCondition(
searchText,
whitelistedOnly,
);
const count = await Pair.count({
include: [
{
model: Currency,
as: 'first_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type'],
},
{
model: Currency,
as: 'second_currency',
attributes: ['asset_id', 'code', 'id', 'name', 'type'],
},
],
include: includeCondition,
where: searchCondition,
});

View file

@ -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';
@ -68,7 +66,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 +194,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' };
@ -336,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: {
user_id: userRow.id,
},
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,
@ -356,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' };
@ -677,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();

View file

@ -1,4 +1,4 @@
import { Op } from 'sequelize';
import { Op, Transaction } from 'sequelize';
import Offer from '../schemes/Offer';
import GetUserBody from '../interfaces/bodies/user/GetUserBody';
import SetFavouriteCurrsBody from '../interfaces/bodies/user/SetFavouriteCurrsBody';
@ -15,7 +15,7 @@ class UserModel {
return selected;
}
async add(userData: UserData) {
async add(userData: UserData, { transaction }: { transaction?: Transaction } = {}) {
try {
const userRow = await this.getUserRow(userData.address);
if (userRow) return true;
@ -27,12 +27,15 @@ class UserModel {
if (oldAddressOfCurrentAlias) {
await User.update(
{ address: userData.address },
{ where: { alias: userData.alias } },
{ where: { alias: userData.alias }, transaction },
);
return true;
}
await User.create({ alias: userData.alias, address: userData.address });
await User.create(
{ alias: userData.alias, address: userData.address },
{ transaction },
);
return true;
} catch (err) {

View file

@ -1,8 +1,16 @@
import express from 'express';
import middleware from '@/middleware/middleware.js';
import { requestAuthBodyValidator } from '@/interfaces/bodies/auth/RequestAuthBody.js';
import authController from '../controllers/auth.controller.js';
const authRouter = express.Router();
authRouter.post(
'/auth/request-auth',
middleware.expressValidator(requestAuthBodyValidator),
authController.requestAuth.bind(authController),
);
authRouter.post('/auth', authController.auth);
export default authRouter;

View file

@ -1,4 +1,5 @@
import express from 'express';
import { getAssetsPriceRatesValidator } from '@/interfaces/bodies/dex/GetAssetsPriceRatesBody.js';
import dexController from '../controllers/dex.controller.js';
import middleware from '../middleware/middleware.js';
@ -9,7 +10,11 @@ dexRouter.post('/dex/get-pairs-pages-amount', dexController.getPairsPagesAmount)
dexRouter.post('/dex/get-pair', dexController.getPair);
dexRouter.post('/dex/renew-bot', middleware.verifyToken, dexController.registerBot);
dexRouter.post('/dex/volume-stats', dexController.volumeStats);
dexRouter.post('/dex/get-assets-price-rates', dexController.getAssetsPriceRates);
dexRouter.post(
'/dex/get-assets-price-rates',
middleware.expressValidator(getAssetsPriceRatesValidator),
dexController.getAssetsPriceRates,
);
dexRouter.post('/dex/find-pair', dexController.findPairID);
export default dexRouter;

View file

@ -1,4 +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';
@ -11,19 +16,39 @@ ordersRouter.use(
'/orders/get',
'/orders/cancel',
'/orders/apply-order',
'/orders/get-user-orders-pairs',
'/orders/cancel-all',
],
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);
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;

View file

@ -0,0 +1,45 @@
import sequelize from '@/sequelize';
import { DataTypes, Model } from 'sequelize';
class AuthMessage extends Model {
declare readonly id: number;
declare address: string;
declare alias: string;
declare message: string;
declare expires_at: Date;
declare readonly createdAt: Date;
declare readonly updatedAt: Date;
}
AuthMessage.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
address: {
type: DataTypes.STRING,
allowNull: false,
},
alias: {
type: DataTypes.STRING,
allowNull: false,
},
message: {
type: DataTypes.STRING,
allowNull: false,
},
expires_at: {
type: DataTypes.DATE,
allowNull: false,
},
},
{
sequelize,
modelName: 'AuthMessage',
},
);
export default AuthMessage;

View file

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

View file

@ -3,6 +3,7 @@ import express from 'express';
import http from 'http';
import { Server } from 'socket.io';
import authMessagesCleanService from '@/workers/authMessagesCleanService';
import authRouter from './routes/auth.router';
import offersRouter from './routes/offers.router';
import userRouter from './routes/user.router';
@ -73,6 +74,7 @@ process.on('unhandledRejection', (reason, promise) => {
assetsUpdateChecker.run();
ordersModerationService.run();
authMessagesCleanService.run();
exchangeModel.runPairStatsDaemon();
statsModel.init();
@ -101,6 +103,8 @@ process.on('unhandledRejection', (reason, promise) => {
res.send({ success: true, userData: req.body.userData }),
);
app.use(middleware.resultGlobalErrorHandler);
server.listen(PORT, () => console.log(`Server is running on port ${PORT}`));
})();

View file

@ -0,0 +1,41 @@
import authMessagesModel from '@/models/AuthMessages';
const CLEAN_INTERVAL = 60 * 60 * 1000; // 1 hour
class AuthMessagesCleanService {
run = async () => {
console.log(
`Auth messages clean service is running. Cleaning interval: ${CLEAN_INTERVAL / 1000} sec.`,
);
async function clean() {
console.log(`[${new Date()}] Cleaning auth messages...`);
await authMessagesModel.deleteAllExpired({ now: new Date() });
console.log(`[${new Date()}] Auth messages cleaned.`);
}
// eslint-disable-next-line no-constant-condition
while (true) {
try {
await clean();
} catch (error) {
console.log(
`[${new Date()}] Error while cleaning auth messages. Continuing on next iteration. Error:`,
);
console.error(error);
}
console.log(
`[${new Date()}] Auth messages cleaned. Next cleaning in ${CLEAN_INTERVAL / 1000} sec.`,
);
await new Promise((resolve) => setTimeout(resolve, CLEAN_INTERVAL));
}
};
}
const authMessagesCleanService = new AuthMessagesCleanService();
export default authMessagesCleanService;