Close Bobs-Fund feature
- Adding feature to close bobs' fund investment upon user requests - Added: API for the same
This commit is contained in:
parent
80879a250b
commit
8a82a0b675
@ -247,6 +247,45 @@ CREATE TABLE CloseBondTransact(
|
||||
FOREIGN KEY (bond_id) REFERENCES BlockchainBonds(bond_id)
|
||||
);
|
||||
|
||||
CREATE TABLE BobsFund(
|
||||
fund_id VARCHAR(128) NOT NULL,
|
||||
begin_date DATE NOT NULL,
|
||||
btc_base DECIMAL(16, 2) NOT NULL,
|
||||
usd_base DECIMAL(16, 2) NOT NULL,
|
||||
fee DECIMAL(6, 5) NOT NULL,
|
||||
duration VARCHAR(10) NOT NULL,
|
||||
tapout_window VARCHAR(10),
|
||||
tapout_interval VARCHAR(40),
|
||||
PRIMARY KEY(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE BobsFundInvestments(
|
||||
fund_id VARCHAR(128) NOT NULL,
|
||||
floID CHAR(34) NOT NULL,
|
||||
amount_in DECIMAL(16, 2) NOT NULL,
|
||||
close_id VARCHAR(128),
|
||||
amount_out DECIMAL(16, 2),
|
||||
PRIMARY KEY(fund_id, floID),
|
||||
FOREIGN KEY (fund_id) REFERENCES BobsFund(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE CloseFundTransact(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
fund_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(fund_id, floID),
|
||||
FOREIGN KEY (fund_id) REFERENCES BobsFund(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE DirectConvert(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
|
||||
@ -1339,6 +1339,37 @@
|
||||
})
|
||||
}
|
||||
|
||||
exchangeAPI.closeBobsFundInvestment = function (fund_id, floID, privKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.verifyPrivKey(privKey, floID))
|
||||
return reject(ExchangeError(ExchangeError.BAD_REQUEST_CODE, "Invalid Private Key", errorCode.INVALID_PRIVATE_KEY));
|
||||
let request = {
|
||||
floID: floID,
|
||||
fund_id: fund_id,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
request.pubKey = floCrypto.getPubKeyHex(privKey);
|
||||
request.sign = signRequest({
|
||||
type: "close_bobs_fund",
|
||||
fund_id: data.fund_id,
|
||||
timestamp: data.timestamp
|
||||
}, privKey);
|
||||
console.debug(request);
|
||||
|
||||
fetch_api('/close-bobs-fund-investment', {
|
||||
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.init = function refreshDataFromBlockchain(adminID = floGlobals.adminID, appName = floGlobals.application) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let nodes, assets, tags, lastTx;
|
||||
|
||||
@ -91,6 +91,7 @@ module.exports = function App(secret, DB) {
|
||||
|
||||
//close blockchain-bond
|
||||
app.post('/close-blockchain-bonds', Request.CloseBlockchainBond);
|
||||
app.post('/close-bobs-fund-investment', Request.CloseBobsFund);
|
||||
|
||||
//Manage user tags (Access to trusted IDs only)
|
||||
app.post('/add-tag', Request.AddUserTag);
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
const market = require("./market");
|
||||
const conversion = require('./services/conversion');
|
||||
const blokchain_bonds = require("./services/bonds");
|
||||
const bobs_fund = require("./services/bobs-fund");
|
||||
|
||||
const {
|
||||
SIGN_EXPIRE_TIME,
|
||||
@ -335,6 +336,18 @@ function CloseBlockchainBond(req, res) {
|
||||
}, () => blokchain_bonds.closeBond(data.bond_id, data.floID, `${data.timestamp}.${data.sign}`));
|
||||
}
|
||||
|
||||
function CloseBobsFund(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Conversion", {
|
||||
type: "close_bobs_fund",
|
||||
fund_id: data.fund_id,
|
||||
timestamp: data.timestamp
|
||||
}, () => bobs_fund.closeFund(data.fund_id, data.floID, `${data.timestamp}.${data.sign}`));
|
||||
}
|
||||
|
||||
/* Public Requests */
|
||||
|
||||
function GetLoginCode(req, res) {
|
||||
@ -531,6 +544,7 @@ module.exports = {
|
||||
ConvertTo,
|
||||
ConvertFrom,
|
||||
CloseBlockchainBond,
|
||||
CloseBobsFund,
|
||||
set trustedIDs(ids) {
|
||||
trustedIDs = ids;
|
||||
},
|
||||
|
||||
286
src/services/bobs-fund.js
Normal file
286
src/services/bobs-fund.js
Normal file
@ -0,0 +1,286 @@
|
||||
'use strict';
|
||||
|
||||
const eCode = require('../../docs/scripts/floExchangeAPI').errorCode;
|
||||
const getRate = require('./conversion').getRate;
|
||||
|
||||
var DB; //container for database
|
||||
|
||||
const bobsFund = (function () {
|
||||
const productStr = "Bobs Fund";
|
||||
|
||||
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 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, USD_base, USD_net, amount, fee) {
|
||||
let gain, interest, net;
|
||||
gain = (BTC_net - BTC_base) / BTC_base;
|
||||
interest = gain * (1 - fee)
|
||||
net = amount / USD_base;
|
||||
net += net * interest;
|
||||
return net * USD_net;
|
||||
}
|
||||
|
||||
function stringify_main(BTC_base, USD_base, start_date, duration, investments, fee = 0, tapoutWindow = null, tapoutInterval = null) {
|
||||
let result = [
|
||||
`${productStr}`,
|
||||
`Base Value: ${BTC_base} USD`,
|
||||
`USD INR rate at start: ${USD_base}`,
|
||||
`Start date: ${dateFormat(start_date)}`,
|
||||
`Duration: ${duration}`,
|
||||
`Management Fee: ${fee != 0 ? fee + "%" : "0 (Zero)"}`
|
||||
];
|
||||
if (tapoutInterval) {
|
||||
if (Array.isArray(tapoutInterval)) {
|
||||
let x = tapoutInterval.pop(),
|
||||
y = tapoutInterval.join(", ")
|
||||
tapoutInterval = `${y} and ${x}`
|
||||
}
|
||||
result.push(`Tapout availability: ${tapoutWindow} after ${tapoutInterval}`);
|
||||
}
|
||||
result.push(`Investment(s) (INR): ${investments.map(f => `${f[0].trim()}-${f[1].trim()}`).join("; ")}`);
|
||||
return result.join("|");
|
||||
}
|
||||
|
||||
function stringify_continue(fundID, investments) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`continue: ${fundID}`,
|
||||
`Investment(s) (INR): ${investments.map(f => `${f[0].trim()}-${f[1].trim()}`).join("; ")}`
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function parse_main(data) {
|
||||
let funds = {};
|
||||
if (!Array.isArray(data))
|
||||
data = [data];
|
||||
data.forEach(fd => {
|
||||
let cont = /continue: [a-z0-9]{64}\|/.test(fd);
|
||||
fd.data.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "start date":
|
||||
cont ? null : funds["start_date"] = d[1]; break;
|
||||
case "base value":
|
||||
cont ? null : funds["BTC_base"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "usd inr rate at start":
|
||||
cont ? null : funds["USD_base"] = parseFloat(d[1]); break;
|
||||
case "duration":
|
||||
cont ? null : funds["duration"] = parsePeriod(d[1]); break;
|
||||
case "management fee":
|
||||
cont ? null : funds["fee"] = parseFloat(d[1]); break;
|
||||
case "tapout availability":
|
||||
let x = d[1].toLowerCase().split("after")
|
||||
funds["tapoutInterval"] = x[1].match(/\d+ [a-z]+/gi).map(y => parsePeriod(y))
|
||||
funds["topoutWindow"] = parsePeriod(x[0]); break;
|
||||
case "invesment(s) (inr)":
|
||||
case "investment(s) (inr)":
|
||||
funds["amounts"] = funds["amounts"] || [];
|
||||
funds["amounts"].push(d[1].split("; ").map(a => {
|
||||
a = a.split("-");
|
||||
return [a[0], parseNumber(a[1])]
|
||||
})); break;
|
||||
}
|
||||
});
|
||||
})
|
||||
return funds;
|
||||
}
|
||||
|
||||
function stringify_end(fund_id, floID, end_date, BTC_net, USD_net, amount, ref_sign, payment_ref) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`Fund: ${fund_id}`,
|
||||
`Investor: ${floID}`,
|
||||
`End value: ${BTC_net} USD`,
|
||||
`Date of withdrawal: ${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 fund) send by market nodes
|
||||
let details = {};
|
||||
data.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "fund":
|
||||
details["fundID"] = d[1]; break;
|
||||
case "investor":
|
||||
details["floID"] = d[1]; break;
|
||||
case "end value":
|
||||
details["BTC_net"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "date of withdrawal":
|
||||
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,
|
||||
continue: stringify_continue,
|
||||
end: stringify_end
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
bobsFund.config = {
|
||||
adminID: "FFXy5pJnfzu2fCDLhpUremyXQjGtFpgCDN",
|
||||
application: "BobsFund"
|
||||
}
|
||||
|
||||
function refreshBlockchainData(nodeList = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT num FROM LastTx WHERE floID=?", [bobsFund.config.adminID]).then(result => {
|
||||
let lastTx = result.length ? result[0].num : 0;
|
||||
floBlockchainAPI.readData(bobsFund.config.adminID, {
|
||||
ignoreOld: lastTx,
|
||||
senders: [nodeList].concat(bobsFund.config.adminID), //sentOnly: true,
|
||||
tx: true,
|
||||
filter: d => d.startsWith(bobsFund.config.productStr)
|
||||
}).then(result => {
|
||||
let promises = [];
|
||||
result.data.forEach(d => {
|
||||
let fund = d.senders.has(bobsFund.config.adminID) ? bobsFund.parse.main(d.data) : null;
|
||||
if (fund && fund.amount) {
|
||||
let fund_id = d.data.match(/continue: [a-z0-9]{64}\|/);
|
||||
if (!fund_id) {
|
||||
fund_id = d.txid;
|
||||
promises.push(DB.query("INSERT INTO BobsFund(fund_id, begin_date, btc_base, usd_base, fee, duration, tapout_window, tapout_interval) VALUES ? ON DUPLICATE KEY UPDATE fund_id=fund_id",
|
||||
[[[fund_id, fund.start_date, fund.BTC_base, fund.USD_base, fund.fee, fund.duration, fund.topoutWindow, fund.tapoutInterval]]]));
|
||||
} else
|
||||
fund_id = fund_id.pop().match(/[a-z0-9]{64}/).pop();
|
||||
let investments = fund.amounts.map(i => [fund_id, i[0], i[1]]);
|
||||
promises.push(DB.query("INSERT INTO BobsFundInvestments(fund_id, floID, amount_in) VALUES ?", [investments]));
|
||||
}
|
||||
else {
|
||||
let details = bobsFund.parse.end(d.data);
|
||||
if (details.fundID && details.floID && details.amountFinal)
|
||||
promises.push(DB.query("UPDATE BobsFundInvestments SET close_id=? amount_out=? WHERE fund_id=? AND floID=?", [d.txid, details.amountFinal, details.fundID, details.floID]));
|
||||
}
|
||||
});
|
||||
promises.push(DB.query("INSERT INTO LastTx (floID, num) VALUE (?, ?) ON DUPLICATE KEY UPDATE num=?", [bobsFund.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 fund data might not have been saved in database correctly");
|
||||
resolve(result.totalTxs);
|
||||
})
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function closeFund(fund_id, floID, ref) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT status FROM CloseFundTransact WHERE fund_id=?", [fund_id]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, `Fund closing already in process`));
|
||||
DB.query("SELECT * FROM BobsFund WHERE fund_id=?", [fund_id]).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID(eCode.NOT_FOUND, 'Fund not found'));
|
||||
let fund = result[0];
|
||||
DB.query("SELECT * FROM BobsFundInvestments WHERE fund_id=? AND floID=?", [fund_id, floID]).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID(eCode.NOT_OWNER, 'User is not an investor of this fund'));
|
||||
let investment = result[0];
|
||||
if (investment.close_id)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, `Fund investment already closed (${investment.close_id})`));
|
||||
/* TODO: tapout and lockin period check
|
||||
if (Date.now() < bobsFund.dateAdder(fund.begin_date, fund.duration).getTime())
|
||||
return reject(INVALID(eCode.INSUFFICIENT_PERIOD, 'Fund still in lock-in period'));
|
||||
*/
|
||||
getRate.BTC_USD().then(btc_rate => {
|
||||
getRate.USD_INR().then(usd_rate => {
|
||||
let end_date = new Date(),
|
||||
net_value = bobsFund.calcNetValue(fund.btc_base, btc_rate, fund.usd_base, usd_rate, investment.amount_in, fund.fee)
|
||||
DB.query("INSERT INTO CloseFundTransact(fund_id, floID, amount, end_date, btc_net, usd_net, ref_sign, status) VALUE ?", [[fund_id, floID, 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))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refresh: refreshBlockchainData,
|
||||
set DB(db) {
|
||||
DB = db;
|
||||
},
|
||||
util: bobsFund,
|
||||
closeFund
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user