'use strict'; const coupling = require('./coupling'); const background = require('./background'); const blockchain = background.blockchain; const { PERIOD_INTERVAL, WAIT_TIME, TRADE_HASH_PREFIX, TRANSFER_HASH_PREFIX } = require('./_constants')["market"]; const eCode = require('../docs/scripts/floExchangeAPI').errorCode; const updateBalance = coupling.updateBalance; var DB, assetList; //container for database and allowed assets function login(floID, proxyKey) { return new Promise((resolve, reject) => { DB.query("INSERT INTO UserSession (floID, proxyKey) VALUE (?, ?) " + "ON DUPLICATE KEY UPDATE session_time=DEFAULT, proxyKey=?", [floID, proxyKey, proxyKey]) .then(result => resolve("Login Successful")) .catch(error => reject(error)) }) } function logout(floID) { return new Promise((resolve, reject) => { DB.query("DELETE FROM UserSession WHERE floID=?", [floID]) .then(result => resolve("Logout successful")) .catch(error => reject(error)) }) } function getRateHistory(asset, duration) { return new Promise((resolve, reject) => { if (!asset || !assetList.includes(asset)) reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset(${asset})`)); else coupling.price.getHistory(asset, duration) .then(result => resolve(result)) .catch(error => reject(error)) }) } function getBalance(floID, token) { return new Promise((resolve, reject) => { if (floID && !floCrypto.validateAddr(floID)) reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID(${floID})`)); else if (token && token !== floGlobals.currency && !assetList.includes(token)) reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid token(${token})`)); else if (!floID && !token) reject(INVALID(eCode.MISSING_PARAMETER, 'Missing parameters: requires atleast one (floID, token)')); else { var promise; if (floID && token) promise = getBalance.floID_token(floID, token); else if (floID) promise = getBalance.floID(floID); else if (token) promise = getBalance.token(token); promise.then(result => resolve(result)).catch(error => reject(error)) } }) } getBalance.floID_token = (floID, token) => new Promise((resolve, reject) => { DB.query("SELECT quantity AS balance FROM UserBalance WHERE floID=? AND token=?", [floID, token]).then(result => resolve({ floID, token, balance: result.length ? result[0].balance.toFixed(8) : 0 })).catch(error => reject(error)) }); getBalance.floID = (floID) => new Promise((resolve, reject) => { DB.query("SELECT token, quantity AS balance FROM UserBalance WHERE floID=?", [floID]).then(result => { let response = { floID, balance: {} }; for (let row of result) response.balance[row.token] = row.balance.toFixed(8); resolve(response); }).catch(error => reject(error)) }); getBalance.token = (token) => new Promise((resolve, reject) => { DB.query("SELECT floID, quantity AS balance FROM UserBalance WHERE token=?", [token]).then(result => { let response = { token: token, balance: {} }; for (let row of result) response.balance[row.floID] = row.balance.toFixed(8); resolve(response); }).catch(error => reject(error)) }); const getAssetBalance = (floID, asset) => new Promise((resolve, reject) => { let promises = []; promises.push(DB.query("SELECT IFNULL(SUM(quantity), 0) AS balance FROM UserBalance WHERE floID=? AND token=?", [floID, asset])); promises.push(asset === floGlobals.currency ? DB.query("SELECT IFNULL(SUM(quantity*maxPrice), 0) AS locked FROM BuyOrder WHERE floID=?", [floID]) : DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=?", [floID, asset]) ); Promise.all(promises).then(result => resolve({ total: result[0][0].balance, locked: result[1][0].locked, net: result[0][0].balance - result[1][0].locked })).catch(error => reject(error)) }); getAssetBalance.check = (floID, asset, amount) => new Promise((resolve, reject) => { getAssetBalance(floID, asset).then(balance => { if (balance.total < amount) reject(INVALID(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset}`)); else if (balance.net < amount) reject(INVALID(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset} (Some are locked in orders)`)); else resolve(true); }).catch(error => reject(error)) }); function addSellOrder(floID, asset, quantity, min_price) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(floID)) return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`)); else if (typeof quantity !== "number" || quantity <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`)); else if (typeof min_price !== "number" || min_price <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid min_price (${min_price})`)); else if (!assetList.includes(asset)) return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset (${asset})`)); getAssetBalance.check(floID, asset, quantity).then(_ => { checkSellRequirement(floID, asset, quantity, min_price).then(_ => { DB.query("INSERT INTO SellOrder(floID, asset, quantity, minPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, min_price]).then(result => { resolve('Sell Order placed successfully'); coupling.initiate(asset); }).catch(error => reject(error)); }).catch(error => reject(error)) }).catch(error => reject(error)); }); } const checkSellRequirement = (floID, asset, quantity, min_price) => new Promise((resolve, reject) => { Promise.all([ DB.query("SELECT IFNULL(SUM(quantity), 0) AS total_chips FROM SellChips WHERE floID=? AND asset=?", [floID, asset]), DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=?", [floID, asset]) ]).then(result => { let total = result[0][0].total_chips, locked = result[1][0].locked; if (total < locked + quantity) reject(INVALID(eCode.INSUFFICIENT_SELLCHIP, `Insufficient sell-chips for ${asset}`)); else Promise.all([ DB.query("SELECT IFNULL(SUM(quantity), 0) AS total_chips FROM SellChips WHERE floID=? AND asset=? AND base<=?", [floID, asset, min_price]), DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=? AND minPrice<=?", [floID, asset, min_price]) ]).then(result => { let g_total = result[0][0].total_chips, g_locked = result[1][0].locked; let l_total = total - g_total, l_locked = locked - g_locked; var rem = g_total - g_locked; if (l_locked > l_total) rem -= l_locked - l_total; if (rem < quantity) reject(INVALID(eCode.GREATER_SELLCHIP_BASE, `Cannot sell below purchased price`)); else resolve(true); }).catch(error => reject(error)) }).catch(error => reject(error)) }); function addBuyOrder(floID, asset, quantity, max_price) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(floID)) return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`)); else if (typeof quantity !== "number" || quantity <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`)); else if (typeof max_price !== "number" || max_price <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid max_price (${max_price})`)); else if (!assetList.includes(asset)) return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset (${asset})`)); getAssetBalance.check(floID, floGlobals.currency, quantity * max_price).then(_ => { DB.query("INSERT INTO BuyOrder(floID, asset, quantity, maxPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, max_price]).then(result => { resolve('Buy Order placed successfully'); coupling.initiate(asset); }).catch(error => reject(error)); }).catch(error => reject(error)); }); } function cancelOrder(type, id, floID) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(floID)) return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`)); let tableName; if (type === "buy") tableName = "BuyOrder"; else if (type === "sell") tableName = "SellOrder"; else return reject(INVALID(eCode.INVALID_TYPE, "Invalid Order type! Order type must be buy (or) sell")); DB.query(`SELECT floID, asset FROM ${tableName} WHERE id=?`, [id]).then(result => { if (result.length < 1) return reject(INVALID(eCode.NOT_FOUND, "Order not found!")); else if (result[0].floID !== floID) return reject(INVALID(eCode.NOT_OWNER, "Order doesnt belong to the current user")); let asset = result[0].asset; //Delete the order DB.query(`DELETE FROM ${tableName} WHERE id=?`, [id]).then(result => { resolve(tableName + "#" + id + " cancelled successfully"); coupling.initiate(asset); }).catch(error => reject(error)); }).catch(error => reject(error)); }); } function getAccountDetails(floID) { return new Promise((resolve, reject) => { let select = []; select.push(["token, quantity", "UserBalance"]); select.push(["id, asset, quantity, minPrice, time_placed", "SellOrder"]); select.push(["id, asset, quantity, maxPrice, time_placed", "BuyOrder"]); let promises = select.map(a => DB.query(`SELECT ${a[0]} FROM ${a[1]} WHERE floID=? ${a[2] || ""}`, [floID])); Promise.allSettled(promises).then(results => { let response = { floID: floID, time: Date.now() }; results.forEach((a, i) => { if (a.status === "rejected") console.error(a.reason); else switch (i) { case 0: response.tokenBalance = a.value; break; case 1: response.sellOrders = a.value; break; case 2: response.buyOrders = a.value; break; } }); DB.query("SELECT * FROM TradeTransactions WHERE seller=? OR buyer=?", [floID, floID]) .then(result => response.transactions = result) .catch(error => console.error(error)) .finally(_ => resolve(response)); }); }); } function getUserTransacts(floID) { return new Promise((resolve, reject) => { DB.query("(SELECT 'deposit' as type, txid, token, amount, status FROM InputToken WHERE floID=?)" + "UNION (SELECT 'deposit' as type, txid, coin as token, amount, status FROM InputCoin WHERE floID=?)" + "UNION (SELECT 'withdraw' as type, txid, token, amount, status FROM OutputToken WHERE floID=?)" + "UNION (SELECT 'withdraw' as type, txid, coin as token, amount, status FROM OutputCoin WHERE floID=?)", [floID, floID, floID, floID]) .then(result => resolve(result)) .catch(error => reject(error)) }) } function getTransactionDetails(txid) { return new Promise((resolve, reject) => { let tableName, type; if (txid.startsWith(TRANSFER_HASH_PREFIX)) { tableName = 'TransferTransactions'; type = 'transfer'; } else if (txid.startsWith(TRADE_HASH_PREFIX)) { tableName = 'TradeTransactions'; type = 'trade'; } else return reject(INVALID(eCode.INVALID_TX_ID, "Invalid TransactionID")); DB.query(`SELECT * FROM ${tableName} WHERE txid=?`, [txid]).then(result => { if (result.length) { let details = result[0]; details.type = type; if (tableName === 'TransferTransactions') //As json object is stored for receiver in transfer (to support one-to-many) details.receiver = JSON.parse(details.receiver); resolve(details); } else reject(INVALID(eCode.NOT_FOUND, "Transaction not found")); }).catch(error => reject(error)) }) } function transferToken(sender, receivers, token) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(sender)) reject(INVALID(eCode.INVALID_FLO_ID, `Invalid sender (${sender})`)); else if (token !== floGlobals.currency && !assetList.includes(token)) reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid token (${token})`)); else { let invalidIDs = [], totalAmount = 0; for (let floID in receivers) if (!floCrypto.validateAddr(floID)) invalidIDs.push(floID); else totalAmount += receivers[floID]; if (invalidIDs.length) reject(INVALID(eCode.INVALID_FLO_ID, `Invalid receiver (${invalidIDs})`)); else getAssetBalance.check(sender, token, totalAmount).then(_ => { let txQueries = []; txQueries.push(updateBalance.consume(sender, token, totalAmount)); for (let floID in receivers) txQueries.push(updateBalance.add(floID, token, receivers[floID])); checkDistributor(sender, token).then(result => { if (result) for (let floID in receivers) txQueries.push(["INSERT INTO SellChips (floID, asset, quantity) VALUES (?, ?, ?)", [floID, token, receivers[floID]]]); let time = Date.now(); let hash = TRANSFER_HASH_PREFIX + Crypto.SHA256(JSON.stringify({ sender: sender, receiver: receivers, token: token, totalAmount: totalAmount, tx_time: time, })); txQueries.push([ "INSERT INTO TransferTransactions (sender, receiver, token, totalAmount, tx_time, txid) VALUE (?, ?, ?, ?, ?, ?)", [sender, JSON.stringify(receivers), token, totalAmount, global.convertDateToString(time), hash] ]); DB.transaction(txQueries) .then(result => resolve(hash)) .catch(error => reject(error)) }).catch(error => reject(error)) }).catch(error => reject(error)) } }) } function depositFLO(floID, txid) { return new Promise((resolve, reject) => { DB.query("SELECT status FROM InputCoin WHERE txid=? AND floID=? AND coin=?", [txid, floID, "FLO"]).then(result => { if (result.length) { switch (result[0].status) { case "PENDING": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process")); case "REJECTED": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected")); case "SUCCESS": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add coins")); } } else DB.query("INSERT INTO InputCoin(txid, floID, coin, status) VALUES (?, ?, ?, ?)", [txid, floID, "FLO", "PENDING"]) .then(result => resolve("Deposit request in process")) .catch(error => reject(error)); }).catch(error => reject(error)) }); } function withdrawFLO(floID, amount) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(floID)) return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`)); else if (typeof amount !== "number" || amount <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`)); getAssetBalance.check(floID, "FLO", amount).then(_ => { let txQueries = []; txQueries.push(updateBalance.consume(floID, "FLO", amount)); DB.transaction(txQueries).then(result => { blockchain.sendCoin.init(floID, "FLO", amount); resolve("Withdrawal request is in process"); }).catch(error => reject(error)); }).catch(error => reject(error)); }); } function depositToken(floID, txid) { return new Promise((resolve, reject) => { DB.query("SELECT status FROM InputToken WHERE txid=? AND floID=?", [txid, floID]).then(result => { if (result.length) { switch (result[0].status) { case "PENDING": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process")); case "REJECTED": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected")); case "SUCCESS": return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add tokens")); } } else DB.query("INSERT INTO InputToken(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) .then(result => resolve("Deposit request in process")) .catch(error => reject(error)); }).catch(error => reject(error)) }); } function withdrawToken(floID, token, amount) { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(floID)) return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`)); else if (typeof amount !== "number" || amount <= 0) return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`)); else if ((!assetList.includes(token) && token !== floGlobals.currency) || token === "FLO") return reject(INVALID(eCode.INVALID_TOKEN_NAME, "Invalid Token")); //Check for FLO balance (transaction fee) let required_flo = floGlobals.sendAmt + floGlobals.fee; getAssetBalance.check(floID, "FLO", required_flo).then(_ => { getAssetBalance.check(floID, token, amount).then(_ => { let txQueries = []; txQueries.push(updateBalance.consume(floID, "FLO", required_flo)); txQueries.push(updateBalance.consume(floID, token, amount)); DB.transaction(txQueries).then(result => { //Send Token to user via token API blockchain.sendToken.init(floID, token, amount); resolve("Withdrawal request is in process"); }).catch(error => reject(error)); }).catch(error => reject(error)); }).catch(error => reject(error)); }); } function addTag(floID, tag) { return new Promise((resolve, reject) => { DB.query("INSERT INTO UserTag (floID, tag) VALUE (?,?)", [floID, tag]) .then(result => resolve(`Added ${floID} to ${tag}`)) .catch(error => { if (error.code === "ER_DUP_ENTRY") reject(INVALID(eCode.DUPLICATE_ENTRY, `${floID} already in ${tag}`)); else if (error.code === "ER_NO_REFERENCED_ROW") reject(INVALID(eCode.INVALID_TAG, `Invalid Tag`)); else reject(error); }); }); } function removeTag(floID, tag) { return new Promise((resolve, reject) => { DB.query("DELETE FROM UserTag WHERE floID=? AND tag=?", [floID, tag]) .then(result => resolve(`Removed ${floID} from ${tag}`)) .catch(error => reject(error)); }) } function addDistributor(floID, asset) { return new Promise((resolve, reject) => { DB.query("INSERT INTO Distributors (floID, asset) VALUE (?,?)", [floID, asset]) .then(result => resolve(`Added ${asset} distributor: ${floID}`)) .catch(error => { if (error.code === "ER_DUP_ENTRY") reject(INVALID(eCode.DUPLICATE_ENTRY, `${floID} is already ${asset} disributor`)); else if (error.code === "ER_NO_REFERENCED_ROW") reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid Asset`)); else reject(error); }); }); } function removeDistributor(floID, asset) { return new Promise((resolve, reject) => { DB.query("DELETE FROM Distributors WHERE floID=? AND tag=?", [floID, asset]) .then(result => resolve(`Removed ${asset} distributor: ${floID}`)) .catch(error => reject(error)); }) } function checkDistributor(floID, asset) { return new Promise((resolve, reject) => { DB.query("SELECT id FROM Distributors WHERE floID=? AND asset=?", [floID, asset]) .then(result => resolve(result.length ? true : false)) .catch(error => reject(error)) }) } function periodicProcess() { blockchainReCheck(); } periodicProcess.start = function() { periodicProcess.stop(); periodicProcess(); assetList.forEach(asset => coupling.initiate(asset, true)); coupling.price.storeHistory.start(); periodicProcess.instance = setInterval(periodicProcess, PERIOD_INTERVAL); }; periodicProcess.stop = function() { if (periodicProcess.instance !== undefined) { clearInterval(periodicProcess.instance); delete periodicProcess.instance; } coupling.stopAll(); coupling.price.storeHistory.stop(); }; var lastSyncBlockHeight = 0; function blockchainReCheck() { if (blockchainReCheck.timeout !== undefined) { clearTimeout(blockchainReCheck.timeout); delete blockchainReCheck.timeout; } if (!blockchain.chests.list.length) return blockchainReCheck.timeout = setTimeout(blockchainReCheck, WAIT_TIME); floBlockchainAPI.promisedAPI('api/blocks?limit=1').then(result => { if (lastSyncBlockHeight < result.blocks[0].height) { lastSyncBlockHeight = result.blocks[0].height; background.confirmDepositFLO(); background.confirmDepositToken(); background.retryWithdrawalCoin(); background.retryWithdrawalToken(); background.confirmWithdrawalFLO(); background.confirmWithdrawalBTC(); background.confirmWithdrawalToken(); console.debug("Last Block :", lastSyncBlockHeight); } }).catch(error => console.error(error)); } module.exports = { login, logout, get rates() { return coupling.price.currentRates; }, get priceCountDown() { return coupling.price.lastTimes; }, get chests() { return blockchain.chests; }, set chests(c) { blockchain.chests = c; }, addBuyOrder, addSellOrder, cancelOrder, getRateHistory, getBalance, getAccountDetails, getUserTransacts, getTransactionDetails, transferToken, depositFLO, withdrawFLO, depositToken, withdrawToken, addTag, removeTag, addDistributor, removeDistributor, periodicProcess, set DB(db) { DB = db; coupling.DB = db; }, set assetList(assets) { assetList = assets; }, get assetList() { return assetList }, set collectAndCall(fn) { blockchain.collectAndCall = fn; }, };