Close Bobs-Fund feature

- Adding feature to close bobs' fund investment upon user requests
- Added: API for the same
This commit is contained in:
sairajzero 2022-10-11 03:15:34 +05:30
parent 80879a250b
commit 8a82a0b675
5 changed files with 372 additions and 1 deletions

View File

@ -247,6 +247,45 @@ CREATE TABLE CloseBondTransact(
FOREIGN KEY (bond_id) REFERENCES BlockchainBonds(bond_id) 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( CREATE TABLE DirectConvert(
id INT NOT NULL AUTO_INCREMENT, id INT NOT NULL AUTO_INCREMENT,
floID CHAR(34) NOT NULL, floID CHAR(34) NOT NULL,

View File

@ -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) { exchangeAPI.init = function refreshDataFromBlockchain(adminID = floGlobals.adminID, appName = floGlobals.application) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let nodes, assets, tags, lastTx; let nodes, assets, tags, lastTx;

View File

@ -91,6 +91,7 @@ module.exports = function App(secret, DB) {
//close blockchain-bond //close blockchain-bond
app.post('/close-blockchain-bonds', Request.CloseBlockchainBond); app.post('/close-blockchain-bonds', Request.CloseBlockchainBond);
app.post('/close-bobs-fund-investment', Request.CloseBobsFund);
//Manage user tags (Access to trusted IDs only) //Manage user tags (Access to trusted IDs only)
app.post('/add-tag', Request.AddUserTag); app.post('/add-tag', Request.AddUserTag);

View File

@ -3,6 +3,7 @@
const market = require("./market"); const market = require("./market");
const conversion = require('./services/conversion'); const conversion = require('./services/conversion');
const blokchain_bonds = require("./services/bonds"); const blokchain_bonds = require("./services/bonds");
const bobs_fund = require("./services/bobs-fund");
const { const {
SIGN_EXPIRE_TIME, SIGN_EXPIRE_TIME,
@ -335,6 +336,18 @@ function CloseBlockchainBond(req, res) {
}, () => blokchain_bonds.closeBond(data.bond_id, data.floID, `${data.timestamp}.${data.sign}`)); }, () => 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 */ /* Public Requests */
function GetLoginCode(req, res) { function GetLoginCode(req, res) {
@ -531,6 +544,7 @@ module.exports = {
ConvertTo, ConvertTo,
ConvertFrom, ConvertFrom,
CloseBlockchainBond, CloseBlockchainBond,
CloseBobsFund,
set trustedIDs(ids) { set trustedIDs(ids) {
trustedIDs = ids; trustedIDs = ids;
}, },

286
src/services/bobs-fund.js Normal file
View 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
}