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)
|
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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
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