diff --git a/server/api/controllers/admin.js b/server/api/controllers/admin.js index ce424f3c4e..5c6a311b32 100644 --- a/server/api/controllers/admin.js +++ b/server/api/controllers/admin.js @@ -2458,6 +2458,43 @@ const updateQuickTradeConfig = (req, res) => { }); }; +const getBalancesAdmin = (req, res) => { + loggerAdmin.verbose(req.uuid, 'controllers/admin/getBalancesAdmin/auth', req.auth); + + const { + user_id, + currency, + format + } = req.swagger.params; + + + if (format.value && req.auth.scopes.indexOf(ROLES.ADMIN) === -1) { + return res.status(403).json({ message: API_KEY_NOT_PERMITTED }); + } + + toolsLib.user.getAllBalancesAdmin({ + user_id: user_id.value, + currency: currency.value, + format: format.value, + additionalHeaders: { + 'x-forwarded-for': req.headers['x-forwarded-for'] + } + }) + .then((data) => { + if (format.value === 'all') { + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-users.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerAdmin.error(req.uuid, 'controllers/admin/getBalancesAdmin', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +} + module.exports = { createInitialAdmin, getAdminKit, @@ -2519,5 +2556,6 @@ module.exports = { revokeUserSessionByAdmin, sendEmailByAdmin, sendRawEmailByAdmin, - updateQuickTradeConfig + updateQuickTradeConfig, + getBalancesAdmin }; diff --git a/server/api/controllers/broker.js b/server/api/controllers/broker.js index a76247c4f5..0ab004079a 100644 --- a/server/api/controllers/broker.js +++ b/server/api/controllers/broker.js @@ -47,7 +47,6 @@ const createBrokerPair = (req, res) => { user_id, min_size, max_size, - increment_size, type, quote_expiry_time, rebalancing_symbol, @@ -67,7 +66,6 @@ const createBrokerPair = (req, res) => { user_id, min_size, max_size, - increment_size, type, quote_expiry_time, rebalancing_symbol, @@ -84,7 +82,6 @@ const createBrokerPair = (req, res) => { user_id, min_size, max_size, - increment_size, type, quote_expiry_time, rebalancing_symbol, @@ -115,13 +112,11 @@ const testBroker = (req, res) => { const { formula, spread, - increment_size } = req.swagger.params.data.value; toolsLib.broker.testBroker({ formula, spread, - increment_size }) .then((data) => { return res.json(data); @@ -181,7 +176,6 @@ function updateBrokerPair(req, res) { sell_price, min_size, max_size, - increment_size, paused, user_id, type, @@ -200,7 +194,6 @@ function updateBrokerPair(req, res) { sell_price, min_size, max_size, - increment_size, paused, user_id, type, @@ -264,7 +257,6 @@ function getBrokerPairs(req, res) { 'paused', 'min_size', 'max_size', - 'increment_size', 'type', 'quote_expiry_time', 'rebalancing_symbol', diff --git a/server/api/swagger/admin.yaml b/server/api/swagger/admin.yaml index 6d000d1f45..6aa1609d81 100644 --- a/server/api/swagger/admin.yaml +++ b/server/api/swagger/admin.yaml @@ -3187,4 +3187,48 @@ paths: - bearer - hmac x-security-scopes: - - admin \ No newline at end of file + - admin + /admin/balances: + x-swagger-router-controller: admin + get: + description: Get exchange balances of users for admin + operationId: getBalancesAdmin + tags: + - Admin + parameters: + - name: user_id + in: query + required: false + type: number + - name: currency + in: query + required: false + type: string + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - admin + x-token-permissions: + - can_read \ No newline at end of file diff --git a/server/db/migrations/20230628100835-remove-broker-increment-size.js b/server/db/migrations/20230628100835-remove-broker-increment-size.js new file mode 100644 index 0000000000..7d5ccb9c3f --- /dev/null +++ b/server/db/migrations/20230628100835-remove-broker-increment-size.js @@ -0,0 +1,14 @@ +'use strict'; + +const TABLE = 'Brokers'; +const COLUMN = 'increment_size'; + +module.exports = { + up: (queryInterface, Sequelize) => + queryInterface.removeColumn(TABLE, COLUMN), + down: (queryInterface, Sequelize) => + queryInterface.addColumn(TABLE, COLUMN, { + type: Sequelize.DOUBLE, + allowNull: false + }) +}; \ No newline at end of file diff --git a/server/db/models/broker.js b/server/db/models/broker.js index 02cb451748..56ef3f6de7 100644 --- a/server/db/models/broker.js +++ b/server/db/models/broker.js @@ -47,10 +47,6 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.DOUBLE, allowNull: false }, - increment_size: { - type: DataTypes.DOUBLE, - allowNull: false - }, type: { type: DataTypes.ENUM('manual', 'dynamic'), defaultValue: 'manual', diff --git a/server/init.js b/server/init.js index dd9b6ebb71..866021e982 100644 --- a/server/init.js +++ b/server/init.js @@ -125,7 +125,7 @@ const checkStatus = () => { status.constants ), Tier.findAll(), - Broker.findAll({ attributes: ['id', 'symbol', 'buy_price', 'sell_price', 'paused', 'min_size', 'max_size', 'increment_size']}), + Broker.findAll({ attributes: ['id', 'symbol', 'buy_price', 'sell_price', 'paused', 'min_size', 'max_size']}), QuickTrade.findAll(), status.dataValues ]); diff --git a/server/utils/hollaex-tools-lib/tools/broker.js b/server/utils/hollaex-tools-lib/tools/broker.js index 09b608349e..8b43381f5a 100644 --- a/server/utils/hollaex-tools-lib/tools/broker.js +++ b/server/utils/hollaex-tools-lib/tools/broker.js @@ -9,7 +9,7 @@ const { EXCHANGE_PLAN_INTERVAL_TIME, EXCHANGE_PLAN_PRICE_SOURCE } = require(`${S const { getNodeLib } = require(`${SERVER_PATH}/init`); const { client } = require('./database/redis'); const { getUserByKitId } = require('./user'); -const { validatePair, getKitTier, getKitConfig, getAssetsPrices, getQuickTrades } = require('./common'); +const { validatePair, getKitTier, getKitConfig, getAssetsPrices, getQuickTrades, getKitCoin } = require('./common'); const { sendEmail } = require('../../../mail'); const { MAILTYPE } = require('../../../mail/strings'); const { verifyBearerTokenPromise } = require('./security'); @@ -53,8 +53,6 @@ const validateBrokerPair = (brokerPair) => { throw new Error('Broker minimum order size must be bigger than zero.'); } else if (new BigNumber(brokerPair.max_size).comparedTo(new BigNumber(brokerPair.min_size)) !== 1) { throw new Error('Broker maximum order size must be bigger than minimum order size.'); - } else if (new BigNumber(brokerPair.increment_size).comparedTo(0) !== 1) { - throw new Error('Broker order price increment must be bigger than zero.'); } else if (brokerPair.symbol && !validatePair(brokerPair.symbol)) { throw new Error('invalid symbol'); } @@ -86,19 +84,19 @@ const getQuoteDynamicBroker = async (side, broker, user_id = null, orderData) => const baseCurrencyPrice = await calculatePrice(side, spread, formula, refresh_interval, id); - const decimalPoint = new BigNumber(broker.increment_size).dp(); - const roundedPrice = new BigNumber(baseCurrencyPrice).decimalPlaces(decimalPoint).toNumber(); const responseObject = { - price: roundedPrice + price: baseCurrencyPrice }; + const { size, receiving_amount, spending_amount } = calculateSize(orderData, side, responseObject, symbol); + responseObject.receiving_amount = receiving_amount; + responseObject.spending_amount = spending_amount; + //check if there is user_id, if so, assing token if (user_id) { - const size = calculateSize(orderData, side, responseObject, decimalPoint, symbol); - // Generate randomToken to be used during deal execution - const randomToken = generateRandomToken(user_id, symbol, side, quote_expiry_time, roundedPrice, size, 'broker'); + const randomToken = generateRandomToken(user_id, symbol, side, quote_expiry_time, baseCurrencyPrice, size, 'broker'); responseObject.token = randomToken; // set expiry const expiryDate = new Date(); @@ -111,21 +109,21 @@ const getQuoteDynamicBroker = async (side, broker, user_id = null, orderData) => }; const getQuoteManualBroker = async (broker, side, user_id = null, orderData) => { - const { symbol, quote_expiry_time, sell_price, buy_price, increment_size } = broker; + const { symbol, quote_expiry_time, sell_price, buy_price } = broker; const baseCurrencyPrice = side === 'buy' ? sell_price : buy_price; - const decimalPoint = new BigNumber(increment_size).dp(); - const roundedPrice = new BigNumber(baseCurrencyPrice).decimalPlaces(decimalPoint).toNumber(); - const responseObject = { - price: roundedPrice + price: baseCurrencyPrice }; - const size = calculateSize(orderData, side, responseObject, decimalPoint, symbol); + const { size, receiving_amount, spending_amount } = calculateSize(orderData, side, responseObject, symbol); + responseObject.receiving_amount = receiving_amount; + responseObject.spending_amount = spending_amount; if (user_id) { - const randomToken = generateRandomToken(user_id, symbol, side, quote_expiry_time, roundedPrice, size, 'broker'); + + const randomToken = generateRandomToken(user_id, symbol, side, quote_expiry_time, baseCurrencyPrice, size, 'broker'); responseObject.token = randomToken; // set expiry const expiryDate = new Date(); @@ -135,7 +133,7 @@ const getQuoteManualBroker = async (broker, side, user_id = null, orderData) => return responseObject; } -const calculateSize = (orderData, side, responseObject, decimalPoint, symbol) => { +const calculateSize = (orderData, side, responseObject, symbol) => { if (orderData == null) { throw new Error(COIN_INPUT_MISSING); } @@ -143,19 +141,38 @@ const calculateSize = (orderData, side, responseObject, decimalPoint, symbol) => let size = null; let { spending_currency, receiving_currency, spending_amount, receiving_amount } = orderData; + const coins = symbol.split('-'); + const baseCoinInfo = getKitCoin(coins[0]); + const quoteCointInfo = getKitCoin(coins[1]); + if (spending_currency == null && receiving_currency == null) { throw new Error(AMOUNTS_MISSING); } if (spending_amount != null) { - const sourceAmount = new BigNumber(side === 'buy' ? spending_amount / responseObject.price : spending_amount * responseObject.price) - .decimalPlaces(decimalPoint).toNumber(); - receiving_amount = sourceAmount; + const incrementUnit = side === 'buy' ? baseCoinInfo.increment_unit : quoteCointInfo.increment_unit; + const targetedAmount = side === 'buy' ? spending_amount / responseObject.price : spending_amount * responseObject.price; + + if (incrementUnit < 1) { + const decimalPoint = new BigNumber(incrementUnit).dp(); + const sourceAmount = new BigNumber(targetedAmount).decimalPlaces(decimalPoint).toNumber(); + receiving_amount = sourceAmount; + } else { + receiving_amount = targetedAmount - (targetedAmount % incrementUnit); + } + } else if (receiving_amount != null) { - const sourceAmount = new BigNumber(side === 'buy' ? receiving_amount * responseObject.price : receiving_amount / responseObject.price) - .decimalPlaces(decimalPoint).toNumber(); - spending_amount = sourceAmount; + const incrementUnit = side === 'buy' ? quoteCointInfo.increment_unit : baseCoinInfo.increment_unit + const targetedAmount = side === 'buy' ? receiving_amount * responseObject.price : receiving_amount / responseObject.price; + + if (incrementUnit < 1) { + const decimalPoint = new BigNumber(incrementUnit).dp(); + const sourceAmount = new BigNumber(targetedAmount).decimalPlaces(decimalPoint).toNumber(); + spending_amount = sourceAmount; + } else { + spending_amount = targetedAmount - (targetedAmount % incrementUnit); + } } if (`${spending_currency}-${receiving_currency}` === symbol) { @@ -163,7 +180,7 @@ const calculateSize = (orderData, side, responseObject, decimalPoint, symbol) => } else { size = receiving_amount } - return size; + return { size, spending_amount, receiving_amount }; } @@ -301,7 +318,7 @@ const fetchBrokerQuote = async (brokerQuote) => { }; const testBroker = async (data) => { - const { formula, spread, increment_size } = data; + const { formula, spread } = data; try { if (spread == null) { throw new Error(BROKER_FORMULA_NOT_FOUND); @@ -323,7 +340,7 @@ const testBroker = async (data) => { throw new Error(PRICE_NOT_FOUND); } - const decimalPoint = new BigNumber(increment_size).dp(); + const decimalPoint = new BigNumber(price).dp(); return { buy_price: new BigNumber(price * (1 - (spread / 100))).decimalPlaces(decimalPoint).toNumber(), sell_price: new BigNumber(price * (1 + (spread / 100))).decimalPlaces(decimalPoint).toNumber() @@ -408,7 +425,6 @@ const reverseTransaction = async (orderData) => { const quickTradeConfig = quickTrades.find(quickTrade => quickTrade.symbol === symbol); if (quickTradeConfig && quickTradeConfig.type === 'broker' && quickTradeConfig.active && broker && !broker.paused && broker.account) { - const decimalPoint = new BigNumber(broker.increment_size).dp(); const objectKeys = Object.keys(broker.account); const exchangeKey = objectKeys[0]; @@ -420,12 +436,7 @@ const reverseTransaction = async (orderData) => { }) const formattedRebalancingSymbol = broker.rebalancing_symbol && broker.rebalancing_symbol.split('-').join('/').toUpperCase(); - - const marketTicker = await exchange.fetchTicker(symbol); - - const roundedPrice = new BigNumber(side === 'buy' ? marketTicker.last * 1.01 : marketTicker.last * 0.99) - .decimalPlaces(decimalPoint).toNumber(); - exchange.createOrder(formattedRebalancingSymbol, 'limit', side, size, roundedPrice) + exchange.createOrder(formattedRebalancingSymbol, 'market', side, size) .catch((err) => { notifyUser(err.message, broker.user_id); }); } } @@ -459,7 +470,6 @@ const createBrokerPair = async (brokerPair) => { type, account, formula, - increment_size, rebalancing_symbol } = brokerPair; @@ -492,7 +502,7 @@ const createBrokerPair = async (brokerPair) => { } if (formula) { - const brokerPrice = await testBroker({ formula, spread, increment_size }); + const brokerPrice = await testBroker({ formula, spread }); if (!Number(brokerPrice.sell_price) || !Number(brokerPrice.buy_price)) { throw new Error(FORMULA_MARKET_PAIR_ERROR); } @@ -534,7 +544,6 @@ const updateBrokerPair = async (id, data) => { type, account, formula, - increment_size, rebalancing_symbol } = data; @@ -566,7 +575,7 @@ const updateBrokerPair = async (id, data) => { } if (formula) { - const brokerPrice = await testBroker({ formula, spread, increment_size }); + const brokerPrice = await testBroker({ formula, spread }); if (!Number(brokerPrice.sell_price) || !Number(brokerPrice.buy_price)) { throw new Error(FORMULA_MARKET_PAIR_ERROR); } @@ -586,7 +595,6 @@ const updateBrokerPair = async (id, data) => { 'sell_price', 'min_size', 'max_size', - 'increment_size', 'paused', 'type', 'quote_expiry_time', diff --git a/server/utils/hollaex-tools-lib/tools/order.js b/server/utils/hollaex-tools-lib/tools/order.js index 81c5576e1e..83b52663fa 100644 --- a/server/utils/hollaex-tools-lib/tools/order.js +++ b/server/utils/hollaex-tools-lib/tools/order.js @@ -162,7 +162,6 @@ const getUserQuickTrade = async (spending_currency, spending_amount, receiving_a } }) .then((brokerQuote) => { - const decimalPoint = new BigNumber(broker.increment_size).dp(); const responseObj = { spending_currency, receiving_currency, @@ -172,15 +171,9 @@ const getUserQuickTrade = async (spending_currency, spending_amount, receiving_a type: 'broker' } if (spending_amount != null) { - const sourceAmount = new BigNumber(side === 'buy' ? spending_amount / brokerQuote.price : spending_amount * brokerQuote.price) - .decimalPlaces(decimalPoint).toNumber(); - - responseObj.receiving_amount = sourceAmount; - + responseObj.receiving_amount = brokerQuote.receiving_amount; } else if (receiving_amount != null) { - const sourceAmount = new BigNumber(side === 'buy' ? receiving_amount * brokerQuote.price : receiving_amount / brokerQuote.price) - .decimalPlaces(decimalPoint).toNumber(); - responseObj.spending_amount = sourceAmount; + responseObj.spending_amount = brokerQuote.spending_amount;; } const baseCoinSize = side === 'buy' ? responseObj.receiving_amount : responseObj.spending_amount; diff --git a/server/utils/hollaex-tools-lib/tools/user.js b/server/utils/hollaex-tools-lib/tools/user.js index 16da9b8ca3..815dc96805 100644 --- a/server/utils/hollaex-tools-lib/tools/user.js +++ b/server/utils/hollaex-tools-lib/tools/user.js @@ -1987,6 +1987,57 @@ const revokeExchangeUserSession = async (sessionId, userId = null) => { return updatedSession.dataValues; } +const getAllBalancesAdmin = async (opts = { + user_id: null, + currency: null, + format: null, + additionalHeaders: null +}) => { + + let network_id = null; + if (opts.user_id) { + // check mapKitIdToNetworkId + const idDictionary = await mapKitIdToNetworkId([opts.user_id]); + if (!has(idDictionary, opts.user_id)) { + throw new Error(USER_NOT_FOUND); + } else if (!idDictionary[opts.user_id]) { + throw new Error(USER_NOT_REGISTERED_ON_NETWORK); + } else { + network_id = idDictionary[opts.user_id]; + } + } + + return getNodeLib().getBalances({ + userId: network_id, + currency: opts.currency, + format: opts.format, + additionalHeaders: opts.additionalHeaders + }) + .then(async (balances) => { + if (balances.data.length > 0) { + const networkIds = balances.data.map((balance) => balance.user_id).filter(id => id); + const idDictionary = await mapNetworkIdToKitId(networkIds); + for (let balance of balances.data) { + const user_kit_id = idDictionary[balance.user_id]; + balance.network_id = balance.user_id; + balance.user_id = user_kit_id; + if (balance.User) balance.User.id = user_kit_id; + } + } + + if (opts.format && opts.format === 'all') { + if (balances.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(balances.data, Object.keys(balances.data[0])); + return csv; + } else { + return balances; + } + }); +} + + module.exports = { loginUser, getUserTier, @@ -2043,5 +2094,6 @@ module.exports = { revokeExchangeUserSession, updateLoginStatus, findUserLatestLogin, - createUserLogin + createUserLogin, + getAllBalancesAdmin }; diff --git a/web/src/containers/Admin/AdminFinancials/Balances.js b/web/src/containers/Admin/AdminFinancials/Balances.js new file mode 100644 index 0000000000..100856d068 --- /dev/null +++ b/web/src/containers/Admin/AdminFinancials/Balances.js @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import MultiFilter from './TableFilter'; +import { getExchangeBalances } from './action'; + +// const columns = [ +// { +// title: 'User Id', +// dataIndex: 'user_id', +// key: 'user_id', +// render: (user_id, data) => { +// return ( +//