diff --git a/docs/scripts/floExchangeAPI.js b/docs/scripts/floExchangeAPI.js index 06028e8..045ed0f 100644 --- a/docs/scripts/floExchangeAPI.js +++ b/docs/scripts/floExchangeAPI.js @@ -1527,6 +1527,66 @@ }) } + exchangeAPI.checkBlockchainBond = function (prior_time, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + prior_time, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "check_blockchain_bond", + prior_time, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/check-blockchain-bond', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => { + responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + + exchangeAPI.checkBobsFund = function (prior_time, floID, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + floID: floID, + prior_time, + timestamp: Date.now() + }; + if (floCrypto.getFloID(proxySecret) === floID) //Direct signing (without proxy) + request.pubKey = floCrypto.getPubKeyHex(proxySecret); + request.sign = signRequest({ + type: "check_bobs_fund", + prior_time, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch_api('/check-bobs-fund', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => { + responseParse(result) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + } + exchangeAPI.closeBobsFundInvestment = function (fund_id, floID, privKey) { return new Promise((resolve, reject) => { if (!floCrypto.verifyPrivKey(privKey, floID)) @@ -1660,9 +1720,10 @@ exchangeAPI.init = function refreshDataFromBlockchain() { return new Promise((resolve, reject) => { - let nodes, assets, tags, lastTx; + let nodes, trusted = new Set(), assets, tags, lastTx; try { nodes = JSON.parse(localStorage.getItem('exchange-nodes')); + trusted = new Set((localStorage.getItem('exchange-trusted') || "").split(',')); assets = new Set((localStorage.getItem('exchange-assets') || "").split(',')); tags = new Set((localStorage.getItem('exchange-tags') || "").split(',')); if (typeof nodes !== 'object' || nodes === null) @@ -1671,6 +1732,7 @@ lastTx = parseInt(localStorage.getItem('exchange-lastTx')) || 0; } catch (error) { nodes = {}; + trusted = new Set(); assets = new Set(); tags = new Set(); lastTx = 0; @@ -1691,6 +1753,15 @@ for (let n in content.Nodes.add) nodes[n] = content.Nodes.add[n]; } + //Trusted List + if (content.Trusted) { + if (content.Trusted.remove) + for (let id of content.Trusted.remove) + trusted.delete(id); + if (content.Trusted.add) + for (let id of content.Trusted.add) + trusted.add(id); + } //Asset List if (content.Assets) { for (let a in content.Assets) @@ -1708,6 +1779,7 @@ }); localStorage.setItem('exchange-lastTx', result.totalTxs); localStorage.setItem('exchange-nodes', JSON.stringify(nodes)); + localStorage.setItem('exchange-trusted', Array.from(trusted).join(",")); localStorage.setItem('exchange-assets', Array.from(assets).join(",")); localStorage.setItem('exchange-tags', Array.from(tags).join(",")); nodeURL = nodes; @@ -1718,8 +1790,21 @@ }) } + const config = exchangeAPI.config = { + get trustedList() { + return new Set((localStorage.getItem('exchange-trusted') || "").split(',')); + }, + get assetList() { + return new Set((localStorage.getItem('exchange-assets') || "").split(',')); + }, + get tagList() { + return new Set((localStorage.getItem('exchange-tags') || "").split(',')); + } + } + exchangeAPI.clearAllLocalData = function () { localStorage.removeItem('exchange-nodes'); + localStorage.removeItem('exchange-trusted'); localStorage.removeItem('exchange-assets'); localStorage.removeItem('exchange-tags'); localStorage.removeItem('exchange-lastTx'); diff --git a/src/app.js b/src/app.js index c844164..9f50e92 100644 --- a/src/app.js +++ b/src/app.js @@ -102,11 +102,15 @@ module.exports = function App(secret) { app.post('/withdraw-convert-coin-fund', Request.WithdrawConvertCoinFund); app.post('/withdraw-convert-currency-fund', Request.WithdrawConvertCurrencyFund); - //close blockchain-bond + //close blockchain-bond and bobs-fund-investment app.post('/close-blockchain-bonds', Request.CloseBlockchainBond); app.post('/close-bobs-fund-investment', Request.CloseBobsFund); - //Manage user tags (Access to trusted IDs only) + //check balance for blockchain-bond and bobs-fund (trusted IDs only) + app.post('/check-blockchain-bond', Request.CheckBlockchainBondBalance); + app.post('/check-bobs-fund', Request.CheckBobsFundBalance); + + //Manage user tags (trusted IDs only) app.post('/add-tag', Request.AddUserTag); app.post('/remove-tag', Request.RemoveUserTag); app.post('/add-distributor', Request.AddDistributor); diff --git a/src/request.js b/src/request.js index 9ed2316..d560a12 100644 --- a/src/request.js +++ b/src/request.js @@ -104,10 +104,10 @@ function logRequest(floID, req_str, sign, proxy = false) { .then(_ => null).catch(error => console.error(error)); } -function processRequest(res, floID, pubKey, sign, rText, validateObj, marketFn) { +function processRequest(res, floID, pubKey, sign, rText, validateObj, marketFn, log = true) { validateRequest(validateObj, sign, floID, pubKey).then(req_str => { marketFn().then(result => { - logRequest(floID, req_str, sign, !pubKey); + if (log) logRequest(floID, req_str, sign, !pubKey); res.send(result); }).catch(error => { if (error instanceof INVALID) @@ -452,6 +452,28 @@ function CloseBobsFund(req, res) { }, () => bobs_fund.closeFund(data.fund_id, data.floID, `${data.timestamp}.${data.sign}`)); } +function CheckBlockchainBondBalance(req, res) { + let data = req.body; + if (!trustedIDs.includes(data.floID)) + res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied")); + else processRequest(res, data.floID, data.pubKey, data.sign, "Check blockchain-bond", { + type: "check_blockchain_bond", + prior_time: data.prior_time, + timestamp: data.timestamp + }, () => blockchain_bonds.checkBondBalance(data.prior_time), false); +} + +function CheckBobsFundBalance(req, res) { + let data = req.body; + if (!trustedIDs.includes(data.floID)) + res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied")); + else processRequest(res, data.floID, data.pubKey, data.sign, "Check bobs-fund", { + type: "check_bobs_fund", + prior_time: data.prior_time, + timestamp: data.timestamp + }, () => bobs_fund.checkFundBalance(data.prior_time), false); +} + /* Public Requests */ function GetLoginCode(req, res) { @@ -695,6 +717,8 @@ module.exports = { WithdrawConvertCurrencyFund, CloseBlockchainBond, CloseBobsFund, + CheckBlockchainBondBalance, + CheckBobsFundBalance, set trustedIDs(ids) { trustedIDs = ids; }, diff --git a/src/services/bobs-fund.js b/src/services/bobs-fund.js index 85ed663..414ad14 100644 --- a/src/services/bobs-fund.js +++ b/src/services/bobs-fund.js @@ -1,6 +1,7 @@ 'use strict'; const DB = require("../database"); +const { sink_chest, sink_groups } = require("../keys"); const eCode = require('../../docs/scripts/floExchangeAPI').errorCode; const pCode = require('../../docs/scripts/floExchangeAPI').processCode; const getRate = require('./conversion').getRate; @@ -210,7 +211,7 @@ function refreshBlockchainData(nodeList = []) { tx: true, filter: d => d.startsWith(bobsFund.productStr) }).then(result => { - let promises = []; + let txQueries = []; result.data.reverse().forEach(d => { let fund = bobsFund.parse(d.data); if (d.senders.has(bobsFund.config.adminID) && !/close:/.test(d.data)) { @@ -220,11 +221,11 @@ function refreshBlockchainData(nodeList = []) { let values = [fund_id, fund.start_date, fund.BTC_base, fund.USD_base, fund.fee, fund.duration]; if (fund.tapoutInterval) values.push(fund.topoutWindow, fund.tapoutInterval.join(',')); - promises.push(DB.query(`INSERT INTO BobsFund(fund_id, begin_date, btc_base, usd_base, fee, duration ${fund.tapoutInterval ? ", tapout_window, tapout_interval" : ""}) VALUES ? ON DUPLICATE KEY UPDATE fund_id=fund_id`, [[values]])); + txQueries.push([`INSERT INTO BobsFund(fund_id, begin_date, btc_base, usd_base, fee, duration ${fund.tapoutInterval ? ", tapout_window, tapout_interval" : ""}) VALUE (?) ON DUPLICATE KEY UPDATE fund_id=fund_id`, [values]]) } else fund_id = fund_id.pop().match(/[a-z0-9]{64}/).pop(); let investments = Object.entries(fund.investments).map(a => [fund_id, a[0], a[1].amount]); - promises.push(DB.query("INSERT INTO BobsFundInvestments(fund_id, floID, amount_in) VALUES ? ON DUPLICATE KEY UPDATE floID=floID", [investments])); + txQueries.push(["INSERT INTO BobsFundInvestments(fund_id, floID, amount_in) VALUES ? ON DUPLICATE KEY UPDATE floID=floID", [investments]]); } else { let fund_id = d.data.match(/close: [a-z0-9]{64}\|/); @@ -232,20 +233,16 @@ function refreshBlockchainData(nodeList = []) { fund_id = fund_id.pop().match(/[a-z0-9]{64}/).pop(); let closing_details = Object.entries(fund.investments).filter(a => typeof a[1].closed === "object" && a[1].closed.amountFinal).pop(); //only one close-fund will be there in a tx if (closing_details) - promises.push(DB.query("UPDATE BobsFundInvestments SET close_id=?, amount_out=? WHERE fund_id=? AND floID=?", [d.txid, closing_details[1].closed.amountFinal, fund_id, closing_details[0]])) + txQueries.push(["UPDATE BobsFundInvestments SET close_id=?, amount_out=? WHERE fund_id=? AND floID=?", + [d.txid, closing_details[1].closed.amountFinal, fund_id, closing_details[0]]]) } } }); - Promise.allSettled(promises).then(results => { - if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0)) { - console.debug(results.filter(r => r.status === "rejected")); - reject("Some fund data might not have been saved in database correctly"); - } - else - DB.query("INSERT INTO LastTx (floID, num) VALUE (?) ON DUPLICATE KEY UPDATE num=?", [[bobsFund.config.adminID, result.totalTxs], result.totalTxs]) - .then(_ => resolve(result.totalTxs)) - .catch(error => reject(error)); - }) + txQueries.push(["INSERT INTO LastTx (floID, num) VALUE (?) ON DUPLICATE KEY UPDATE num=?", + [[bobsFund.config.adminID, result.totalTxs], result.totalTxs]]) + DB.transaction(txQueries) + .then(_ => resolve(result.totalTxs)) + .catch(error => reject(["Bobs-Fund refresh data failed!", error])); }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -296,6 +293,59 @@ function closeFund(fund_id, floID, ref) { }) } +function checkFundBalance(prior_time) { + return new Promise((resolve, reject) => { + prior_time = new Date(prior_time); + let cur_date = Date.now(); + if (isNaN(prior_time) || prior_time.toString() == "Invalid Date") + return reject(INVALID(eCode.INVALID_VALUE, `Invalid Date for prior_time`)); + let sql_query = "SELECT bf.begin_date, bf.btc_base, bf.usd_base, bf.fee, bf.duration, fi.amount_in, cf.amount AS amount_close FROM BobsFund AS bf" + + " INNER JOIN BobsFundInvestments AS fi ON bf.fund_id = fi.fund_id" + + " LEFT JOIN CloseFundTransact AS cf ON fi.fund_id = cf.fund_id AND fi.floID = cf.floID" + + " WHERE fi.close_id IS NULL AND (cf.r_status IS NULL OR cf.r_status NOT IN (?))"; + DB.query(sql_query, [[pCode.STATUS_SUCCESS, pCode.STATUS_CONFIRMATION]]).then(result => { + getRate.BTC_USD().then(btc_rate => { + getRate.USD_INR().then(usd_rate => { + let pending = { require_amount_cash: 0, n_investment: 0 }, + ready = { require_amount_cash: 0, n_investment: 0 }, + upcoming = { require_amount_cash: 0, n_investment: 0 } + result.forEach(i => { + if (i.amount_close) { + pending.require_amount_cash += i.amount_close; + pending.n_investment++; + } else { + let end_date = bobsFund.dateAdder(i.begin_date, i.duration); + if (end_date < prior_time) { + let net_value = bobsFund.calcNetValue(i.btc_base, btc_rate, i.usd_base, usd_rate, i.amount_in, i.fee); + if (end_date > cur_date) { + upcoming.require_amount_cash += net_value; + upcoming.n_investment++; + } else { + ready.require_amount_cash += net_value; + ready.n_investment++; + } + } + } + }) + pending.require_amount_cash = global.toStandardDecimal(pending.require_amount_cash); + ready.require_amount_cash = global.toStandardDecimal(ready.require_amount_cash); + upcoming.require_amount_cash = global.toStandardDecimal(upcoming.require_amount_cash); + pending.require_amount_btc = global.toStandardDecimal(pending.require_amount_cash / (btc_rate * usd_rate)); + ready.require_amount_btc = global.toStandardDecimal(ready.require_amount_cash / (btc_rate * usd_rate)); + upcoming.require_amount_btc = global.toStandardDecimal(upcoming.require_amount_cash / (btc_rate * usd_rate)); + Promise.allSettled(sink_chest.list(sink_groups.BOBS_FUND) + .map(id => btcOperator.getBalance(btcOperator.convert.legacy2bech(id)))).then(result => { + let balance = result.filter(r => r.status === 'fulfilled').reduce((a, bal) => a += bal, 0); + resolve({ pending, ready, upcoming, balance }); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + + + }) +} + module.exports = { refresh(nodeList) { refreshBlockchainData(nodeList) @@ -303,5 +353,6 @@ module.exports = { .catch(error => console.error(error)); }, util: bobsFund, + checkFundBalance, closeFund } \ No newline at end of file diff --git a/src/services/bonds.js b/src/services/bonds.js index c5a8e28..4b43466 100644 --- a/src/services/bonds.js +++ b/src/services/bonds.js @@ -1,6 +1,7 @@ 'use strict'; const DB = require("../database"); +const { sink_chest, sink_groups } = require("../keys"); const eCode = require('../../docs/scripts/floExchangeAPI').errorCode; const pCode = require('../../docs/scripts/floExchangeAPI').processCode; const getRate = require('./conversion').getRate; @@ -193,28 +194,24 @@ function refreshBlockchainData(nodeList = []) { tx: true, filter: d => d.startsWith(blockchainBond.productStr) }).then(result => { - let promises = []; + let txQueries = []; result.data.reverse().forEach(d => { let bond = d.senders.has(blockchainBond.config.adminID) ? blockchainBond.parse.main(d.data) : null; if (bond && bond.amount) - promises.push(DB.query("INSERT INTO BlockchainBonds(bond_id, floID, amount_in, begin_date, btc_base, usd_base, gain_cut, min_ipa, max_period, lockin_period) VALUES ? ON DUPLICATE KEY UPDATE bond_id=bond_id", - [[[d.txid, bond.floID, bond.amount, bond.startDate, bond.BTC_base, bond.USD_base, bond.cut, bond.minIpa, bond.maxPeriod, bond.lockinPeriod]]])); + txQueries.push(["INSERT INTO BlockchainBonds(bond_id, floID, amount_in, begin_date, btc_base, usd_base, gain_cut, min_ipa, max_period, lockin_period) VALUE (?) ON DUPLICATE KEY UPDATE bond_id=bond_id", + [[d.txid, bond.floID, bond.amount, bond.startDate, bond.BTC_base, bond.USD_base, bond.cut, bond.minIpa, bond.maxPeriod, bond.lockinPeriod]]]); else { let details = blockchainBond.parse.end(d.data); if (details.bondID && details.amountFinal) - promises.push(DB.query("UPDATE BlockchainBonds SET close_id=?, amount_out=? WHERE bond_id=?", [d.txid, details.amountFinal, details.bondID])); + txQueries.push(["UPDATE BlockchainBonds SET close_id=?, amount_out=? WHERE bond_id=?", + [d.txid, details.amountFinal, details.bondID]]); } }); - Promise.allSettled(promises).then(results => { - if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0)) { - reject("Some bond data might not have been saved in database correctly"); - console.debug(results.filter(r => r.status === "rejected")); - } - else - DB.query("INSERT INTO LastTx (floID, num) VALUE (?) ON DUPLICATE KEY UPDATE num=?", [[blockchainBond.config.adminID, result.totalTxs], result.totalTxs]) - .then(_ => resolve(result.totalTxs)) - .catch(error => reject(error)); - }) + txQueries.push(["INSERT INTO LastTx (floID, num) VALUE (?) ON DUPLICATE KEY UPDATE num=?", + [[blockchainBond.config.adminID, result.totalTxs], result.totalTxs]]) + DB.transaction(txQueries) + .then(_ => resolve(result.totalTxs)) + .catch(error => reject(["Blockchain-bonds refresh data failed!", error])); }).catch(error => reject(error)) }).catch(error => reject(error)) }) @@ -249,6 +246,57 @@ function closeBond(bond_id, floID, ref) { }) } +function checkBondBalance(prior_time) { + return new Promise((resolve, reject) => { + prior_time = new Date(prior_time); + let cur_date = Date.now(); + if (isNaN(prior_time) || prior_time.toString() == "Invalid Date") + return reject(INVALID(eCode.INVALID_VALUE, `Invalid Date for prior_time`)); + let sql_query = "SELECT bb.*, cb.amount AS amount_close FROM BlockchainBonds AS bb" + + " LEFT JOIN CloseBondTransact AS cb ON bb.bond_id = cb.bond_id" + + " WHERE bb.close_id IS NULL AND (cb.r_status IS NULL OR cb.r_status NOT IN (?))"; + DB.query(sql_query, [[pCode.STATUS_SUCCESS, pCode.STATUS_CONFIRMATION]]).then(result => { + getRate.BTC_USD().then(btc_rate => { + getRate.USD_INR().then(usd_rate => { + let pending = { require_amount_cash: 0, n_bond: 0 }, + ready = { require_amount_cash: 0, n_bond: 0 }, + upcoming = { require_amount_cash: 0, n_bond: 0 } + result.forEach(bond => { + if (bond.amount_close) { + pending.require_amount_cash += bond.amount_close; + pending.n_bond++; + } else { + let end_date = blockchainBond.dateAdder(bond.begin_date, bond.lockin_period) + if (end_date < prior_time) { + let net_value = blockchainBond.calcNetValue(bond.btc_base, btc_rate, bond.begin_date, bond.min_ipa, bond.max_period, bond.gain_cut, bond.amount_in, bond.usd_base, usd_rate); + if (end_date > cur_date) { + upcoming.require_amount_cash += net_value; + upcoming.n_bond++; + } else { + ready.require_amount_cash += net_value; + ready.n_bond++; + } + } + } + + }); + pending.require_amount_cash = global.toStandardDecimal(pending.require_amount_cash); + ready.require_amount_cash = global.toStandardDecimal(ready.require_amount_cash); + upcoming.require_amount_cash = global.toStandardDecimal(upcoming.require_amount_cash); + pending.require_amount_btc = global.toStandardDecimal(pending.require_amount_cash / (btc_rate * usd_rate)); + ready.require_amount_btc = global.toStandardDecimal(ready.require_amount_cash / (btc_rate * usd_rate)); + upcoming.require_amount_btc = global.toStandardDecimal(upcoming.require_amount_cash / (btc_rate * usd_rate)); + Promise.allSettled(sink_chest.list(sink_groups.BLOCKCHAIN_BONDS) + .map(id => btcOperator.getBalance(btcOperator.convert.legacy2bech(id)))).then(result => { + let balance = result.filter(r => r.status === 'fulfilled').reduce((a, bal) => a += bal, 0); + resolve({ pending, ready, upcoming, balance }); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) +} + module.exports = { refresh(nodeList) { refreshBlockchainData(nodeList) @@ -256,5 +304,6 @@ module.exports = { .catch(error => console.error(error)); }, util: blockchainBond, + checkBondBalance, closeBond } \ No newline at end of file