trade-backend/src/models/ExchangeTransactions.ts
Claude 1623966c32
rebrand(lethean): update branding, ports, and config for Lethean blockchain
- Coin: Zano → Lethean, ticker: ZAN/ZANO → LTHN
- Ports: 11211 → 36941 (mainnet RPC), 46941 (testnet RPC)
- Wallet: 11212 → 36944/46944
- Address prefix: iTHN
- URLs: zano.org → lethean.io
- Explorer links: explorer.lthn.io

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:24:07 +01:00

543 lines
14 KiB
TypeScript

import Decimal from 'decimal.js';
import { Op } from 'sequelize';
import type { Transaction as SequelizeTransaction } from 'sequelize';
import CancelTransactionBody from '@/interfaces/bodies/exchange-transactions/CancelTransactionBody.js';
import sequelize from '@/sequelize.js';
import { sendDeleteOrderMessage, sendUpdatePairStatsMessage } from '../socket/main.js';
import ordersModel from './Orders.js';
import userModel from './User.js';
import io from '../server.js';
import ConfirmTransactionBody from '../interfaces/bodies/exchange-transactions/ConfirmTransactionBody.js';
import Transaction from '../schemes/Transaction';
import Order from '../schemes/Order';
import Pair from '../schemes/Pair.js';
interface OrderWithTransactions extends Order {
buy_orders: Transaction[];
sell_orders: Transaction[];
}
const PRICE_BASE_URL = 'https://explorer.lethean.org/api/get_historical_lethean_price?timestamp=';
class ExchangeModel {
private lthn_price_data: {
now: string | null;
back24hr: string | null;
} = {
now: null,
back24hr: null,
};
constructor() {
this.runLtheanPriceDaemon();
}
getLtheanPriceData() {
return this.lthn_price_data;
}
async getLtheanPriceForTimestamp(timestamp: number) {
try {
const priceData = await fetch(`${PRICE_BASE_URL}${timestamp}`).then((res) =>
res.json(),
);
const priceParsed = priceData?.data?.price;
if (!priceParsed) {
console.log(priceData);
throw new Error('Failed to fetch Lethean price data for timestamp');
}
return { success: true, data: priceParsed };
} catch (error) {
console.log(error);
return { success: false, data: 'Internal error' };
}
}
async updateLtheanPrice() {
try {
const priceDataNow = await fetch(`${PRICE_BASE_URL}${Date.now()}`).then((res) =>
res.json(),
);
const priceDataBack24hr = await fetch(
`${PRICE_BASE_URL}${Date.now() - 24 * 60 * 60 * 1000}`,
).then((res) => res.json());
const priceNowParsed = priceDataNow?.data?.price;
const priceBack24hrParsed = priceDataBack24hr?.data?.price;
if (!priceNowParsed || !priceBack24hrParsed) {
console.log(priceDataNow, priceDataBack24hr);
throw new Error('Failed to fetch Lethean price data');
}
this.lthn_price_data = {
now: priceNowParsed,
back24hr: priceBack24hrParsed,
};
} catch (error) {
console.log(error);
}
}
async runLtheanPriceDaemon() {
while (true) {
await this.updateLtheanPrice();
await new Promise((resolve) => setTimeout(resolve, 30 * 1000));
}
}
async runPairStatsDaemon() {
(async () => {
while (true) {
console.log('Running pair stats update...');
const date = +new Date();
try {
const pairs = await Pair.findAll({
attributes: ['id'],
});
for (const pair of pairs) {
const statsResult = await this.calculatePairStats(pair.id.toString());
if (!statsResult.success || typeof statsResult.data === 'string') {
throw new Error('Error while getting pair stats');
}
const stats = statsResult.data;
await Pair.update(
{
rate: stats.rate,
coefficient: stats.coefficient,
high: stats.high,
low: stats.low,
volume: stats.volume,
},
{
where: {
id: pair.id,
},
},
);
sendUpdatePairStatsMessage(io, pair.id.toString(), stats);
}
} catch (error) {
console.log(error);
}
console.log(
`Pair stats update completed in ${Math.floor((+new Date() - date) / 1000)}s`,
);
await new Promise((resolve) => setTimeout(resolve, 1000 * 60 * 5));
}
})();
}
private async calculatePairStats(pairId: string) {
try {
if (!this.lthn_price_data.now || !this.lthn_price_data.back24hr) {
await this.updateLtheanPrice();
if (!this.lthn_price_data.now || !this.lthn_price_data.back24hr) {
throw new Error('Failed to fetch Lethean price data');
}
}
const date = new Date();
const lastTimestamp = date.getTime();
date.setHours(date.getHours() - 24);
const firstTimestamp = date.getTime();
const orders = (await Order.findAll({
where: {
pair_id: pairId,
timestamp: {
[Op.gte]: firstTimestamp,
[Op.lte]: lastTimestamp,
},
},
include: [
{
model: Transaction,
as: 'buy_orders',
attributes: ['buy_order_id', 'sell_order_id', 'amount', 'timestamp'],
required: true,
where: {
status: 'confirmed',
},
order: [['timestamp', 'ASC']],
},
],
order: [['timestamp', 'ASC']],
})) as OrderWithTransactions[];
const allTransactionsWithPrices = orders
.flatMap((order) =>
order.buy_orders.map((transaction) => {
const buyOrderPrice = order.price;
return {
...transaction.toJSON(),
buy_order_price: buyOrderPrice,
};
}),
)
.sort((a, b) => a.timestamp - b.timestamp);
const firstOrderPrice = allTransactionsWithPrices[0]?.buy_order_price || NaN;
const lastOrderPrice = allTransactionsWithPrices.at(-1)?.buy_order_price || NaN;
const firstPriceInUSD = new Decimal(firstOrderPrice || '0').mul(
new Decimal(this.lthn_price_data.back24hr || '1'),
);
const lastPriceInUSD = new Decimal(lastOrderPrice || '0').mul(
new Decimal(this.lthn_price_data.now || '1'),
);
const change_coefficient = lastPriceInUSD
.minus(firstPriceInUSD || '0')
.div(firstPriceInUSD || '1')
.mul(100)
.toNumber();
const prices = allTransactionsWithPrices.map((e) => e.buy_order_price);
const lastTradedOrder = await Order.findOne({
where: {
pair_id: pairId,
},
include: [
{
model: Transaction,
as: 'buy_orders',
attributes: [],
required: true,
where: {
status: 'confirmed',
},
},
],
order: [['timestamp', 'DESC']],
});
const lastKnownPrice = new Decimal(lastTradedOrder?.price || '0').toNumber();
const data = {
rate: new Decimal(lastKnownPrice || '0').toNumber(),
coefficient: change_coefficient,
high: 0,
low: 0,
volume: 0,
};
if (prices.length > 0) {
data.high = Decimal.max(...prices).toNumber();
data.low = Decimal.min(...prices).toNumber();
} else {
data.high = lastKnownPrice;
data.low = lastKnownPrice;
}
for (const transaction of allTransactionsWithPrices) {
data.volume += new Decimal(transaction.amount)
.mul(transaction.buy_order_price)
.toNumber();
}
return { success: true, data };
} catch (err) {
console.log(err);
return { success: false, data: 'Internal error' };
}
}
async returnTransactionAmount(
transactionId: number,
sequelizeTransaction?: SequelizeTransaction,
) {
const transactionRow = await Transaction.findByPk(transactionId, {
transaction: sequelizeTransaction,
lock: sequelizeTransaction?.LOCK?.UPDATE,
});
if (!transactionRow) return console.error('Transaction row not found.');
const [affected] = await Transaction.update(
{ status: 'rejected', rejected_at: new Date() },
{
where: { id: transactionRow.id, status: 'pending' },
transaction: sequelizeTransaction,
},
);
if ((affected as number) === 0) {
return;
}
const buyOrder = await Order.findByPk(transactionRow.buy_order_id, {
transaction: sequelizeTransaction,
lock: sequelizeTransaction?.LOCK?.UPDATE,
});
const sellOrder = await Order.findByPk(transactionRow.sell_order_id, {
transaction: sequelizeTransaction,
lock: sequelizeTransaction?.LOCK?.UPDATE,
});
if (!(buyOrder && sellOrder)) return console.error('Buy or sell order not found.');
const newBuyOrderLeft = Decimal.min(
new Decimal(buyOrder.left).add(transactionRow.amount),
new Decimal(buyOrder.amount),
).toFixed();
const newSellOrderLeft = Decimal.min(
new Decimal(sellOrder.left).add(transactionRow.amount),
new Decimal(sellOrder.amount),
).toFixed();
buyOrder.left = newBuyOrderLeft;
sellOrder.left = newSellOrderLeft;
buyOrder.status = 'active';
sellOrder.status = 'active';
await buyOrder.save({ transaction: sequelizeTransaction });
await sellOrder.save({ transaction: sequelizeTransaction });
}
async createTransaction(
buy_order_id: number,
sell_order_id: number,
amount: string,
creator: string,
hex_raw_proposal: string,
) {
const timestamp = Date.now();
await Transaction.create({
buy_order_id,
sell_order_id,
amount,
timestamp,
status: 'pending',
creator: creator === 'buy' ? 'buy' : 'sell',
hex_raw_proposal,
});
}
// async rejectTransaction(body: ConfirmTransactionBody) {
// try {
// const userData = body.userData;
// const transactionId = body.transactionId;
// const transaction = await Transaction.findByPk(transactionId);
// if (!transaction) {
// return { success: false, data: "Transaction doesn't exist." };
// }
// if (transaction.status !== "pending") {
// return { success: false, data: "Transaction is not pending" };
// }
// const timestamp = Date.now();
// const buyOrder = await ordersModel.getOrderRow(transaction.buy_order_id);
// const sellOrder = await ordersModel.getOrderRow(transaction.sell_order_id);
// if (!(buyOrder && sellOrder)) {
// throw new Error("Buy or sell order not found.");
// }
// if (!
// (
// (buyOrder.user_id !== userData.id ||
// sellOrder.user_id !== userData.id)
// )
// ) {
// return { success: false, data: "You are not a participant of this transaction" };
// }
// await Transaction.update({ status: "rejected" }, { where: { id: transactionId } });
// await Order.update({ left: new Decimal(buyOrder.left).plus(transaction.amount).toFixed(), status: "active" }, { where: { id: buyOrder.id } });
// await Order.update({ left: new Decimal(sellOrder.left).plus(transaction.amount).toFixed(), status: "active" }, { where: { id: sellOrder.id } });
// } catch(err) {
// console.log(err);
// return { success: false, data: "Internal error" };
// }
// }
async confirmTransaction(body: ConfirmTransactionBody) {
try {
const { userData } = body;
const { transactionId } = body;
const userRow = await userModel.getUserRow(userData.address);
if (!userRow) {
throw new Error('User not found.');
}
const transaction = await Transaction.findByPk(transactionId);
if (!transaction) {
return { success: false, data: "Transaction doesn't exist." };
}
if (transaction.status !== 'pending') {
return { success: false, data: 'Transaction is not pending' };
}
const buyOrder = await ordersModel.getOrderRow(transaction.buy_order_id);
const sellOrder = await ordersModel.getOrderRow(transaction.sell_order_id);
if (!(buyOrder && sellOrder)) {
throw new Error('Buy or sell order not found.');
}
if (
!(transaction.creator === 'sell'
? buyOrder.user_id === userRow.id
: sellOrder.user_id === userRow.id)
) {
return { success: false, data: 'You are not a participant of this transaction' };
}
await Transaction.update({ status: 'confirmed' }, { where: { id: transactionId } });
if (buyOrder.status === 'zero') {
await Order.update({ status: 'finished' }, { where: { id: buyOrder.id } });
sendDeleteOrderMessage(io, buyOrder.pair_id.toString(), buyOrder.id.toString());
}
if (sellOrder.status === 'zero') {
await Order.update({ status: 'finished' }, { where: { id: sellOrder.id } });
sendDeleteOrderMessage(io, sellOrder.pair_id.toString(), sellOrder.id.toString());
}
const pairId = buyOrder.pair_id.toString();
const statsResult = await this.calculatePairStats(pairId);
if (!statsResult.success || typeof statsResult.data === 'string') {
throw new Error('Error while getting pair stats');
}
const stats = statsResult.data;
await Pair.update(
{
rate: stats.rate,
coefficient: stats.coefficient,
high: stats.high,
low: stats.low,
volume: stats.volume,
},
{
where: {
id: pairId,
},
},
);
sendUpdatePairStatsMessage(io, pairId, stats);
return { success: true };
} catch (err) {
console.log(err);
return { success: false, data: 'Internal error' };
}
}
async cancelTransaction(body: CancelTransactionBody) {
try {
return await sequelize.transaction(async (t) => {
const { userData } = body;
const { transactionId } = body;
const userRow = await userModel.getUserRow(userData.address);
if (!userRow) {
throw new Error('User not found.');
}
const transaction = await Transaction.findByPk(transactionId);
if (!transaction) {
return { success: false, data: "Transaction doesn't exist." };
}
const transactionOwnerOrder =
transaction.creator === 'buy'
? transaction.buy_order_id
: transaction.sell_order_id;
const ownerOrder = await Order.findByPk(transactionOwnerOrder, {
transaction: t,
lock: t.LOCK.UPDATE,
});
if (!ownerOrder) {
throw new Error('Owner order not found.');
}
if (ownerOrder.user_id !== userRow.id) {
return { success: false, data: 'You are not the creator of this transaction' };
}
if (transaction.status !== 'pending') {
return { success: false, data: 'Transaction is not pending' };
}
await this.returnTransactionAmount(transaction.id, t);
return { success: true };
});
} catch (error) {
console.log(error);
return { success: false, data: 'Internal error' };
}
}
async getActiveTxByOrdersIds(firstOrderId: number, secondOrderId: number) {
const txRow = await Transaction.findOne({
where: {
[Op.or]: [
{
buy_order_id: firstOrderId,
sell_order_id: secondOrderId,
},
{
buy_order_id: secondOrderId,
sell_order_id: firstOrderId,
},
],
status: 'pending',
},
});
return txRow?.toJSON();
}
}
const exchangeModel = new ExchangeModel();
export default exchangeModel;