From 90d2a4764e6c6babef8e6575a4bdb0b63aff553f Mon Sep 17 00:00:00 2001 From: sairajzero Date: Mon, 10 Oct 2022 19:19:18 +0530 Subject: [PATCH] Close-Blockchain-bond support - Adding feature to close blockchain bonds upon user requests (when lock-in period is over) - Improved some syntax and structure of coding --- args/schema.sql | 34 ++++++ src/background.js | 147 ++++++++++++++---------- src/blockchain.js | 72 +++++++----- src/market.js | 13 +-- src/services/bonds.js | 255 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 423 insertions(+), 98 deletions(-) create mode 100644 src/services/bonds.js diff --git a/args/schema.sql b/args/schema.sql index 0e15a73..c8a52e6 100644 --- a/args/schema.sql +++ b/args/schema.sql @@ -214,6 +214,39 @@ CREATE TABLE AuditTrade( /* External Service */ +CREATE TABLE BlockchainBonds( + bond_id VARCHAR(128) NOT NULL, + floID CHAR(34) NOT NULL, + amount_in DECIMAL(16, 2) NOT NULL, + begin_date DATE NOT NULL, + btc_base DECIMAL(16, 2) NOT NULL, + usd_base DECIMAL(16, 2) NOT NULL, + gain_cut DECIMAL(6, 5) NOT NULL, + min_ipa DECIMAL(6, 5) NOT NULL, + max_period VARCHAR(10) NOT NULL, + lockin_period VARCHAR(10) NOT NULL, + close_id VARCHAR(128), + amount_out DECIMAL(16, 2), + PRIMARY KEY(bond_id) +); + +CREATE TABLE CloseBondTransact( + id INT NOT NULL AUTO_INCREMENT, + bond_id VARCHAR(128) NOT NULL, + floID CHAR(34) NOT NULL, + amount DECIMAL(16, 2) NOT NULL, + end_date DATE NOT NULL, + ref_sign VARCHAR(180) NOT NULL, + btc_net DECIMAL(16, 2) NOT NULL, + usd_net DECIMAL(16, 2) NOT NULL, + txid VARCHAR(128), + close_id VARCHAR(128), + status VARCHAR(50) NOT NULL, + KEY(id), + PRIMARY KEY(bond_id), + FOREIGN KEY (bond_id) REFERENCES BlockchainBonds(bond_id) +); + CREATE TABLE DirectConvert( id INT NOT NULL AUTO_INCREMENT, floID CHAR(34) NOT NULL, @@ -225,6 +258,7 @@ CREATE TABLE DirectConvert( out_txid VARCHAR(128), locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status VARCHAR(50) NOT NULL, + PRIMARY KEY(id) ); /* Backup Feature (Tables & Triggers) */ diff --git a/src/background.js b/src/background.js index 8a11f92..2f3b431 100644 --- a/src/background.js +++ b/src/background.js @@ -1,12 +1,14 @@ 'use strict'; const blockchain = require('./blockchain'); const conversion_rates = require('./services/conversion').getRate; +const bond_util = require('./services/bonds').util; const { LAUNCH_SELLER_TAG, MAXIMUM_LAUNCH_SELL_CHIPS, } = require('./_constants')["market"]; +var DB; //container for database const _sql = require('./_constants').sql; var updateBalance; // container for updateBalance function @@ -15,19 +17,19 @@ const verifyTx = {}; function confirmDepositFLO() { DB.query("SELECT id, floID, txid FROM DepositCoin WHERE coin=? AND status=?", ["FLO", "PENDING"]).then(results => { - results.forEach(req => { - verifyTx.FLO(req.floID, req.txid).then(amount => { - addSellChipsIfLaunchSeller(req.floID, amount).then(txQueries => { - txQueries.push(updateBalance.add(req.floID, "FLO", amount)); - txQueries.push(["UPDATE DepositCoin SET status=?, amount=? WHERE id=?", ["SUCCESS", amount, req.id]]); + results.forEach(r => { + verifyTx.FLO(r.floID, r.txid).then(amount => { + addSellChipsIfLaunchSeller(r.floID, amount).then(txQueries => { + txQueries.push(updateBalance.add(r.floID, "FLO", amount)); + txQueries.push(["UPDATE DepositCoin SET status=?, amount=? WHERE id=?", ["SUCCESS", amount, r.id]]); DB.transaction(txQueries) - .then(result => console.debug("FLO deposited:", req.floID, amount)) + .then(result => console.debug("FLO deposited:", r.floID, amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)) }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE DepositCoin SET status=? WHERE id=?", ["REJECTED", req.id]) + DB.query("UPDATE DepositCoin SET status=? WHERE id=?", ["REJECTED", r.id]) .then(_ => null).catch(error => console.error(error)); }); }) @@ -90,25 +92,25 @@ function addSellChipsIfLaunchSeller(floID, quantity) { function confirmDepositToken() { DB.query("SELECT id, floID, txid FROM DepositToken WHERE status=?", ["PENDING"]).then(results => { - results.forEach(req => { - verifyTx.token(req.floID, req.txid).then(({ token, amount, flo_amount }) => { - DB.query("SELECT id FROM DepositCoin where floID=? AND coin=? AND txid=?", [req.floID, "FLO", req.txid]).then(result => { + results.forEach(r => { + verifyTx.token(r.floID, r.txid).then(({ token, amount, flo_amount }) => { + DB.query("SELECT id FROM DepositCoin where floID=? AND coin=? AND txid=?", [r.floID, "FLO", r.txid]).then(result => { let txQueries = []; //Add the FLO balance if necessary if (!result.length) { - txQueries.push(updateBalance.add(req.floID, "FLO", flo_amount)); - txQueries.push(["INSERT INTO DepositCoin(txid, floID, coin, amount, status) VALUES (?, ?, ?, ?, ?)", [req.txid, req.floID, "FLO", flo_amount, "SUCCESS"]]); + txQueries.push(updateBalance.add(r.floID, "FLO", flo_amount)); + txQueries.push(["INSERT INTO DepositCoin(txid, floID, coin, amount, status) VALUES (?, ?, ?, ?, ?)", [r.txid, r.floID, "FLO", flo_amount, "SUCCESS"]]); } - txQueries.push(["UPDATE DepositToken SET status=?, token=?, amount=? WHERE id=?", ["SUCCESS", token, amount, req.id]]); - txQueries.push(updateBalance.add(req.floID, token, amount)); + txQueries.push(["UPDATE DepositToken SET status=?, token=?, amount=? WHERE id=?", ["SUCCESS", token, amount, r.id]]); + txQueries.push(updateBalance.add(r.floID, token, amount)); DB.transaction(txQueries) - .then(result => console.debug("Token deposited:", req.floID, token, amount)) + .then(result => console.debug("Token deposited:", r.floID, token, amount)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE DepositToken SET status=? WHERE id=?", ["REJECTED", req.id]) + DB.query("UPDATE DepositToken SET status=? WHERE id=?", ["REJECTED", r.id]) .then(_ => null).catch(error => console.error(error)); }); }) @@ -142,24 +144,24 @@ verifyTx.token = function (sender, txid, currencyOnly = false) { function retryWithdrawalCoin() { DB.query("SELECT id, floID, coin, amount FROM WithdrawCoin WHERE status=?", ["PENDING"]).then(results => { - results.forEach(req => blockchain.sendCoin.retry(req.floID, req.coin, req.amount, req.id)); + results.forEach(r => blockchain.sendCoin.retry(r.floID, r.coin, r.amount, r.id)); }).catch(error => console.error(error)); } function retryWithdrawalToken() { DB.query("SELECT id, floID, token, amount FROM WithdrawToken WHERE status=?", ["PENDING"]).then(results => { - results.forEach(req => blockchain.sendToken.retry(req.floID, req.token, req.amount, req.id)); + results.forEach(r => blockchain.sendToken.retry(r.floID, r.token, r.amount, r.id)); }).catch(error => console.error(error)); } function confirmWithdrawalFLO() { DB.query("SELECT id, floID, amount, txid FROM WithdrawCoin WHERE coin=? AND status=?", ["FLO", "WAITING_CONFIRMATION"]).then(results => { - results.forEach(req => { - floBlockchainAPI.getTx(req.txid).then(tx => { + results.forEach(r => { + floBlockchainAPI.getTx(r.txid).then(tx => { if (!tx.blockheight || !tx.confirmations) //Still not confirmed return; - DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug("FLO withdrawed:", req.floID, req.amount)) + DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", r.id]) + .then(result => console.debug("FLO withdrawed:", r.floID, r.amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)); }) @@ -168,12 +170,12 @@ function confirmWithdrawalFLO() { function confirmWithdrawalBTC() { DB.query("SELECT id, floID, amount, txid FROM WithdrawCoin WHERE coin=? AND status=?", ["BTC", "WAITING_CONFIRMATION"]).then(results => { - results.forEach(req => { - btcOperator.getTx(req.txid).then(tx => { + results.forEach(r => { + btcOperator.getTx(r.txid).then(tx => { if (!tx.blockhash || !tx.confirmations) //Still not confirmed return; - DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug("BTC withdrawed:", req.floID, req.amount)) + DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", r.id]) + .then(result => console.debug("BTC withdrawed:", r.floID, r.amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)); }) @@ -182,10 +184,10 @@ function confirmWithdrawalBTC() { function confirmWithdrawalToken() { DB.query("SELECT id, floID, token, amount, txid FROM WithdrawToken WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { - results.forEach(req => { - floTokenAPI.getTx(req.txid).then(tx => { - DB.query("UPDATE WithdrawToken SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug("Token withdrawed:", req.floID, req.token, req.amount)) + results.forEach(r => { + floTokenAPI.getTx(r.txid).then(tx => { + DB.query("UPDATE WithdrawToken SET status=? WHERE id=?", ["SUCCESS", r.id]) + .then(result => console.debug("Token withdrawed:", r.floID, r.token, r.amount)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }) @@ -216,27 +218,27 @@ verifyTx.BTC = function (sender, txid) { function verifyConvert() { DB.query("SELECT id, floID, mode, in_txid FROM DirectConvert WHERE status=? AND coin=?", ["PENDING", "BTC"]).then(results => { - results.forEach(req => { + results.forEach(r => { if (mode == _sql.CONVERT_MODE_GET) { - verifyTx.token(req.floID, req.in_txid, true).then(({ amount }) => { + verifyTx.token(r.floID, r.in_txid, true).then(({ amount }) => { conversion_rates.BTC_INR().then(rate => { - blockchain.convertToCoin.init(req.floID, "BTC", amount, amount / rate, req.id) + blockchain.convertToCoin.init(r.floID, "BTC", amount, amount / rate, r.id) }).catch(error => console.error(error)) }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", req.id]) + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", r.id]) .then(_ => null).catch(error => console.error(error)); }); } else if (mode == _sql.CONVERT_MODE_PUT) { - verifyTx.BTC(req.floID, req.in_txid).then(quantity => { + verifyTx.BTC(r.floID, r.in_txid).then(quantity => { conversion_rates.BTC_INR().then(rate => { - blockchain.convertFromCoin.init(req.floID, quantity * rate, quantity, req.id) + blockchain.convertFromCoin.init(r.floID, quantity * rate, quantity, r.id) }).catch(error => console.error(error)) }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", req.id]) + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", r.id]) .then(_ => null).catch(error => console.error(error)); }); } @@ -246,48 +248,79 @@ function verifyConvert() { function retryConvert() { DB.query("SELECT id, floID, mode, amount, quantity FROM DirectConvert WHERE status=? AND coin=?", ["PROCESSING", "BTC"]).then(results => { - results.forEach(req => { + results.forEach(r => { if (mode == _sql.CONVERT_MODE_GET) - blockchain.convertToCoin.retry(req.floID, "BTC", req.quantity, req.id); + blockchain.convertToCoin.retry(r.floID, "BTC", r.quantity, r.id); else if (mode == _sql.CONVERT_MODE_PUT) - blockchain.convertFromCoin.retry(req.floID, req.amount, req.id) + blockchain.convertFromCoin.retry(r.floID, r.amount, r.id) }) }).catch(error => console.error(error)) } function confirmConvert() { DB.query("SELECT id, floID, mode, amount, quantity, out_txid FROM DirectConvert WHERE status=? AND coin=?", ["WAITING_CONFIRMATION", "BTC"]).then(results => { - results.forEach(req => { + results.forEach(r => { if (mode == _sql.CONVERT_MODE_GET) - btcOperator.getTx(req.out_txid).then(tx => { + btcOperator.getTx(r.out_txid).then(tx => { if (!tx.blockhash || !tx.confirmations) //Still not confirmed return; - DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug(`${req.floID} converted ${amount} to ${req.quantity} BTC`)) + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", r.id]) + .then(result => console.debug(`${r.floID} converted ${amount} to ${r.quantity} BTC`)) .catch(error => console.error(error)) }).catch(error => console.error(error)); else if (mode == _sql.CONVERT_MODE_PUT) - floTokenAPI.getTx(req.out_txid).then(tx => { - DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug(`${req.floID} converted ${req.quantity} BTC to ${amount}`)) + floTokenAPI.getTx(r.out_txid).then(tx => { + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", r.id]) + .then(result => console.debug(`${r.floID} converted ${r.quantity} BTC to ${amount}`)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }) }).catch(error => console.error(error)); } +function retryBondClosing() { + DB.query("SELECT id, floID, amount FROM CloseBondTransact WHERE status=?", ["PENDING"]).then(results => { + results.forEach(r => blockchain.bondTransact.retry(r.floID, r.amount, r.id)) + }).catch(error => console.error(error)) +} + +function confirmBondClosing() { + DB.query("SELECT * FROM CloseBondTransact WHERE status=?", ["WAITING_CONFIRMATION"]).then(result => { + results.forEach(r => { + floTokenAPI.getTx(r.txid).then(tx => { + let closeBondString = bond_util.stringify.end(r.bond_id, r.end_date, r.btc_net, r.usd_net, r.amount, r.ref_sign, r.txid); + floBlockchainAPI.writeData(global.myFloID, closeBondString, global.myPrivKey, bond_util.config.adminID).then(txid => { + DB.query("UPDATE CloseBondTransact SET status=?, close_id=? WHERE id=?", ["SUCCESS", txid, r.id]) + .then(result => console.debug("Bond closed:", r.bond_id)) + .catch(error => console.error(error)); + }).catch(error => console.error(error)) + }).catch(error => console.error(error)); + }) + }).catch(error => reject(error)) +} + +function processAll() { + confirmDepositFLO(); + confirmDepositToken(); + retryWithdrawalCoin(); + retryWithdrawalToken(); + confirmWithdrawalFLO(); + confirmWithdrawalBTC(); + confirmWithdrawalToken(); + verifyConvert(); + retryConvert(); + confirmConvert(); + retryBondClosing(); + confirmBondClosing(); +} + module.exports = { blockchain, - confirmDepositFLO, - confirmDepositToken, - retryWithdrawalCoin, - retryWithdrawalToken, - confirmWithdrawalFLO, - confirmWithdrawalBTC, - confirmWithdrawalToken, - verifyConvert, - retryConvert, - confirmConvert, + process: processAll, + set DB(db) { + DB = db; + blockchain.DB = db; + }, set updateBalance(f) { updateBalance = f; } diff --git a/src/blockchain.js b/src/blockchain.js index 812258e..7a475cd 100644 --- a/src/blockchain.js +++ b/src/blockchain.js @@ -2,18 +2,20 @@ var collectAndCall; //container for collectAndCall function from backup module var chests; //container for blockchain ids (where assets are stored) +var DB; //container for database -const WITHDRAWAL_MESSAGE = "(withdrawal from market)", - TYPE_TOKEN = "TOKEN", +const TYPE_TOKEN = "TOKEN", TYPE_COIN = "COIN", - TYPE_CONVERT = "CONVERT"; + TYPE_CONVERT = "CONVERT", + TYPE_BOND = "BOND"; const balance_locked = {}, balance_cache = {}, callbackCollection = { [TYPE_COIN]: {}, [TYPE_TOKEN]: {}, - [TYPE_CONVERT]: {} + [TYPE_CONVERT]: {}, + [TYPE_BOND]: {} }; function getBalance(sinkID, asset) { @@ -55,43 +57,43 @@ function getSinkID(quantity, asset, sinkList = null) { }) } -function sendTx(floID, asset, quantity, sinkID, sinkKey) { +const WITHDRAWAL_MESSAGE = { + [TYPE_COIN]: "(withdrawal from market)", + [TYPE_TOKEN]: "(withdrawal from market)", + [TYPE_CONVERT]: "(convert coin)", + [TYPE_BOND]: "(bond closing)" +} + +function sendTx(floID, asset, quantity, sinkID, sinkKey, message) { switch (asset) { case "FLO": - return floBlockchainAPI.sendTx(sinkID, floID, quantity, sinkKey, WITHDRAWAL_MESSAGE); + return floBlockchainAPI.sendTx(sinkID, floID, quantity, sinkKey, message); case "BTC": let btc_sinkID = btcOperator.convert.legacy2bech(sinkID), btc_receiver = btcOperator.convert.legacy2bech(floID); return btcOperator.sendTx(btc_sinkID, sinkKey, btc_receiver, quantity, null); default: - return floTokenAPI.sendToken(sinkKey, quantity, floID, WITHDRAWAL_MESSAGE, asset); + return floTokenAPI.sendToken(sinkKey, quantity, floID, message, asset); } } -const tableUpdate = { - [TYPE_COIN]: (id, txid) => { - DB.query("UPDATE WithdrawCoin SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) - .then(_ => null).catch(error => console.error(error)) - }, - [TYPE_TOKEN]: (id, txid) => { - DB.query("UPDATE WithdrawToken SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) - .then(_ => null).catch(error => console.error(error)) - }, - [TYPE_CONVERT]: (id, txid) => { - DB.query("UPDATE DirectConvert SET status=?, out_txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) - .then(_ => null).catch(error => console.error(error)); - } +const updateSyntax = { + [TYPE_COIN]: "UPDATE WithdrawCoin SET status=?, txid=? WHERE id=?", + [TYPE_TOKEN]: "UPDATE WithdrawToken SET status=?, txid=? WHERE id=?", + [TYPE_CONVERT]: "UPDATE DirectConvert SET status=?, out_txid=? WHERE id=?", + [TYPE_BOND]: "UPDATE CloseBondTransact SET status=?, txid=? WHERE id=?" }; function sendAsset(floID, asset, quantity, type, id) { getSinkID(quantity, asset).then(sinkID => { let callback = (sinkKey) => { //Send asset to user via API - sendTx(floID, asset, quantity, sinkID, sinkKey).then(txid => { + sendTx(floID, asset, quantity, sinkID, sinkKey, WITHDRAWAL_MESSAGE[type]).then(txid => { if (!txid) console.error("Transaction not successful"); else //Transaction was successful, Add in DB - tableUpdate[type](id, txid); + DB.query(updateSyntax[type], ["WAITING_CONFIRMATION", txid, id]) + .then(_ => null).catch(error => console.error(error)); }).catch(error => console.error(error)).finally(_ => { delete callbackCollection[type][id]; balance_locked[sinkID][asset] -= quantity; @@ -114,8 +116,7 @@ function sendCoin_init(floID, coin, quantity) { function sendCoin_retry(floID, coin, quantity, id) { if (id in callbackCollection[TYPE_COIN]) console.debug("A callback is already pending for this Coin transfer"); - else - sendAsset(floID, coin, quantity, TYPE_COIN, id); + else sendAsset(floID, coin, quantity, TYPE_COIN, id); } function sendToken_init(floID, token, quantity) { @@ -127,8 +128,7 @@ function sendToken_init(floID, token, quantity) { function sendToken_retry(floID, token, quantity, id) { if (id in callbackCollection[TYPE_TOKEN]) console.debug("A callback is already pending for this Token transfer"); - else - sendAsset(floID, token, quantity, TYPE_TOKEN, id); + else sendAsset(floID, token, quantity, TYPE_TOKEN, id); } function convertToCoin_init(floID, coin, currency_amount, coin_quantity, id) { @@ -139,9 +139,8 @@ function convertToCoin_init(floID, coin, currency_amount, coin_quantity, id) { function convertToCoin_retry(floID, coin, coin_quantity, id) { if (id in callbackCollection[TYPE_CONVERT]) - console.debug("A callback is already pending for this Coin Convert"); - else - sendAsset(floID, coin, coin_quantity, TYPE_CONVERT, id); + console.debug("A callback is already pending for this Coin convert"); + else sendAsset(floID, coin, coin_quantity, TYPE_CONVERT, id); } function convertFromCoin_init(floID, currency_amount, coin_quantity, id) { @@ -153,8 +152,13 @@ function convertFromCoin_init(floID, currency_amount, coin_quantity, id) { function convertFromCoin_retry(floID, current_amount, id) { if (id in callbackCollection[TYPE_CONVERT]) console.debug("A callback is already pending for this Coin Convert"); - else - sendAsset(floID, floGlobals.currency, current_amount, TYPE_CONVERT, id); + else sendAsset(floID, floGlobals.currency, current_amount, TYPE_CONVERT, id); +} + +function bondTransact_retry(floID, amount, id) { + if (id in callbackCollection[TYPE_BOND]) + console.debug("A callback is already pending for this Bond closing"); + else sendAsset(floID, floGlobals.currency, amount, TYPE_BOND, id); } module.exports = { @@ -182,5 +186,11 @@ module.exports = { convertFromCoin: { init: convertFromCoin_init, retry: convertFromCoin_retry + }, + bondTransact: { + retry: bondTransact_retry + }, + set DB(db) { + DB = db; } } \ No newline at end of file diff --git a/src/market.js b/src/market.js index 05a231d..5d03dc4 100644 --- a/src/market.js +++ b/src/market.js @@ -2,6 +2,7 @@ const coupling = require('./coupling'); const background = require('./background'); + const blockchain = background.blockchain; const { @@ -504,16 +505,7 @@ function periodicProcess() { 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(); - background.verifyConvert(); - background.retryConvert(); - background.confirmConvert(); + background.process(); console.debug("Last Block :", lastSyncBlockHeight); } }).catch(error => console.error(error)); @@ -558,6 +550,7 @@ module.exports = { set DB(db) { DB = db; coupling.DB = db; + blockchain.DB = db; }, set assetList(assets) { assetList = assets; diff --git a/src/services/bonds.js b/src/services/bonds.js new file mode 100644 index 0000000..6dba81b --- /dev/null +++ b/src/services/bonds.js @@ -0,0 +1,255 @@ +'use strict'; + +const eCode = require('../eCode_test')// require('../../docs/scripts/floExchangeAPI').errorCode; +const getRate = require('./conversion').getRate; + +var DB; //container for database + +const blockchainBond = (function () { + const magnitude = m => { + switch (m) { + case "thousand": return 1000; + case "lakh": case "lakhs": return 100000; + case "million": return 1000000; + case "crore": case "crores": return 10000000; + default: return null; + } + } + const parseNumber = (str) => { + let n = 0, + g = 0; + str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => { + if (!isNaN(s)) + g = parseFloat(s); + else { + let m = magnitude(s); + if (m !== null) { + n += m * g; + g = 0; + } + } + }); + return n + g; + } + const parsePeriod = (str) => { + let P = '', n = 0; + str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => { + if (!isNaN(s)) + n = parseFloat(s); + else switch (s) { + case "year(s)": case "year": case "years": P += (n + 'Y'); n = 0; break; + case "month(s)": case "month": case "months": P += (n + 'M'); n = 0; break; + case "day(s)": case "day": case "days": P += (n + 'D'); n = 0; break; + } + }); + return P; + } + const dateFormat = (date = null) => { + let d = (date ? new Date(date) : new Date()).toDateString(); + return [d.substring(8, 10), d.substring(4, 7), d.substring(11, 15)].join(" "); + } + const yearDiff = (d1 = null, d2 = null) => { + d1 = d1 ? new Date(d1) : new Date(); + d2 = d2 ? new Date(d2) : new Date(); + let y = d1.getYear() - d2.getYear(), + m = d1.getMonth() - d2.getMonth(), + d = d1.getDate() - d2.getDate() + return y + m / 12 + d / 365; + } + + const dateAdder = function (start_date, duration) { + let date = new Date(start_date); + let y = parseInt(duration.match(/\d+Y/)), + m = parseInt(duration.match(/\d+M/)), + d = parseInt(duration.match(/\d+D/)); + if (!isNaN(y)) + date.setFullYear(date.getFullYear() + y); + if (!isNaN(m)) + date.setMonth(date.getMonth() + m); + if (!isNaN(d)) + date.setDate(date.getDate() + d); + return date; + } + + function calcNetValue(BTC_base, BTC_net, startDate, minIpa, maxPeriod, cut, amount, USD_base, USD_net) { + let gain, duration, interest, net; + gain = (BTC_net - BTC_base) / BTC_base; + duration = yearDiff(Math.min(Date.now(), dateAdder(startDate, maxPeriod).getTime()), startDate); + interest = Math.max(cut * gain, minIpa * duration); + net = amount / USD_base; + net += net * interest; + return net * USD_net; + } + + function stringify_main(BTC_base, start_date, guaranteed_interest, guarantee_period, gain_cut, amount, USD_base, lockin_period, floID) { + return [ + `Product: RanchiMall Bitcoin Bond`, + `Base value: ${BTC_base} USD`, + `Date of bond start: ${start_date}`, + `Guaranteed interest: ${guaranteed_interest}% per annum simple for ${guarantee_period}`, + `Bond value: guaranteed interest or ${gain_cut}% of the gains whichever is higher`, + `Amount invested: Rs ${amount}`, + `USD INR rate at start: ${USD_base}`, + `Lockin period: ${lockin_period}`, + `FLO ID of Bond Holder: ${floID}` + ].join("|"); + } + + function parse_main(data) { + //Data (add bond) sent by admin + let details = {}; + data.split("|").forEach(d => { + d = d.split(': '); + switch (d[0].toLowerCase()) { + case "base value": + details["BTC_base"] = parseNumber(d[1].slice(0, -4)); break; + case "date of bond start": + details["startDate"] = new Date(d[1]); break; + case "guaranteed interest": + details["minIpa"] = parseFloat(d[1].match(/\d+%/)) / 100; + details["maxPeriod"] = parsePeriod(d[1].match(/for .+/).toString()); break; + case "bond value": + details["cut"] = parseFloat(d[1].match(/\d+%/)) / 100; break; + case "amount invested": + details["amount"] = parseNumber(d[1].substring(3)); break; + case "usd inr rate at start": + details["USD_base"] = parseFloat(d[1]); break; + case "lockin period": + details["lockinPeriod"] = parsePeriod(d[1]); break; + case "flo id of bond holder": + details["floID"] = d[1]; break; + } + }) + return details; + } + + + function stringify_end(bond_id, end_date, BTC_net, USD_net, amount, ref_sign, payment_ref) { + return [ + `Product: RanchiMall Bitcoin Bond`, + `Bond: ${bond_id}`, + `End value: ${BTC_net} USD`, + `Date of bond end: ${end_date}`, + `USD INR rate at end: ${USD_net}`, + `Amount withdrawn: Rs ${amount} via ${payment_ref}`, + `Reference: ${ref_sign}` + ].join("|"); + } + + function parse_end(data) { + //Data (end bond) send by market nodes + let details = {}; + data.split("|").forEach(d => { + d = d.split(': '); + switch (d[0].toLowerCase()) { + case "bond": + details["bondID"] = d[1]; + case "end value": + details["BTC_net"] = parseNumber(d[1].slice(0, -4)); break; + case "date of bond end": + details["endDate"] = new Date(d[1]); break; + case "amount withdrawn": + details["amountFinal"] = parseNumber(d[1].match(/\d.+ via/).toString()); + details["payment_refRef"] = d[1].match(/via .+/).toString().substring(4); break; + case "usd inr rate at end": + details["USD_net"] = parseFloat(d[1]); break; + case "reference": + details["refSign"] = d[1]; break; + } + }) + } + + return { + dateAdder, + dateFormat, + calcNetValue, + parse: { + main: parse_main, + end: parse_end + }, + stringify: { + main: stringify_main, + end: stringify_end + } + } + +})(); + +blockchainBond.config = { + adminID: "FBBstZ2GretgQqDP55yt8iVd4KNZkdvEzH", + application: "BlockchainBonds", + productStr: "Product: RanchiMall Bitcoin Bond", +} + +function refreshBlockchainData(nodeList = []) { + return new Promise((resolve, reject) => { + DB.query("SELECT num FROM LastTx WHERE floID=?", [blockchainBond.config.adminID]).then(result => { + let lastTx = result.length ? result[0].num : 0; + floBlockchainAPI.readData(blockchainBond.config.adminID, { + ignoreOld: lastTx, + senders: [nodeList].concat(blockchainBond.config.adminID), //sentOnly: true, + tx: true, + filter: d => d.startsWith(blockchainBond.config.productStr) + }).then(result => { + let promises = []; + result.data.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]]])); + 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])); + + } + }); + promises.push(DB.query("INSERT INTO LastTx (floID, num) VALUE (?, ?) ON DUPLICATE KEY UPDATE num=?", [blockchainBond.config.adminID, result.totalTxs, result.totalTxs])); + Promise.allSettled(promises).then(results => { + //console.debug(results.filter(r => r.status === "rejected")); + if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0)) + console.warn("Some bond data might not have been saved in database correctly"); + resolve(result.totalTxs); + }) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) +} + +function closeBond(bond_id, floID, ref) { + return new Promise((resolve, reject) => { + DB.query("SELECT status FROM CloseBondTransact WHERE bond_id=?", [bond_id]).then(result => { + if (result.length) + return reject(INVALID(eCode.DUPLICATE_ENTRY, `Bond closing already in process`)); + DB.query("SELECT * FROM BlockchainBonds WHERE bond_id=?", [bond_id]).then(result => { + if (!result.length) + return reject(INVALID(eCode.NOT_FOUND, 'Bond not found')); + let bond = result[0]; + if (bond.floID !== floID) + return reject(INVALID(eCode.NOT_OWNER, 'Bond doesnot belong to the user')); + if (bond.close_id) + return reject(INVALID(eCode.DUPLICATE_ENTRY, `Bond already closed (${bond.close_id})`)); + if (Date.now() < blockchainBond.dateAdder(bond.begin_date, bond.lockin_period).getTime()) + return reject(INVALID(eCode.INSUFFICIENT_PERIOD, 'Bond still in lock-in period')); + getRate.BTC_USD().then(btc_rate => { + getRate.USD_INR().then(usd_rate => { + let end_date = new Date(), + 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); + DB.query("INSERT INTO CloseBondTransact(bond_id, amount, end_date, btc_net, usd_net, ref_sign, status) VALUE ?", [[bond_id, net_value, end_date, btc_rate, usd_rate, ref, "PENDING"]]) + .then(result => resolve({ "USD_net": usd_rate, "BTC_net": btc_rate, "amount_out": net_value, "end_date": end_date })) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) +} + +module.exports = { + refresh: refreshBlockchainData, + set DB(db) { + DB = db; + }, + util: blockchainBond, + closeBond +} \ No newline at end of file