Adding server files

This commit is contained in:
sairajzero 2023-06-12 14:23:52 +05:30
parent 285d4342c4
commit a8dc399113
16 changed files with 3712 additions and 0 deletions

127
src/app.js Normal file
View File

@ -0,0 +1,127 @@
'use strict';
const express = require('express');
//const cookieParser = require("cookie-parser");
//const sessions = require('express-session');
const Request = require('./request');
const path = require('path');
const PUBLIC_DIR = path.resolve(__dirname, '..', 'docs');
module.exports = function App(secret) {
if (!(this instanceof App))
return new App(secret);
var server = null;
const app = express();
//session middleware
/*app.use(sessions({
secret: secret,
saveUninitialized: true,
resave: false,
name: "session"
}));*/
// parsing the incoming data
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
//serving public file
app.use(express.static(PUBLIC_DIR));
// cookie parser middleware
//app.use(cookieParser());
/* Decentralising - Users will load from user-end files and request via APIs only
//Initital page loading
app.get('/', (req, res) => res.sendFile('home.html', {
root: PUBLIC_DIR
}));
*/
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', "*");
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
// Pass to next layer of middleware
next();
})
//get code for login
app.get('/get-login-code', Request.GetLoginCode);
//login request
app.post('/login', Request.Login);
//logout request
app.post('/logout', Request.Logout);
//place sell or buy order
app.post('/buy', Request.PlaceBuyOrder);
app.post('/sell', Request.PlaceSellOrder);
//cancel sell or buy order
app.post('/cancel', Request.CancelOrder);
//transfer amount to another user
//app.post('/transfer-asset', Request.TransferAsset);
//list all orders and trades
app.get('/list-sellorders', Request.ListSellOrders);
app.get('/list-buyorders', Request.ListBuyOrders);
app.get('/list-trades', Request.ListTradeTransactions);
//get rates, balance and tx
app.get('/rate-history', Request.GetRateHistory);
//app.get('/get-balance', Request.GetBalance);
//app.get('/get-transaction', Request.GetTransaction);
app.get('/get-sink', Request.GetSink);
//get account details
app.post('/account', Request.Account);
//withdraw and deposit request
app.post('/deposit-asset', Request.DepositAsset);
app.post('/withdraw-asset', Request.WithdrawAsset);
app.post('/get-transact', Request.GetUserTransacts);
//generate or discard sinks (admin only)
app.post('/generate-sink', Request.GenerateSink);
app.post('/reshare-sink', Request.ReshareSink);
app.post('/discard-sink', Request.DiscardSink);
Request.secret = secret;
//Properties
let self = this;
//return server, express-app
Object.defineProperty(self, "server", {
get: () => server
});
Object.defineProperty(self, "express", {
get: () => app
});
//Refresh data (from blockchain)
//self.refreshData = (nodeList) => Request.refreshData(nodeList);
//Start (or) Stop servers
self.start = (port) => new Promise(resolve => {
server = app.listen(port, () => {
resolve(`Server Running at port ${port}`);
});
});
self.stop = () => new Promise(resolve => {
server.close(() => {
server = null;
resolve('Server stopped');
});
});
//(Node is not master) Pause serving the clients
self.pause = () => Request.pause();
//(Node is master) Resume serving the clients
self.resume = () => Request.resume();
}

249
src/background.js Normal file
View File

@ -0,0 +1,249 @@
'use strict';
const keys = require('./keys');
const blockchain = require('./blockchain');
const pCode = require('../docs/scripts/floTradeAPI').processCode;
const DB = require("./database");
const coupling = require('./coupling');
const price = require('./price');
const {
PERIOD_INTERVAL,
REQUEST_TIMEOUT
} = require('./_constants')['background'];
var updateBalance; // container for updateBalance function
const verifyTx = {};
function confirmDepositFLO() {
DB.query("SELECT id, floID, txid FROM VaultTransactions WHERE mode=? AND asset=? AND asset_type=? AND r_status=?", [pCode.VAULT_MODE_DEPOSIT, "FLO", pCode.ASSET_TYPE_COIN, pCode.STATUS_PENDING]).then(results => {
results.forEach(r => {
verifyTx.FLO(r.floID, r.txid, keys.sink_groups.TRADE).then(amount => {
var txQueries = [];
txQueries.push(updateBalance.add(r.floID, "FLO", amount));
txQueries.push(["UPDATE VaultTransactions SET r_status=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, amount, r.id]]);
DB.transaction(txQueries)
.then(result => console.info("FLO deposited:", r.floID, amount))
.catch(error => console.error(error))
}).catch(error => {
console.error(error);
if (error[0])
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
.then(_ => null).catch(error => console.error(error));
});
})
}).catch(error => console.error(error))
}
verifyTx.FLO = function (sender, txid, group) {
return new Promise((resolve, reject) => {
floBlockchainAPI.getTx(txid).then(tx => {
let vin_sender = tx.vin.filter(v => v.addr === sender)
if (!vin_sender.length)
return reject([true, "Transaction not sent by the sender"]);
if (vin_sender.length !== tx.vin.length)
return reject([true, "Transaction input containes other floIDs"]);
if (!tx.blockheight)
return reject([false, "Transaction not included in any block yet"]);
if (!tx.confirmations)
return reject([false, "Transaction not confirmed yet"]);
let amount = tx.vout.reduce((a, v) => keys.sink_chest.includes(group, v.scriptPubKey.addresses[0]) ? a + v.value : a, 0);
if (amount == 0)
return reject([true, "Transaction receiver is not market ID"]); //Maybe reject as false? (to compensate delay in chestsList loading from other nodes)
else
resolve(amount);
}).catch(error => reject([false, error]))
})
}
function confirmDepositBTC() {
DB.query("SELECT id, floID, txid FROM VaultTransactions WHERE mode=? AND asset=? AND asset_type=? AND r_status=?", [pCode.VAULT_MODE_DEPOSIT, "BTC", pCode.ASSET_TYPE_COIN, pCode.STATUS_PENDING]).then(results => {
results.forEach(r => {
verifyTx.BTC(r.floID, r.txid, keys.sink_groups.TRADE).then(amount => {
var txQueries = [];
txQueries.push(updateBalance.add(r.floID, "BTC", amount));
txQueries.push(["UPDATE VaultTransactions SET r_status=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, amount, r.id]]);
DB.transaction(txQueries)
.then(result => console.info("BTC deposited:", r.floID, amount))
.catch(error => console.error(error))
}).catch(error => {
console.error(error);
if (error[0])
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
.then(_ => null).catch(error => console.error(error));
});
})
}).catch(error => console.error(error))
}
verifyTx.BTC = function (sender, txid, group) {
return new Promise((resolve, reject) => {
btcOperator.getTx(txid).then(tx => {
let vin_sender = tx.inputs.filter(v => floCrypto.isSameAddr(v.address, sender))
if (!vin_sender.length)
return reject([true, "Transaction not sent by the sender"]);
if (vin_sender.length !== tx.inputs.length)
return reject([true, "Transaction input containes other floIDs"]);
if (!tx.block)
return reject([false, "Transaction not included in any block yet"]);
if (!tx.confirmations)
return reject([false, "Transaction not confirmed yet"]);
let amount = tx.outputs.reduce((a, v) =>
keys.sink_chest.includes(group, floCrypto.toFloID(v.address, { bech: [coinjs.bech32.version] })) ? a + parseFloat(v.value) : a, 0);
if (amount == 0)
return reject([true, "Transaction receiver is not market ID"]); //Maybe reject as false? (to compensate delay in chestsList loading from other nodes)
else
resolve(amount);
}).catch(error => reject([false, error]))
})
}
/*
function confirmDepositToken() {
DB.query("SELECT id, floID, txid FROM VaultTransactions WHERE mode=? AND asset_type=? AND r_status=?", [pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_TOKEN, pCode.STATUS_PENDING]).then(results => {
results.forEach(r => {
verifyTx.token(r.floID, r.txid, keys.sink_groups.TRADE).then(({ token, amount, flo_amount }) => {
DB.query("SELECT id FROM VaultTransactions where floID=? AND mode=? AND asset=? AND asset_type=? AND txid=?", [r.floID, pCode.VAULT_MODE_DEPOSIT, "FLO", pCode.ASSET_TYPE_COIN, r.txid]).then(result => {
let txQueries = [];
//Add the FLO balance if necessary
if (!result.length) {
txQueries.push(updateBalance.add(r.floID, "FLO", flo_amount));
txQueries.push(["INSERT INTO VaultTransactions(txid, floID, mode, asset_type, asset, amount, r_status) VALUES (?)", [[r.txid, r.floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_COIN, "FLO", flo_amount, pCode.STATUS_SUCCESS]]]);
}
txQueries.push(["UPDATE VaultTransactions SET r_status=?, asset=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, token, amount, r.id]]);
txQueries.push(updateBalance.add(r.floID, token, amount));
DB.transaction(txQueries)
.then(result => console.info("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 VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
.then(_ => null).catch(error => console.error(error));
});
})
}).catch(error => console.error(error))
}
verifyTx.token = function (sender, txid, group, currencyOnly = false) {
return new Promise((resolve, reject) => {
floTokenAPI.getTx(txid).then(tx => {
if (tx.parsedFloData.type !== "transfer")
return reject([true, "Transaction type not 'transfer'"]);
else if (tx.parsedFloData.transferType !== "token")
return reject([true, "Transaction transfer is not 'token'"]);
var token = tx.parsedFloData.tokenIdentification,
amount = tx.parsedFloData.tokenAmount;
if (currencyOnly && token !== floGlobals.currency)
return reject([true, "Token not currency"]);
else if (!currencyOnly && ((!keys.assets.includes(token) && token !== floGlobals.currency) || token === "FLO"))
return reject([true, "Token not authorised"]);
let vin_sender = tx.transactionDetails.vin.filter(v => v.addr === sender)
if (!vin_sender.length)
return reject([true, "Transaction not sent by the sender"]);
let flo_amount = tx.transactionDetails.vout.reduce((a, v) => keys.sink_chest.includes(group, v.scriptPubKey.addresses[0]) ? a + v.value : a, 0);
if (flo_amount == 0)
return reject([true, "Transaction receiver is not market ID"]); //Maybe reject as false? (to compensate delay in chestsList loading from other nodes)
else
resolve({ token, amount, flo_amount });
}).catch(error => reject([false, error]))
})
}
*/
function retryVaultWithdrawal() {
DB.query("SELECT id, floID, asset, asset_type, amount FROM VaultTransactions WHERE mode=? AND r_status=?", [pCode.VAULT_MODE_WITHDRAW, pCode.STATUS_PENDING]).then(results => {
results.forEach(r => {
if (r.asset_type == pCode.ASSET_TYPE_COIN) {
if (r.asset == "FLO" || r.asset == "BTC")
blockchain.withdrawAsset.retry(r.floID, r.asset, r.amount, r.id);
} else if (r.asset_type == pCode.ASSET_TYPE_TOKEN)
blockchain.withdrawAsset.retry(r.floID, r.asset, r.amount, r.id)
})
}).catch(error => console.error(error))
}
function confirmVaultWithdraw() {
DB.query("SELECT id, floID, asset, asset_type, amount, txid FROM VaultTransactions WHERE mode=? AND r_status=?", [pCode.VAULT_MODE_WITHDRAW, pCode.STATUS_CONFIRMATION]).then(results => {
results.forEach(r => {
if (r.asset_type == pCode.ASSET_TYPE_COIN) {
if (r.asset == "FLO")
floBlockchainAPI.getTx(r.txid).then(tx => {
if (!tx.blockheight || !tx.confirmations) //Still not confirmed
return;
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
.then(result => console.info("FLO withdrawed:", r.floID, r.amount))
.catch(error => console.error(error))
}).catch(error => console.error(error));
else if (r.asset == "BTC")
btcOperator.getTx(r.txid).then(tx => {
if (!tx.block || !tx.confirmations) //Still not confirmed
return;
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
.then(result => console.info("BTC withdrawed:", r.floID, r.amount))
.catch(error => console.error(error))
}).catch(error => console.error(error));
} else if (r.asset_type == pCode.ASSET_TYPE_TOKEN)
floTokenAPI.getTx(r.txid).then(tx => {
if (!tx.transactionDetails.blockheight || !tx.transactionDetails.confirmations) //Still not confirmed
return;
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
.then(result => console.info("Token withdrawed:", r.floID, r.asset, r.amount))
.catch(error => console.error(error));
}).catch(error => console.error(error));
})
}).catch(error => console.error(error));
}
//Periodic Process
function processAll() {
//deposit-withdraw asset balance
if (keys.sink_chest.list(keys.sink_groups.TRADE).length) {
confirmDepositFLO();
confirmDepositBTC();
//confirmDepositToken(); //commented as its only BTC to FLO and vise versa for now
retryVaultWithdrawal();
confirmVaultWithdraw();
}
}
var lastSyncBlockHeight = 0;
function periodicProcess() {
floBlockchainAPI.promisedAPI('api/blocks?limit=1').then(result => {
if (lastSyncBlockHeight < result.blocks[0].height) {
lastSyncBlockHeight = result.blocks[0].height;
processAll();
console.log("Last Block :", lastSyncBlockHeight);
}
}).catch(error => console.error(error));
}
function periodicProcess_start() {
periodicProcess_stop();
periodicProcess();
//keys.assets.tradeList.forEach(asset => coupling.initiate(asset)); //Trigger pairing only when order placed
periodicProcess.instance = setInterval(periodicProcess, PERIOD_INTERVAL);
}
function periodicProcess_stop() {
if (periodicProcess.instance !== undefined) {
clearInterval(periodicProcess.instance);
delete periodicProcess.instance;
}
coupling.stopAll();
}
module.exports = {
blockchain,
periodicProcess: {
start: periodicProcess_start,
stop: periodicProcess_stop
},
set updateBalance(f) {
updateBalance = f;
}
}

492
src/backup/head.js Normal file
View File

@ -0,0 +1,492 @@
'use strict';
const keys = require('../keys');
const K_Bucket = require('../../docs/scripts/floTradeAPI').K_Bucket;
const slave = require('./slave');
const sync = require('./sync');
const WebSocket = require('ws');
const { BACKUP_INTERVAL } = require("../_constants")["backup"];
const { DISCARD_COOLDOWN } = require("../_constants")["keys"];
var _app, _wss; //Container for app and wss
var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list
const connectedSlaves = {},
shares_collected = {},
shares_pending = {},
discarded_sinks = [];
var _mode = null;
const SLAVE_MODE = 0,
MASTER_MODE = 1;
//Shares
function generateShares(sinkKey) {
let nextNodes = nodeKBucket.nextNode(keys.node_id, null),
aliveNodes = Object.keys(connectedSlaves);
nextNodes.unshift(keys.node_id);
aliveNodes.unshift(keys.node_id);
let shares, mappedShares = {};
shares = keys.generateShares(sinkKey, nextNodes.length, aliveNodes.length);
for (let i in nextNodes)
mappedShares[nextNodes[i]] = shares[i];
return mappedShares;
}
function sendShares(ws, sinkID) {
if (!(sinkID in shares_pending) || !(ws.floID in shares_pending[sinkID].shares))
return;
let { ref, group } = shares_pending[sinkID],
shares = shares_pending[sinkID].shares[ws.floID];
delete shares_pending[sinkID].shares[ws.floID]; //delete the share after sending it to respective slave
shares.forEach(s => ws.send(JSON.stringify({
command: "SINK_SHARE",
sinkID, ref, group,
share: floCrypto.encryptData(s, ws.pubKey)
})));
}
function sendSharesToNodes(sinkID, group, shares) {
if (discarded_sinks.includes(sinkID)) { //sinkID is discarded, abort the new shares
let i = discarded_sinks.findIndex(sinkID);
discarded_sinks.splice(i, 1);
return;
}
let ref = Date.now();
shares_pending[sinkID] = { shares, group, ref };
if (keys.node_id in shares) {
shares_pending[sinkID].shares[keys.node_id].forEach(s =>
keys.addShare(group, sinkID, ref, s).then(_ => null).catch(error => console.error(error)));
delete shares_pending[sinkID].shares[keys.node_id];
}
for (let node in shares)
if (node in connectedSlaves)
sendShares(connectedSlaves[node], sinkID);
keys.sink_chest.set_id(group, sinkID, ref);
}
function requestShare(ws, group, sinkID) {
ws.send(JSON.stringify({
command: "SEND_SHARE",
group, sinkID,
pubKey: keys.node_pub
}));
}
/*
function transferMoneyToNewSink(oldSinkID, oldSinkKey, newSink) {
const transferToken = token => new Promise((resolve, reject) => {
floTokenAPI.getBalance(oldSinkID, token).then(tokenBalance => {
floBlockchainAPI.writeData(oldSinkID, `send ${tokenBalance} ${token}# |Trade-market New sink`, oldSinkKey, newSink.floID, false)
.then(txid => resolve(txid))
.catch(error => reject(error))
})
});
return new Promise((resolve, reject) => {
console.debug("Transferring tokens to new Sink:", newSink.floID)
Promise.allSettled(tokenList.map(token => transferToken(token))).then(result => {
let failedFlag = false;
tokenList.forEach((token, i) => {
if (result[i].status === "fulfilled")
console.log(token, result[i].value);
else {
failedFlag = true;
console.error(token, result[i].reason);
}
});
if (failedFlag)
return reject("Some token transfer has failed");
floBlockchainAPI.getBalance(oldSinkID).then(floBalance => {
floTokenAPI.getBalance(oldSinkID).then(cashBalance => {
floBlockchainAPI.sendTx(oldSinkID, newSink.floID, floBalance - floGlobals.fee, oldSinkKey, `send ${cashBalance} ${floGlobals.currency}# |Trade-market New sink`)
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => reject(error));
}).catch(error => reject(error))
});
})
}
*/
function collectAndCall(group, sinkID, callback, timeout = null) {
if (!(callback instanceof Function))
throw Error("callback should be a function");
if (!(sinkID in shares_collected)) { //if not already collecting shares for sinkID, then initiate collection
shares_collected[sinkID] = { group, ref: 0, callbacks: [], shares: {} };
for (let floID in connectedSlaves)
requestShare(connectedSlaves[floID], group, sinkID);
keys.getShares(group, sinkID)
.then(({ ref, shares }) => shares.forEach(s => collectShares(sinkID, ref, s)))
.catch(error => console.error(error))
}
shares_collected[sinkID].callbacks.push(callback);
if (timeout)
setTimeout(() => {
if (sinkID in shares_collected) {
let i = shares_collected[sinkID].callbacks.indexOf(callback);
delete shares_collected[sinkID].callbacks[i]; //deleting will empty the index, but space will be there so that order of other indexes are not affected
}
}, timeout);
}
collectAndCall.isAlive = (sinkID, callbackRef) => (sinkID in shares_collected && shares_collected[sinkID].callbacks.indexOf(callbackRef) != -1);
function collectShares(sinkID, ref, share) {
if (_mode !== MASTER_MODE)
return console.warn("Not serving as master");
if (!(sinkID in shares_collected))
return console.debug("Received shares for sink thats not been collected right now");
if (shares_collected[sinkID].ref > ref)
return console.debug("Received expired share");
else if (shares_collected[sinkID].ref < ref) {
shares_collected[sinkID].ref = ref;
shares_collected[sinkID].shares = [];
}
if (shares_collected[sinkID].shares.includes(share))
return console.debug("Received duplicate share");
shares_collected[sinkID].shares.push(share);
try {
let sinkKey = floCrypto.retrieveShamirSecret(shares_collected[sinkID].shares);
if (floCrypto.verifyPrivKey(sinkKey, sinkID)) {
console.debug("Shares collected successfully for", sinkID);
shares_collected[sinkID].callbacks.forEach(fn => fn instanceof Function ? fn(sinkKey) : null);
delete shares_collected[sinkID];
}
} catch {
//Unable to retrive sink private key. Waiting for more shares! Do nothing for now
};
}
function connectWS(floID) {
let url = nodeURL[floID];
return new Promise((resolve, reject) => {
const ws = new WebSocket('wss://' + url);
ws.on('open', _ => resolve(ws));
ws.on('error', error => reject(error));
})
}
function connectToMaster(i = 0, init = false) {
if (i >= nodeList.length) {
console.error("No master is found and Node not in list. This should not happen!");
process.exit(1);
}
let floID = nodeList[i];
if (floID === keys.node_id)
serveAsMaster(init);
else
connectWS(floID).then(ws => {
ws.floID = floID;
ws.onclose = () => connectToMaster(i);
serveAsSlave(ws, init);
}).catch(error => {
console.log(`Node(${floID}) is offline`);
connectToMaster(i + 1, init)
});
}
function informLiveNodes(init) {
let message = {
floID: keys.node_id,
type: "UPDATE_MASTER",
pubKey: keys.node_pub,
req_time: Date.now()
};
message.sign = floCrypto.signData(message.type + "|" + message.req_time, keys.node_priv);
message = JSON.stringify(message);
let nodes = nodeList.filter(n => n !== keys.node_id);
Promise.allSettled(nodes.map(n => connectWS(n))).then(result => {
let flag = false;
for (let i in result)
if (result[i].status === "fulfilled") {
let ws = result[i].value;
ws.send(message);
ws.close();
flag = true;
} else
console.warn(`Node(${nodes[i]}) is offline`);
if (init && flag)
syncRequest();
keys.getStoredList().then(stored_list => {
if (Object.keys(stored_list).length) {
keys.getDiscardedList().then(discarded_list => {
let cur_time = Date.now();
for (let group in stored_list)
stored_list[group].forEach(id => {
if (!(id in discarded_list))
reconstructShares(group, id)
else if (cur_time - discarded_list[id] < DISCARD_COOLDOWN) //sinkID still in cooldown period
keys.sink_chest.set_id(group, id, null);
});
}).catch(error => console.error(error))
} else if (init && !flag) {
console.log("Starting the server...");
//generate a sinkID for each group in starting list
keys.sink_groups.initial_list.forEach(group =>
generateSink(group).then(_ => null).catch(e => console.error(e)));
}
}).catch(error => console.error(error));
});
}
function syncRequest(cur = keys.node_id) {
//Sync data from next available node
let nextNode = nodeKBucket.nextNode(cur);
if (!nextNode)
return console.warn("No nodes available to Sync");
connectWS(nextNode)
.then(ws => slave.syncRequest(ws))
.catch(_ => syncRequest(nextNode));
}
function updateMaster(floID) {
let currentMaster = _mode === MASTER_MODE ? keys.node_id : slave.masterWS.floID;
if (nodeList.indexOf(floID) < nodeList.indexOf(currentMaster))
connectToMaster();
}
function reconstructAllActiveShares() {
if (_mode !== MASTER_MODE)
return console.debug("Not serving as master");
console.debug("Reconstructing shares for all active IDs")
let group_list = keys.sink_groups.list;
group_list.forEach(g => {
//active ids also ignore ids that are in queue for reconstructing shares
let active_ids = keys.sink_chest.active_list(g);
active_ids.forEach(id => reconstructShares(g, id));
});
}
function reconstructShares(group, sinkID) {
if (_mode !== MASTER_MODE)
return console.warn(`Not serving as master, but reconstruct-shares is called for ${sinkID}(${group})`);
keys.sink_chest.set_id(group, sinkID, null);
collectAndCall(group, sinkID, sinkKey => sendSharesToNodes(sinkID, group, generateShares(sinkKey)));
}
function slaveConnect(floID, pubKey, ws, slave_sinks) {
if (_mode !== MASTER_MODE)
return console.warn("Not serving as master");
ws.floID = floID;
ws.pubKey = pubKey;
connectedSlaves[floID] = ws;
//Send shares if need to be delivered
for (let sinkID in shares_pending)
if (floID in shares_pending[sinkID].shares)
sendShares(ws, sinkID);
//Request shares if any
for (let sinkID in shares_collected)
requestShare(ws, shares_collected[sinkID].group, sinkID);
//check if sinks in slaves are present
if (slave_sinks instanceof Object) {
for (let group in slave_sinks)
for (let sinkID of slave_sinks[group]) {
if (!keys.sink_chest.includes(group, sinkID))
keys.checkIfDiscarded(sinkID)
.then(result => result === false ? reconstructShares(group, sinkID) : null)
.catch(error => console.error(error))
}
}
}
const eCode = require('../../docs/scripts/floTradeAPI').errorCode;
function generateSink(group) {
return new Promise((resolve, reject) => {
if (!keys.sink_groups.generate_list.includes(group))
return reject(INVALID(eCode.INVALID_VALUE, `Invalid Group ${group}`));
try {
let newSink = floCrypto.generateNewID();
console.debug("Generated sink:", group, newSink.floID);
sendSharesToNodes(newSink.floID, group, generateShares(newSink.privKey));
resolve(`Generated ${newSink.floID} (${group})`);
} catch (error) {
reject(error)
}
})
}
function reshareSink(id) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(id))
return reject(INVALID(eCode.INVALID_VALUE, `Invalid ID ${id}`));
else {
let group = keys.sink_chest.find_group(id);
if (!group)
return reject(INVALID(eCode.NOT_FOUND, `ID ${id} not found`));
else keys.checkIfDiscarded(id).then(result => {
if (result)
return reject(INVALID(eCode.NOT_FOUND, `ID is discarded`));
try {
reconstructShares(group, id);
resolve(`Resharing ${id} (${group})`);
} catch (error) {
reject(error);
}
}).catch(error => reject(error))
}
})
}
function discardSink(id) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(id))
return reject(INVALID(eCode.INVALID_VALUE, `Invalid ID ${id}`));
else if (!keys.sink_chest.find_group(id))
return reject(INVALID(eCode.NOT_FOUND, `ID ${id} not found`));
else keys.checkIfDiscarded(id).then(result => {
if (result)
return reject(INVALID(eCode.DUPLICATE_ENTRY, `ID already discarded`));
keys.discardSink(id).then(result => {
console.debug("Discarded sink:", id);
resolve(result);
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function checkForDiscardedSinks() {
let cur_time = Date.now(),
all_sinks = keys.sink_chest.get_all();
for (let group in all_sinks)
all_sinks[group].forEach(id => keys.checkIfDiscarded(id).then(result => {
console.debug(group, id); //Check if group is correctly mapped, or if its changed by loop
if (result != false) {
if (cur_time - result > DISCARD_COOLDOWN)
keys.sink_chest.rm_id(group, id);
else
keys.sink_chest.set_id(group, id, null);
if (id in shares_collected && !discarded_sinks.includes(id))
discarded_sinks.push(id);
}
}).catch(error => console.debug(error)))
}
//Master interval process
function intervalProcess() {
checkForDiscardedSinks();
}
intervalProcess.start = () => {
intervalProcess.stop();
intervalProcess.instance = setInterval(intervalProcess, BACKUP_INTERVAL);
}
intervalProcess.stop = () => {
if (intervalProcess.instance !== undefined) {
clearInterval(intervalProcess.instance);
delete intervalProcess.instance;
}
}
//Node becomes master
function serveAsMaster(init) {
console.info('Starting master process');
slave.stop();
_mode = MASTER_MODE;
keys.sink_chest.reset();
intervalProcess.start();
informLiveNodes(init);
_app.resume();
}
//Node becomes slave
function serveAsSlave(ws, init) {
console.info('Starting slave process');
intervalProcess.stop();
_app.pause();
slave.start(ws, init);
_mode = SLAVE_MODE;
}
//Transmistter
function startBackupTransmitter(server) {
_wss = new WebSocket.Server({
server
});
_wss.on('connection', ws => {
ws.on('message', message => {
//verify if from a backup node
try {
let invalid = null,
request = JSON.parse(message);
//console.debug(request);
if (!nodeList.includes(request.floID))
invalid = `floID ${request.floID} not in nodeList`;
else if (request.floID !== floCrypto.getFloID(request.pubKey))
invalid = "Invalid pubKey";
else if (!floCrypto.verifySign(request.type + "|" + request.req_time, request.sign, request.pubKey))
invalid = "Invalid signature";
//TODO: check if request time is valid;
else switch (request.type) {
case "BACKUP_SYNC":
sync.sendBackupData(request.last_time, request.checksum, ws);
break;
case "HASH_SYNC":
sync.sendTableHash(request.tables, ws);
break;
case "RE_SYNC":
sync.sendTableData(request.tables, ws);
break;
case "UPDATE_MASTER":
updateMaster(request.floID);
break;
case "SLAVE_CONNECT":
slaveConnect(request.floID, request.pubKey, ws, request.sinks);
break;
case "SINK_SHARE":
collectShares(request.sinkID, request.ref, floCrypto.decryptData(request.share, keys.node_priv))
default:
invalid = "Invalid Request Type";
}
if (invalid)
ws.send(JSON.stringify({
type: request.type,
command: "REQUEST_ERROR",
error: invalid
}));
} catch (error) {
console.error(error);
ws.send(JSON.stringify({
command: "REQUEST_ERROR",
error: 'Unable to process the request!'
}));
}
});
ws.on('close', () => {
// remove from connected slaves (if needed)
if (ws.floID in connectedSlaves)
delete connectedSlaves[ws.floID];
})
});
}
function initProcess(app) {
_app = app;
startBackupTransmitter(_app.server);
connectToMaster(0, true);
}
module.exports = {
init: initProcess,
collectAndCall,
reconstructAllActiveShares,
sink: {
generate: generateSink,
reshare: reshareSink,
discard: discardSink
},
set nodeList(list) {
nodeURL = list;
nodeKBucket = new K_Bucket(floGlobals.adminID, Object.keys(nodeURL));
nodeList = nodeKBucket.order;
},
get nodeList() {
return nodeList;
},
get wss() {
return _wss;
}
};

429
src/backup/slave.js Normal file
View File

@ -0,0 +1,429 @@
'use strict';
const keys = require("../keys");
const DB = require("../database");
const { getTableHashes } = require("./sync");
const {
BACKUP_INTERVAL,
BACKUP_SYNC_TIMEOUT,
CHECKSUM_INTERVAL,
HASH_N_ROW
} = require("../_constants")["backup"];
var masterWS = null; //Container for Master websocket connection
var intervalID = null;
function startSlaveProcess(ws, init) {
if (!ws) throw Error("Master WS connection required");
//stop existing process
stopSlaveProcess();
//set masterWS
ws.on('message', processDataFromMaster);
masterWS = ws;
let sinks_stored = {};
Promise.all([keys.getStoredList(), keys.getDiscardedList()]).then(result => {
let stored_list = result[0],
discarded_list = result[1];
for (let group in stored_list) {
sinks_stored[group] = [];
for (let id of stored_list[group])
if (!(id in discarded_list))
sinks_stored[group].push(id);
}
}).catch(error => console.error(error)).finally(_ => {
//inform master
let message = {
floID: keys.node_id,
pubKey: keys.node_pub,
sinks: sinks_stored,
req_time: Date.now(),
type: "SLAVE_CONNECT"
}
message.sign = floCrypto.signData(message.type + "|" + message.req_time, keys.node_priv);
ws.send(JSON.stringify(message));
//start sync
if (init)
requestInstance.open();
intervalID = setInterval(() => requestInstance.open(), BACKUP_INTERVAL);
})
}
function stopSlaveProcess() {
if (masterWS !== null) {
masterWS.onclose = () => null;
masterWS.close();
requestInstance.close();
masterWS = null;
}
if (intervalID !== null) {
clearInterval(intervalID);
intervalID = null;
}
}
function requestBackupSync(checksum_trigger, ws) {
return new Promise((resolve, reject) => {
DB.query('SELECT MAX(u_time) as last_time FROM _backup').then(result => {
let request = {
floID: keys.node_id,
pubKey: keys.node_pub,
type: "BACKUP_SYNC",
last_time: result[0].last_time,
checksum: checksum_trigger,
req_time: Date.now()
};
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
ws.send(JSON.stringify(request));
resolve(request);
}).catch(error => reject(error))
})
}
const requestInstance = {
ws: null,
cache: null,
checksum: null,
delete_data: null,
add_data: null,
request: null,
onetime: null,
last_response_time: null,
checksum_count_down: 0
};
requestInstance.open = function (ws = null) {
const self = this;
//Check if there is an active request
if (self.request) {
console.log("A request is already active");
if (self.last_response_time < Date.now() - BACKUP_SYNC_TIMEOUT)
self.close();
else
return;
}
//Use websocket connection if passed, else use masterWS if available
if (ws) {
ws.on('message', processDataFromMaster);
self.onetime = true;
} else if (masterWS)
ws = masterWS;
else return console.warn("Not connected to master");
requestBackupSync(!self.checksum_count_down || self.onetime, ws).then(request => {
self.request = request;
self.cache = [];
self.last_response_time = Date.now();
self.ws = ws;
}).catch(error => console.error(error))
}
requestInstance.close = function () {
const self = this;
if (self.onetime)
self.ws.close();
else
self.checksum_count_down = self.checksum_count_down ? self.checksum_count_down - 1 : CHECKSUM_INTERVAL;
self.onetime = null;
self.ws = null;
self.cache = null;
self.checksum = null;
self.delete_data = null;
self.add_data = null;
self.request = null;
self.last_response_time = null;
}
function processDataFromMaster(message) {
try {
message = JSON.parse(message);
//console.debug("Master:", message);
if (message.command.startsWith("SYNC"))
processBackupData(message);
else switch (message.command) {
case "SINK_SHARE":
storeSinkShare(message.group, message.sinkID, message.share, message.ref);
break;
case "SEND_SHARE":
sendSinkShare(message.group, message.sinkID, message.pubKey);
break;
case "REQUEST_ERROR":
console.log(message.error);
if (message.type === "BACKUP_SYNC")
requestInstance.close();
break;
}
} catch (error) {
console.error(error);
}
}
function storeSinkShare(group, sinkID, share, ref) {
share = floCrypto.decryptData(share, keys.node_priv);
keys.addShare(group, sinkID, ref, share)
.then(_ => null).catch(error => console.error(error));
}
function sendSinkShare(group, sinkID, pubKey) {
keys.getShares(group, sinkID).then(({ ref, shares }) => {
shares.forEach(s => {
let response = {
type: "SINK_SHARE",
sinkID, ref,
share: floCrypto.encryptData(s, pubKey),
floID: keys.node_id,
pubKey: keys.node_pub,
req_time: Date.now()
}
response.sign = floCrypto.signData(response.type + "|" + response.req_time, keys.node_priv); //TODO: strengthen signature
masterWS.send(JSON.stringify(response));
})
}).catch(error => console.error(error));
}
function processBackupData(response) {
//TODO: Sync improvements needed. (2 types)
//1. Either sync has to be completed or rollback all
//2. Each table/data should be treated as independent chunks
const self = requestInstance;
self.last_response_time = Date.now();
switch (response.command) {
case "SYNC_END":
if (response.status) {
storeBackupData(self.cache, self.checksum).then(result => {
updateBackupTable(self.add_data, self.delete_data);
if (result) {
console.log("Backup Sync completed successfully");
self.close();
} else
console.log("Waiting for re-sync data");
}).catch(_ => {
console.warn("Backup Sync was not successful");
self.close();
});
} else {
console.info("Backup Sync was not successful! Failed info: ", response.info);
self.close();
}
break;
case "SYNC_DELETE":
self.delete_data = response.delete_data;
self.cache.push(cacheBackupData(null, response.delete_data));
break;
case "SYNC_HEADER":
self.add_data = response.add_data;
break;
case "SYNC_UPDATE":
self.cache.push(cacheBackupData(response.table, response.data));
break;
case "SYNC_CHECKSUM":
self.checksum = response.checksum;
break;
case "SYNC_HASH":
verifyHash(response.hashes)
.then(mismatch => requestTableChunks(mismatch, self.ws))
.catch(error => {
console.error(error);
self.close();
});
break;
}
}
const cacheBackupData = (tableName, dataCache) => new Promise((resolve, reject) => {
DB.query("INSERT INTO _backupCache (t_name, data_cache) VALUE (?, ?)", [tableName, JSON.stringify(dataCache)])
.then(_ => resolve(true)).catch(error => {
console.error(error);
reject(false);
})
});
function storeBackupData(cache_promises, checksum_ref) {
return new Promise((resolve, reject) => {
Promise.allSettled(cache_promises).then(_ => {
console.log("START: BackupCache -> Tables");
//Process 'Users' table 1st as it provides foreign key attribute to other tables
DB.query("SELECT * FROM _backupCache WHERE t_name=?", ["Users"]).then(data => {
Promise.allSettled(data.map(d => updateTableData("Users", JSON.parse(d.data_cache)))).then(result => {
storeBackupData.commit(data, result).then(_ => {
DB.query("SELECT * FROM _backupCache WHERE t_name IS NOT NULL").then(data => {
Promise.allSettled(data.map(d => updateTableData(d.t_name, JSON.parse(d.data_cache)))).then(result => {
storeBackupData.commit(data, result).then(_ => {
DB.query("SELECT * FROM _backupCache WHERE t_name IS NULL").then(data => {
Promise.allSettled(data.map(d => deleteTableData(JSON.parse(d.data_cache)))).then(result => {
storeBackupData.commit(data, result).then(_ => {
console.log("END: BackupCache -> Tables");
if (!checksum_ref) //No checksum verification
resolve(true);
else
verifyChecksum(checksum_ref)
.then(result => resolve(result))
.catch(error => reject(error))
});
})
})
})
})
}).catch(error => {
console.error(error);
console.warn("ABORT: BackupCache -> Tables");
reject(false);
});
})
})
}).catch(error => {
console.error(error);
console.warn("ABORT: BackupCache -> Tables");
reject(false);
})
})
})
}
storeBackupData.commit = function (data, result) {
let promises = [];
for (let i = 0; i < data.length; i++)
switch (result[i].status) {
case "fulfilled":
promises.push(DB.query("DELETE FROM _backupCache WHERE id=?", data[i].id));
break;
case "rejected":
console.error(result[i].reason);
promises.push(DB.query("UPDATE _backupCache SET fail=TRUE WHERE id=?", data[i].id));
break;
}
return Promise.allSettled(promises);
}
function updateBackupTable(add_data, delete_data) {
//update _backup table for added data
DB.transaction(add_data.map(r => [
"INSERT INTO _backup (t_name, id, mode, u_time) VALUE (?, ?, TRUE, ?) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=?",
[r.t_name, r.id, validateValue(r.u_time), validateValue(r.u_time)]
])).then(_ => null).catch(error => console.error(error));
//update _backup table for deleted data
DB.transaction(delete_data.map(r => [
"INSERT INTO _backup (t_name, id, mode, u_time) VALUE (?, ?, NULL, ?) ON DUPLICATE KEY UPDATE mode=NULL, u_time=?",
[r.t_name, r.id, validateValue(r.u_time), validateValue(r.u_time)]
])).then(_ => null).catch(error => console.error(error));
}
function deleteTableData(data) {
return new Promise((resolve, reject) => {
let delete_needed = {};
data.forEach(r => r.t_name in delete_needed ? delete_needed[r.t_name].push(r.id) : delete_needed[r.t_name] = [r.id]);
let queries = [];
for (let table in delete_needed)
queries.push(["DELETE FROM ?? WHERE id IN (?)", [table, delete_needed[table]]]);
DB.transaction(queries).then(_ => resolve(true)).catch(error => reject(error));
})
}
function updateTableData(table, data) {
return new Promise((resolve, reject) => {
if (!data.length)
return resolve(null);
let cols = Object.keys(data[0]);
let values = data.map(r => cols.map(c => validateValue(r[c])));
let statement = "INSERT INTO ?? (??) VALUES ? ON DUPLICATE KEY UPDATE " + Array(cols.length).fill("??=VALUES(??)").join();
let query_values = [table, cols, values];
cols.forEach(c => query_values.push(c, c));
DB.query(statement, query_values).then(_ => resolve(true)).catch(error => reject(error));
})
}
const validateValue = val => (typeof val === "string" && /\.\d{3}Z$/.test(val)) ? new Date(val) : val;
function verifyChecksum(checksum_ref) {
return new Promise((resolve, reject) => {
DB.query("CHECKSUM TABLE ??", [Object.keys(checksum_ref)]).then(result => {
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
let mismatch = [];
for (let table in checksum)
if (checksum[table] != checksum_ref[table])
mismatch.push(table);
//console.debug("Checksum-mismatch:", mismatch);
if (!mismatch.length) //Checksum of every table is verified.
resolve(true);
else { //If one or more tables checksum is not correct, re-request the table data
requestHash(mismatch);
resolve(false);
}
}).catch(error => {
console.error(error);
reject(false);
})
})
}
function requestHash(tables) {
//TODO: resync only necessary data (instead of entire table)
let self = requestInstance;
let request = {
floID: keys.node_id,
pubKey: keys.node_pub,
type: "HASH_SYNC",
tables: tables,
req_time: Date.now()
};
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
self.ws.send(JSON.stringify(request));
self.request = request;
self.checksum = null;
self.cache = [];
}
function verifyHash(hashes) {
const convertIntArray = obj => Object.keys(obj).map(i => parseInt(i));
const checkHash = (table, hash_ref) => new Promise((res, rej) => {
getTableHashes(table).then(hash_cur => {
for (let i in hash_ref)
if (hash_ref[i] === hash_cur[i]) {
delete hash_ref[i];
delete hash_cur[i];
}
res([convertIntArray(hash_ref), convertIntArray(hash_cur)]);
}).catch(error => rej(error))
})
return new Promise((resolve, reject) => {
let tables = Object.keys(hashes);
Promise.allSettled(tables.map(t => checkHash(t, hashes[t]))).then(result => {
let mismatch = {};
for (let t in tables)
if (result[t].status === "fulfilled") {
mismatch[tables[t]] = result[t].value; //Data that are incorrect/missing/deleted
//Data to be deleted (incorrect data will be added by resync)
let id_end = result[t].value[1].map(i => i * HASH_N_ROW); //eg if i=2 AND H_R_C = 5 then id_end = 2 * 5 = 10 (ie, range 6-10)
Promise.allSettled(id_end.map(i =>
DB.query("DELETE FROM ?? WHERE id BETWEEN ? AND ?", [tables[t], i - HASH_N_ROW + 1, i]) //eg, i - HASH_N_ROW + 1 = 10 - 5 + 1 = 6
)).then(_ => null);
} else
console.error(result[t].reason);
//console.debug("Hash-mismatch", mismatch);
resolve(mismatch);
}).catch(error => reject(error))
})
}
function requestTableChunks(tables, ws) {
let request = {
floID: keys.node_id,
pubKey: keys.node_pub,
type: "RE_SYNC",
tables: tables,
req_time: Date.now()
};
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
ws.send(JSON.stringify(request));
}
module.exports = {
get masterWS() {
return masterWS;
},
start: startSlaveProcess,
stop: stopSlaveProcess,
syncRequest: ws => requestInstance.open(ws)
}

282
src/backup/sync.js Normal file
View File

@ -0,0 +1,282 @@
'use strict';
const DB = require("../database");
const {
HASH_N_ROW
} = require("../_constants")["backup"];
//Backup Transfer
function sendBackupData(last_time, checksum, ws) {
if (!last_time) last_time = 0;
else if (typeof last_time === "string" && /\.\d{3}Z$/.test(last_time))
last_time = last_time.substring(0, last_time.length - 1);
let promises = [
backupSync_data(last_time, ws),
backupSync_delete(last_time, ws)
];
if (checksum)
promises.push(backupSync_checksum(ws));
Promise.allSettled(promises).then(result => {
let failedSync = [];
result.forEach(r => r.status === "rejected" ? failedSync.push(r.reason) : null);
if (failedSync.length) {
console.info("Backup Sync Failed:", failedSync);
ws.send(JSON.stringify({
command: "SYNC_END",
status: false,
info: failedSync
}));
} else {
console.info("Backup Sync completed");
ws.send(JSON.stringify({
command: "SYNC_END",
status: true
}));
}
});
}
function backupSync_delete(last_time, ws) {
return new Promise((resolve, reject) => {
DB.query("SELECT * FROM _backup WHERE mode is NULL AND u_time > ?", [last_time]).then(result => {
ws.send(JSON.stringify({
command: "SYNC_DELETE",
delete_data: result
}));
resolve("deleteSync");
}).catch(error => {
console.error(error);
reject("deleteSync");
});
})
}
function backupSync_data(last_time, ws) {
const sendTable = (table, id_list) => new Promise((res, rej) => {
DB.query("SELECT * FROM ?? WHERE id IN (?)", [table, id_list])
.then(data => {
ws.send(JSON.stringify({
table,
command: "SYNC_UPDATE",
data
}));
res(table);
}).catch(error => {
console.error(error);
rej(table);
});
});
return new Promise((resolve, reject) => {
DB.query("SELECT * FROM _backup WHERE mode=TRUE AND u_time > ?", [last_time]).then(result => {
let sync_needed = {};
result.forEach(r => r.t_name in sync_needed ? sync_needed[r.t_name].push(r.id) : sync_needed[r.t_name] = [r.id]);
ws.send(JSON.stringify({
command: "SYNC_HEADER",
add_data: result
}));
let promises = [];
for (let table in sync_needed)
promises.push(sendTable(table, sync_needed[table]));
Promise.allSettled(promises).then(result => {
let failedTables = [];
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
if (failedTables.length)
reject(["dataSync", failedTables]);
else
resolve("dataSync");
});
}).catch(error => {
console.error(error);
reject("dataSync");
});
});
}
function backupSync_checksum(ws) {
return new Promise((resolve, reject) => {
DB.query("SELECT DISTINCT t_name FROM _backup").then(result => {
let tableList = result.map(r => r['t_name']);
if (!tableList.length)
return resolve("checksum");
DB.query("CHECKSUM TABLE ??", [tableList]).then(result => {
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
ws.send(JSON.stringify({
command: "SYNC_CHECKSUM",
checksum: checksum
}));
resolve("checksum");
}).catch(error => {
console.error(error);
reject("checksum");
})
}).catch(error => {
console.error(error);
reject("checksum");
})
})
}
function sendTableHash(tables, ws) {
Promise.allSettled(tables.map(t => getTableHashes(t))).then(result => {
let hashes = {};
for (let i in tables)
if (result[i].status === "fulfilled")
hashes[tables[i]] = result[i].value;
else
console.error(result[i].reason);
ws.send(JSON.stringify({
command: "SYNC_HASH",
hashes: hashes
}));
})
}
function getTableHashes(table) {
return new Promise((resolve, reject) => {
DB.query("SHOW COLUMNS FROM ??", [table]).then(result => {
//columns
let columns = result.map(r => r["Field"]).sort();
//select statement
let statement = "SELECT CEIL(id/?) as group_id,";
let query_values = [HASH_N_ROW];
//aggregate column values
let col_aggregate = columns.map(c => "IFNULL(CRC32(??), 0)").join('+');
columns.forEach(c => query_values.push(c));
//aggregate rows via group by
statement += " SUM(CRC32(MD5(" + col_aggregate + "))) as hash FROM ?? GROUP BY group_id ORDER BY group_id";
query_values.push(table);
//query
DB.query(statement, query_values)
.then(result => resolve(Object.fromEntries(result.map(r => [r.group_id, r.hash]))))
.catch(error => reject(error))
}).catch(error => reject(error))
})
}
function sendTableData(tables, ws) {
let promises = [
tableSync_data(tables, ws),
tableSync_delete(tables, ws),
tableSync_checksum(Object.keys(tables), ws)
];
Promise.allSettled(promises).then(result => {
let failedSync = [];
result.forEach(r => r.status === "rejected" ? failedSync.push(r.reason) : null);
if (failedSync.length) {
console.info("Backup Sync Failed:", failedSync);
ws.send(JSON.stringify({
command: "SYNC_END",
status: false,
info: failedSync
}));
} else {
console.info("Backup Sync completed");
ws.send(JSON.stringify({
command: "SYNC_END",
status: true
}));
}
});
}
function tableSync_delete(tables, ws) {
let getDelete = (table, group_id) => new Promise((res, rej) => {
let id_end = group_id * HASH_N_ROW,
id_start = id_end - HASH_N_ROW + 1;
DB.query("SELECT * FROM _backup WHERE t_name=? AND mode is NULL AND (id BETWEEN ? AND ?)", [table, id_start, id_end])
.then(result => res(result))
.catch(error => rej(error))
})
return new Promise((resolve, reject) => {
let promises = [];
for (let t in tables)
for (let g_id in tables[t][1]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
promises.push(getDelete(t, g_id));
Promise.allSettled(promises).then(results => {
let delete_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
delete_sync = [].concat(...delete_sync); //Convert 2d array into 1d
ws.send(JSON.stringify({
command: "SYNC_DELETE",
delete_data: delete_sync
}));
resolve("deleteSync");
});
})
}
function tableSync_data(tables, ws) {
const sendTable = (table, group_id) => new Promise((res, rej) => {
let id_end = group_id * HASH_N_ROW,
id_start = id_end - HASH_N_ROW + 1;
DB.query("SELECT * FROM ?? WHERE id BETWEEN ? AND ?", [table, id_start, id_end]).then(data => {
ws.send(JSON.stringify({
table,
command: "SYNC_UPDATE",
data
}));
res(table);
}).catch(error => {
console.error(error);
rej(table);
});
});
const getUpdate = (table, group_id) => new Promise((res, rej) => {
let id_end = group_id * HASH_N_ROW,
id_start = id_end - HASH_N_ROW + 1;
DB.query("SELECT * FROM _backup WHERE t_name=? AND mode=TRUE AND (id BETWEEN ? AND ?)", [table, id_start, id_end])
.then(result => res(result))
.catch(error => rej(error))
})
return new Promise((resolve, reject) => {
let promises = [];
for (let t in tables)
for (let g_id of tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
promises.push(getUpdate(t, g_id));
Promise.allSettled(promises).then(results => {
let update_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
update_sync = [].concat(...update_sync); //Convert 2d array into 1d
ws.send(JSON.stringify({
command: "SYNC_HEADER",
add_data: update_sync
}));
let promises = [];
for (let t in tables)
for (let g_id of tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
promises.push(sendTable(t, g_id));
Promise.allSettled(promises).then(result => {
let failedTables = [];
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
if (failedTables.length)
reject(["dataSync", [...new Set(failedTables)]]);
else
resolve("dataSync");
});
});
})
}
function tableSync_checksum(tables, ws) {
return new Promise((resolve, reject) => {
DB.query("CHECKSUM TABLE ??", [tables]).then(result => {
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
ws.send(JSON.stringify({
command: "SYNC_CHECKSUM",
checksum: checksum
}));
resolve("checksum");
}).catch(error => {
console.error(error);
reject("checksum");
})
})
}
module.exports = {
getTableHashes,
sendBackupData,
sendTableHash,
sendTableData
}

123
src/blockchain.js Normal file
View File

@ -0,0 +1,123 @@
'use strict';
const pCode = require('../docs/scripts/floTradeAPI').processCode;
const { collectAndCall } = require('./backup/head');
const keys = require('./keys');
const DB = require("./database");
const TYPE_VAULT = "VAULT"
const SINK_GROUP = {
[TYPE_VAULT]: keys.sink_groups.TRADE
}
const balance_locked = {},
balance_cache = {},
callbackCollection = {
[TYPE_VAULT]: {}
};
function getBalance(sinkID, asset) {
switch (asset) {
case "FLO":
return floBlockchainAPI.getBalance(sinkID);
case "BTC":
let btc_id = btcOperator.convert.legacy2bech(sinkID);
return btcOperator.getBalance(btc_id);
default:
return floTokenAPI.getBalance(sinkID, asset);
}
}
function getSinkID(type, quantity, asset, sinkList = null) {
return new Promise((resolve, reject) => {
if (!sinkList)
sinkList = keys.sink_chest.list(SINK_GROUP[type]).map(s => [s, s in balance_cache ? balance_cache[s][asset] || 0 : 0]) //TODO: improve sorting
.sort((a, b) => b[1] - a[1]).map(x => x[0]);
if (!sinkList.length)
return reject(`Insufficient balance for asset(${asset}) in chest(${SINK_GROUP[type]})`);
let sinkID = sinkList.shift();
getBalance(sinkID, asset).then(balance => {
if (!(sinkID in balance_cache))
balance_cache[sinkID] = {};
balance_cache[sinkID][asset] = balance;
if (balance > (quantity + (sinkID in balance_locked ? balance_locked[sinkID][asset] || 0 : 0)))
return resolve(sinkID);
else
getSinkID(type, quantity, asset, sinkList)
.then(result => resolve(result))
.catch(error => reject(error))
}).catch(error => {
console.error(error);
getSinkID(type, quantity, asset, sinkList)
.then(result => resolve(result))
.catch(error => reject(error))
});
})
}
const WITHDRAWAL_MESSAGE = {
[TYPE_VAULT]: "(withdrawal from market)"
}
function sendTx(floID, asset, quantity, sinkID, sinkKey, message) {
switch (asset) {
case "FLO":
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, { fee_from_receiver: true });
default:
return floTokenAPI.sendToken(sinkKey, quantity, floID, message, asset);
}
}
const updateSyntax = {
[TYPE_VAULT]: "UPDATE VaultTransactions SET r_status=?, txid=? WHERE id=?"
};
function sendAsset(floID, asset, quantity, type, id) {
quantity = global.toStandardDecimal(quantity);
getSinkID(type, quantity, asset).then(sinkID => {
let callback = (sinkKey) => {
//Send asset to user via API
sendTx(floID, asset, quantity, sinkID, sinkKey, WITHDRAWAL_MESSAGE[type]).then(txid => {
if (!txid)
console.error("Transaction not successful");
else //Transaction was successful, Add in database
DB.query(updateSyntax[type], [pCode.STATUS_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;
});
}
collectAndCall(sinkID, callback); //TODO: add timeout to prevent infinite wait
callbackCollection[type][id] = callback;
if (!(sinkID in balance_locked))
balance_locked[sinkID] = {};
balance_locked[sinkID][asset] = (balance_locked[sinkID][asset] || 0) + quantity;
}).catch(error => console.error(error))
}
function withdrawAsset_init(floID, asset, amount) {
amount = global.toStandardDecimal(amount);
let asset_type = ["FLO", "BTC"].includes(asset) ? pCode.ASSET_TYPE_COIN : pCode.ASSET_TYPE_TOKEN;
DB.query("INSERT INTO VaultTransactions (floID, mode, asset_type, asset, amount, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_WITHDRAW, asset_type, asset, amount, pCode.STATUS_PENDING]])
.then(result => sendAsset(floID, asset, amount, TYPE_VAULT, result.insertId))
.catch(error => console.error(error))
}
function withdrawAsset_retry(floID, asset, amount, id) {
if (id in callbackCollection[TYPE_VAULT])
console.debug("A callback is already pending for this Coin transfer");
else sendAsset(floID, asset, amount, TYPE_VAULT, id);
}
module.exports = {
withdrawAsset: {
init: withdrawAsset_init,
retry: withdrawAsset_retry
}
}

203
src/coupling.js Normal file
View File

@ -0,0 +1,203 @@
'use strict';
const price = require("./price");
const DB = require("./database");
const {
WAIT_TIME,
TRADE_HASH_PREFIX
} = require("./_constants")["market"];
const updateBalance = {};
updateBalance.consume = (floID, asset, amount) => ["UPDATE UserBalance SET quantity=quantity-? WHERE floID=? AND asset=?", [amount, floID, asset]];
updateBalance.add = (floID, asset, amount) => ["INSERT INTO UserBalance (floID, asset, quantity) VALUE (?) ON DUPLICATE KEY UPDATE quantity=quantity+?", [[floID, asset, amount], amount]];
const couplingInstance = {},
couplingTimeout = {};
function stopAllInstance() {
for (let asset in couplingTimeout) {
if (couplingTimeout[asset])
clearTimeout(couplingTimeout[asset]);
delete couplingInstance[asset];
delete couplingTimeout[asset];
}
}
function startCouplingForAsset(asset) {
console.debug("startCouplingForAsset")
if (couplingInstance[asset] === true) { //if coupling is already running for asset
/*if (updatePrice) { //wait until current instance is over
if (couplingTimeout[asset]) clearTimeout(couplingTimeout[asset]);
couplingTimeout[asset] = setTimeout(() => startCouplingForAsset(asset, true), WAIT_TIME);
}*/
return;
}
couplingInstance[asset] = true; //set instance as running
recursiveCoupling(asset);
}
const getBestSeller = (asset) => new Promise((resolve, reject) => {
DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity, SellOrder.price FROM SellOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = SellOrder.floID AND UserBalance.asset = SellOrder.asset" +
" WHERE UserBalance.quantity >= SellOrder.quantity AND SellOrder.asset = ?" +
" ORDER BY SellOrder.price ASC, SellOrder.time_placed ASC" +
" LIMIT 1", [asset]
).then(result => {
if (result.length)
resolve(result[0]);
else
resolve(null);
}).catch(error => reject(error))
});
const getBestBuyer = (asset, sell_price) => new Promise((resolve, reject) => {
DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = BuyOrder.floID AND UserBalance.asset = ?" +
" WHERE UserBalance.quantity >= BuyOrder.maxPrice * BuyOrder.quantity AND BuyOrder.asset = ? AND BuyOrder.maxPrice >= ?" +
" ORDER BY BuyOrder.maxPrice DESC, BuyOrder.time_placed ASC" +
" LIMIT 1", [floGlobals.currency, asset, sell_price]
).then(result => {
if (result.length)
resolve(result[0]);
else
resolve(null);
}).catch(error => reject(error))
});
function recursiveCoupling(asset) {
console.debug("recursiveCoupling")
processCoupling(asset).then(result => {
if (!result) { //no valid orders to pair: exit pairing
console.debug("No valid orders to pair");
delete couplingInstance[asset];
return;
}
console.log(result);
if (couplingInstance[asset] === true)
recursiveCoupling(asset);
}).catch(error => {
console.error(error)
delete couplingInstance[asset];
})
}
function processCoupling(asset) {
return new Promise((resolve, reject) => {
getBestSeller(asset).then(best_sell => {
console.debug("Sell:", best_sell);
if (!best_sell) //no valid sell orders
return resolve(null);
let sell_rate = best_sell.price;
getBestBuyer(asset, sell_rate).then(best_buy => {
console.debug("Buy:", best_buy);
if (!best_buy) //no valid buy orders
return resolve(null);
let quantity = Math.min(best_buy.quantity, best_sell.quantity);
let txQueries = processOrders(best_sell, best_buy, asset, sell_rate, quantity);
//begin audit
beginAudit(best_sell.floID, best_buy.floID, asset, sell_rate, quantity).then(audit => {
//process txn query in SQL
DB.transaction(txQueries).then(_ => {
audit.end();
price.storeHistory(asset, sell_rate);
resolve(`Transaction was successful! BuyOrder:${best_buy.id}| SellOrder:${best_sell.id}`)
}).catch(error => reject(error));
}).catch(error => reject(error));
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function processOrders(seller_best, buyer_best, asset, sell_rate, quantity) {
let txQueries = [];
if (quantity > buyer_best.quantity || quantity > seller_best.quantity)
throw Error("Tx quantity cannot be more than order quantity");
//Process Buy Order
if (quantity == buyer_best.quantity)
txQueries.push(["DELETE FROM BuyOrder WHERE id=?", [buyer_best.id]]);
else
txQueries.push(["UPDATE BuyOrder SET quantity=quantity-? WHERE id=?", [quantity, buyer_best.id]]);
//Process Sell Order
if (quantity == seller_best.quantity)
txQueries.push(["DELETE FROM SellOrder WHERE id=?", [seller_best.id]]);
else
txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]);
//Update cash/asset balance for seller and buyer
let totalAmount = sell_rate * quantity;
txQueries.push(updateBalance.add(seller_best.floID, floGlobals.currency, totalAmount));
txQueries.push(updateBalance.consume(buyer_best.floID, floGlobals.currency, totalAmount));
txQueries.push(updateBalance.consume(seller_best.floID, asset, quantity));
txQueries.push(updateBalance.add(buyer_best.floID, asset, quantity));
//Record transaction
let time = Date.now();
let hash = TRADE_HASH_PREFIX + Crypto.SHA256(JSON.stringify({
seller: seller_best.floID,
buyer: buyer_best.floID,
asset: asset,
quantity: quantity,
unitValue: sell_rate,
tx_time: time,
}));
txQueries.push([
"INSERT INTO TradeTransactions (seller, buyer, asset, quantity, unitValue, tx_time, txid) VALUES (?)",
[[seller_best.floID, buyer_best.floID, asset, quantity, sell_rate, new Date(time), hash]]
]);
return txQueries;
}
function beginAudit(sellerID, buyerID, asset, unit_price, quantity) {
return new Promise((resolve, reject) => {
auditBalance(sellerID, buyerID, asset).then(old_bal => resolve({
end: () => endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity)
})).catch(error => reject(error))
})
}
function endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity) {
auditBalance(sellerID, buyerID, asset).then(new_bal => {
DB.query("INSERT INTO AuditTrade (asset, quantity, unit_price, total_cost," +
" sellerID, seller_old_cash, seller_old_asset, seller_new_cash, seller_new_asset," +
" buyerID, buyer_old_cash, buyer_old_asset, buyer_new_cash, buyer_new_asset)" +
" Value (?)", [[
asset, quantity, unit_price, quantity * unit_price,
sellerID, old_bal[sellerID].cash, old_bal[sellerID].asset, new_bal[sellerID].cash, new_bal[sellerID].asset,
buyerID, old_bal[buyerID].cash, old_bal[buyerID].asset, new_bal[buyerID].cash, new_bal[buyerID].asset,
]]).then(_ => null).catch(error => console.error(error))
}).catch(error => console.error(error));
}
function auditBalance(sellerID, buyerID, asset) {
return new Promise((resolve, reject) => {
let balance = {
[sellerID]: {
cash: 0,
asset: 0
},
[buyerID]: {
cash: 0,
asset: 0
}
};
DB.query("SELECT floID, quantity, asset FROM UserBalance WHERE floID IN (?) AND asset IN (?)", [[sellerID, buyerID], [floGlobals.currency, asset]]).then(result => {
for (let i in result) {
if (result[i].asset === floGlobals.currency)
balance[result[i].floID].cash = result[i].quantity;
else if (result[i].asset === asset)
balance[result[i].floID].asset = result[i].quantity;
}
resolve(balance);
}).catch(error => reject(error))
})
}
module.exports = {
initiate: startCouplingForAsset,
stopAll: stopAllInstance,
updateBalance
}

104
src/database.js Normal file
View File

@ -0,0 +1,104 @@
'use strict';
var mysql = require('mysql');
var pool;//container for connected pool;
function connectToDatabase(user, password, dbname, host = 'localhost') {
return new Promise((resolve, reject) => {
pool = mysql.createPool({
host: host,
user: user,
password: password,
database: dbname,
//dateStrings : true,
//timezone: 'UTC'
});
getConnection().then(conn => {
conn.release();
resolve(pool);
}).catch(error => reject(error));
});
}
function getConnection() {
return new Promise((resolve, reject) => {
if (!pool)
return reject("Database not connected");
pool.getConnection((error, conn) => {
if (error)
reject(error);
else
resolve(conn);
});
})
}
function SQL_query(sql, values) {
return new Promise((resolve, reject) => {
getConnection().then(conn => {
const fn = (err, res) => {
conn.release();
(err ? reject(err) : resolve(res));
};
if (values)
conn.query(sql, values, fn);
else
conn.query(sql, fn);
}).catch(error => reject(error));
})
}
function SQL_transaction(queries) {
return new Promise((resolve, reject) => {
getConnection().then(conn => {
conn.beginTransaction(err => {
if (err)
conn.rollback(() => {
conn.release();
reject(err);
});
else {
(function queryFn(result) {
if (!queries.length) {
conn.commit(err => {
if (err)
conn.rollback(() => {
conn.release();
reject(err);
});
else {
conn.release();
resolve(result);
}
});
} else {
let q_i = queries.shift();
const callback = function (err, res) {
if (err)
conn.rollback(() => {
conn.release();
reject(err);
});
else {
result.push(res);
queryFn(result);
}
};
if (!Array.isArray(q_i))
q_i = [q_i];
if (q_i[1])
conn.query(q_i[0], q_i[1], callback);
else
conn.query(q_i[0], callback);
}
})([]);
}
});
}).catch(error => reject(error));
})
}
module.exports = {
connect: connectToDatabase,
query: SQL_query,
transaction: SQL_transaction
};

465
src/keys.js Normal file
View File

@ -0,0 +1,465 @@
'use strict';
const fs = require('fs');
const path = require('path');
const lockfile = require('proper-lockfile');
const DB = require("./database");
var _I = ""; //Instance support
for (let arg of process.argv)
if (/^-I=/.test(arg)) {
_I = arg.split(/=(.*)/s)[1];
break;
}
const {
SHARES_PER_NODE,
SHARE_THRESHOLD,
SHUFFLE_INTERVAL
} = require("./_constants")["keys"];
const PRIV_EKEY_MIN = 32,
PRIV_EKEY_MAX = 48,
PRIME_FILE_TYPE = 'binary',
INDEX_FILE_TYPE = 'utf-8',
UNSIGNED_INT_MIN = 0,
UNSIGNED_INT_MAX = 4294967295,
INDEX_FILE_NAME_LENGTH = 16,
INDEX_FILE_EXT = '.txt',
MIN_DUMMY_FILES = 16,
MAX_DUMMY_FILES = 24,
MIN_DUMMY_SIZE_MUL = 0.5,
MAX_DUMMY_SIZE_MUL = 1.5,
SIZE_FACTOR = 100,
LOCK_RETRY_MIN_TIME = 1 * 1000,
LOCK_RETRY_MAX_TIME = 2 * 1000;
var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
const _x = {
get node_priv() {
if (!node_priv || !e_key)
throw Error("keys not set");
return Crypto.AES.decrypt(node_priv, e_key);
},
set node_priv(key) {
node_pub = floCrypto.getPubKeyHex(key);
node_id = floCrypto.getFloID(node_pub);
if (!key || !node_pub || !node_id)
throw Error("Invalid Keys");
let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
e_key = floCrypto.randString(n);
node_priv = Crypto.AES.encrypt(key, e_key);
},
args_dir: path.resolve(__dirname, '..', 'args'),
get index_dir() {
return path.join(this.args_dir, `indexes${_I}`)
},
get prime_file() {
return path.join(this.args_dir, `prime_index${_I}.b`)
},
get index_file() {
try {
let data = fs.readFileSync(this.prime_file, PRIME_FILE_TYPE),
fname = Crypto.AES.decrypt(data, this.node_priv);
return path.join(this.index_dir, fname + INDEX_FILE_EXT);
} catch (error) {
console.debug(error);
throw Error("Prime-Index Missing/Corrupted");
}
}
}
function initialize() {
return new Promise((resolve, reject) => {
fs.readFile(_x.prime_file, PRIME_FILE_TYPE, (err, res) => {
var data, cur_filename, new_filename, priv_key;
try {
priv_key = _x.node_priv;
} catch (error) {
return reject(error);
}
if (!err) {
if (res.length) { //prime file not empty
try {
cur_filename = Crypto.AES.decrypt(res, priv_key);
} catch (error) {
console.debug(error);
return reject("Prime file corrupted");
} try { //read data from index file
let tmp = fs.readFileSync(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), INDEX_FILE_TYPE);
tmp = Crypto.AES.decrypt(tmp, priv_key);
JSON.parse(tmp); //check if data is JSON parse-able
data = tmp;
} catch (error) {
console.debug(error);
return reject("Index file corrupted");
}
}
}
try {
if (!fs.existsSync(_x.index_dir)) {
fs.mkdirSync(_x.index_dir);
}
} catch (error) {
console.debug(error);
return reject("Index directory creation failed");
}
try { //delete all old dummy files
let files = fs.readdirSync(_x.index_dir);
for (const file of files)
if (!cur_filename || file !== cur_filename + INDEX_FILE_EXT) //check if file is current file
fs.unlinkSync(path.join(_x.index_dir, file));
} catch (error) {
console.debug(error);
return reject("Clear index directory failed");
} try { //create files (dummy and new index file)
let N = floCrypto.randInt(MIN_DUMMY_FILES, MAX_DUMMY_FILES),
k = floCrypto.randInt(0, N);
if (typeof data === 'undefined' || data.length == 0) //no existing data, initialize
data = JSON.stringify({});
let data_size = data.length;
for (let i = 0; i <= N; i++) {
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
if (i == k) {
new_filename = f_name;
f_data = data;
} else {
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
f_data = floCrypto.randString(d_size, false);
}
f_data = Crypto.AES.encrypt(f_data, priv_key);
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
}
} catch (error) {
console.debug(error);
return reject("Index file creation failed");
} try { //update prime file
let en_filename = Crypto.AES.encrypt(new_filename, priv_key);
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
} catch (error) {
console.debug(error);
return reject("Update prime file failed");
}
if (cur_filename)
fs.unlink(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), err => err ? console.debug(err) : null);
shuffle.interval = setInterval(shuffle, SHUFFLE_INTERVAL);
resolve("Key management initiated");
})
})
}
function shuffle() {
readIndexFile().then(data => {
let new_filename, cur_filename = Crypto.AES.decrypt(fs.readFileSync(_x.prime_file, PRIME_FILE_TYPE), _x.node_priv);
fs.readdir(_x.index_dir, (err, files) => {
if (err)
return console.error(err);
data = JSON.stringify(data);
let data_size = data.length;
for (let file of files) {
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
if (file === cur_filename + INDEX_FILE_EXT) {
new_filename = f_name;
f_data = data;
} else {
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
f_data = floCrypto.randString(d_size, false);
}
f_data = Crypto.AES.encrypt(f_data, _x.node_priv);
//rename and rewrite the file
try {
fs.renameSync(path.join(_x.index_dir, file), path.join(_x.index_dir, f_name + INDEX_FILE_EXT));
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
} catch (error) {
console.error(error)
}
}
//update prime file
if (!new_filename)
return console.error("Index file has not been renamed");
let en_filename = Crypto.AES.encrypt(new_filename, _x.node_priv);
try {
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
} catch (error) {
console.error(error);
}
})
}).catch(error => console.error(error))
}
function readIndexFile() {
return new Promise((resolve, reject) => {
fs.readFile(_x.index_file, INDEX_FILE_TYPE, (err, data) => {
if (err) {
console.debug(err);
return reject('Unable to read Index file');
}
try {
data = JSON.parse(Crypto.AES.decrypt(data, _x.node_priv));
resolve(data);
} catch {
reject("Index file corrupted");
}
})
})
}
function writeIndexFile(data) {
return new Promise((resolve, reject) => {
let en_data = Crypto.AES.encrypt(JSON.stringify(data), _x.node_priv);
fs.writeFile(_x.index_file, en_data, INDEX_FILE_TYPE, (err) => {
if (err) {
console.debug(err);
return reject('Unable to write Index file');
} else resolve("Updated Index file");
})
})
}
function getShares(group, id, ignoreDiscarded = true) {
return new Promise((resolve, reject) => {
checkIfDiscarded(id).then(result => {
if (ignoreDiscarded && result != false)
return reject("Trying to get share for discarded ID");
readIndexFile().then(data => {
if (!(group in data))
reject("Group not found in Index file");
else if (!(id in data[group]))
reject("ID not found in Index file");
else {
let ref = data[group][id].shift();
DB.query("SELECT share FROM sinkShares WHERE num IN (?)", [data[group][id]])
.then(result => resolve({ ref, shares: result.map(r => Crypto.AES.decrypt(r.share, _x.node_priv)) }))
.catch(error => reject(error))
}
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function storeShareAtRandom(share) {
return new Promise((resolve, reject) => {
let rand = floCrypto.randInt(UNSIGNED_INT_MIN, UNSIGNED_INT_MAX);
DB.query("INSERT INTO sinkShares(num, share) VALUE (?)", [[rand, share]])
.then(result => resolve(result.insertId)).catch(error => {
if (error.code === "ER_DUP_ENTRY")
storeShareAtRandom(share) //try again (with diff rand_num)
.then(result => resolve(result))
.catch(error => reject(error))
else
reject(error);
})
})
}
function addShare(group, id, ref, share) {
return new Promise((resolve, reject) => {
checkIfDiscarded(id).then(result => {
if (result != false)
return reject("Trying to store share for discarded ID");
lockfile.lock(_x.index_file, { retries: { forever: true, minTimeout: LOCK_RETRY_MIN_TIME, maxTimeout: LOCK_RETRY_MAX_TIME } }).then(release => {
const releaseAndReject = err => {
release().then(_ => null).catch(error => console.error(error));
reject(err);
}
readIndexFile().then(data => {
if (!(group in data))
data[group] = {};
if (!(id in data[group]))
data[group][id] = [ref];
else if (ref < data[group][id][0])
return reject("reference is lower than current");
else if (ref > data[group][id][0]) {
let old_shares = data[group][id];
data[group][id] = [ref];
old_shares.shift();
DB.query("DELETE FROM sinkShares WHERE num in (?)", [old_shares])//delete old shares
.then(_ => null).catch(error => console.error(error));
}
let encrypted_share = Crypto.AES.encrypt(share, _x.node_priv);
console.debug(ref, '|sinkID:', id, '|EnShare:', encrypted_share);
storeShareAtRandom(encrypted_share).then(i => {
data[group][id].push(i);
writeIndexFile(data).then(_ => resolve(i)).catch(error => reject(error))
.finally(_ => release().then(_ => null).catch(error => console.error(error)));
}).catch(error => releaseAndReject(error))
}).catch(error => releaseAndReject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function generateShares(sinkKey, total_n, min_n) {
let shares = floCrypto.createShamirsSecretShares(sinkKey, total_n * SHARES_PER_NODE, min_n * SHARES_PER_NODE * SHARE_THRESHOLD);
let node_shares = Array(total_n);
for (let i = 0; i < total_n; i++)
node_shares[i] = shares.splice(0, SHARES_PER_NODE);
return node_shares;
}
function getStoredList(group = null) {
return new Promise((resolve, reject) => {
readIndexFile().then(data => {
if (group !== null) {
if (group in data)
resolve(Object.keys(data.group));
else
reject("Group not found in Index file");
} else {
let ids = {};
for (let group in data)
ids[group] = Object.keys(data[group]);
resolve(ids);
}
}).catch(error => reject(error))
})
}
function getDiscardedList() {
return new Promise((resolve, reject) => {
DB.query("SELECT floID, discard_time FROM discardedSinks")
.then(result => resolve(Object.fromEntries(result.map(r => [r.floID, r.discard_time]))))
.catch(error => reject(error))
})
}
function checkIfDiscarded(id) {
return new Promise((resolve, reject) => {
DB.query("SELECT discard_time FROM discardedSinks WHERE floID=?", [id])
.then(result => resolve(result.length ? result[0].discard_time : false))
.catch(error => reject(error))
})
}
function discardSink(id) {
return new Promise((resolve, reject) => {
DB.query("INSERT INTO discardedSinks(floID) VALUE (?)", [id])
.then(result => resolve(`Discarded ${id}`))
.catch(error => reject(error))
})
}
//Sink groups and chest
const sink_groups = {
get TRADE() { return "TRADE" },
get list() { //total list
return [this.TRADE]
},
get initial_list() { //list to generate when starting server
return [this.TRADE]
},
get generate_list() { //list allowed to generate
return [this.TRADE]
}
};
const sink_ids = {}, sink_chest = {
reset() {
for (let i in sink_ids)
delete sink_ids[i];
},
set_id(group, id, value) {
if (!(group in sink_ids))
sink_ids[group] = {};
sink_ids[group][id] = value;
},
rm_id(group, id) {
return delete sink_ids[group][id];
},
get_id(group, id) {
return sink_ids[group][id];
},
list(group) {
return Object.keys(sink_ids[group] || {});
},
active_list(group) {
let ids = [];
if (group in sink_ids)
for (let id in sink_ids[group])
if (sink_ids[group][id])
ids.push(id);
return ids;
},
includes(group, id) {
return group in sink_ids ? (id in sink_ids[group]) : null;
},
isActive(group, id) {
return group in sink_ids ? (id in sink_ids && sink_ids[id]) : null;
},
pick(group) {
let ids = this.list(group),
i = floCrypto.randInt(0, ids.length - 1);
return ids[i];
},
active_pick(group) {
let ids = this.active_list(group),
i = floCrypto.randInt(0, ids.length - 1);
return ids[i];
},
find_group(id) {
let group = null;
for (let g in sink_ids)
if (id in sink_ids[g]) {
group = g; break;
}
return group;
},
get_all() {
let ids = {};
for (let g in sink_ids)
ids[g] = Object.keys(sink_ids[g])
return ids;
}
};
//Asset list
var assetList, currency, tradeList; //container for allowed assets, currency and trade asset
const assets = {
set_list(assets) {
if (Array.isArray(assets))
assetList = assets;
},
get list() { return Array.from(assetList) },
includes(asset) { return assetList.includes(asset) },
set_currency(c) { currency = c },
get currency() { return currency },
isCurrency(asset) { return asset === currency },
set_tradeable(assets) {
if (Array.isArray(assets))
tradeList = assets;
},
get tradeList() { return Array.from(tradeList) },
isTradeable(asset) { return tradeList.includes(asset) }
}
module.exports = {
init: initialize,
getShares,
addShare,
generateShares,
getStoredList,
getDiscardedList,
checkIfDiscarded,
discardSink,
set node_priv(key) {
_x.node_priv = key;
},
get node_priv() {
return _x.node_priv;
},
get node_id() {
return node_id;
},
get node_pub() {
return node_pub;
},
get sink_groups() {
return sink_groups;
},
get sink_chest() {
return sink_chest;
},
get assets() {
return assets;
}
}

179
src/main.js Normal file
View File

@ -0,0 +1,179 @@
'use strict';
global.floGlobals = require('../docs/scripts/floGlobals');
require('./set_globals');
require('../docs/scripts/lib');
global.floCrypto = require('../docs/scripts/floCrypto');
global.floBlockchainAPI = require('../docs/scripts/floBlockchainAPI');
global.floTokenAPI = require('../docs/scripts/floTokenAPI');
global.btcOperator = require('../docs/scripts/btcOperator');
const keys = require('./keys');
const DB = require("./database");
const App = require('./app');
const backup = require('./backup/head');
const {
BLOCKCHAIN_REFRESH_INTERVAL
} = require("./_constants")["app"];
(function () {
const { adminID, application, currency } = require("../docs/scripts/floTradeAPI");
floGlobals.adminID = adminID;
floGlobals.application = application;
keys.assets.set_currency(currency);
})();
var app;
function refreshData(startup = false) {
return new Promise((resolve, reject) => {
refreshDataFromBlockchain().then(changes => {
loadDataFromDB(changes, startup).then(_ => {
if (!startup && changes.nodes)
backup.reconstructAllActiveShares();
//app.refreshData(backup.nodeList);
resolve("Data refresh successful")
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function refreshDataFromBlockchain() {
return new Promise((resolve, reject) => {
DB.query("SELECT txid FROM LastTx WHERE floID=?", [floGlobals.adminID]).then(result => {
var query_options = { sentOnly: true, pattern: floGlobals.application };
let lastTx = result.length ? result[0].txid : undefined;
if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
query_options.after = lastTx;
else if (!isNaN(lastTx))//lastTx is tx count (*backward support)
query_options.ignoreOld = parseInt(lastTx);
floBlockchainAPI.readData(floGlobals.adminID, query_options).then(result => {
let promises = [],
nodes_change = false,
assets_change = false;
result.data.reverse().forEach(data => {
var content = JSON.parse(data)[floGlobals.application];
//Node List
if (content.Nodes) {
nodes_change = true;
if (content.Nodes.remove)
for (let n of content.Nodes.remove)
promises.push(DB.query("DELETE FROM NodeList WHERE floID=?", [n]));
if (content.Nodes.add)
for (let n in content.Nodes.add)
promises.push(DB.query("INSERT INTO NodeList (floID, uri) VALUE (?) ON DUPLICATE KEY UPDATE uri=?", [[n, content.Nodes.add[n]], content.Nodes.add[n]]));
if (content.Nodes.update)
for (let n in content.Nodes.update)
promises.push(DB.query("UPDATE NodeList SET uri=? WHERE floID=?", [content.Nodes.update[n], n]));
}
//Asset List
if (content.Assets) {
assets_change = true;
for (let a in content.Assets)
promises.push(DB.query("INSERT INTO AssetList (asset, isTradeable) VALUE (?) ON DUPLICATE KEY UPDATE isTradeable=?", [[a, content.Assets[a]], content.Assets[a]]));
}
});
promises.push(DB.query("INSERT INTO LastTx (floID, txid) VALUE (?) ON DUPLICATE KEY UPDATE txid=?", [[floGlobals.adminID, result.lastItem], result.lastItem]));
//Check if all save process were successful
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 blockchain data might not have been saved in database correctly");
resolve({
nodes: nodes_change,
assets: assets_change
});
});
}).catch(error => reject(error));
}).catch(error => reject(error))
})
}
function loadDataFromDB(changes, startup) {
return new Promise((resolve, reject) => {
let promises = [];
if (startup || changes.nodes)
promises.push(loadDataFromDB.nodeList());
if (startup || changes.assets)
promises.push(loadDataFromDB.assetList());
Promise.all(promises)
.then(_ => resolve("Data load successful"))
.catch(error => reject(error))
})
}
loadDataFromDB.nodeList = function () {
return new Promise((resolve, reject) => {
DB.query("SELECT floID, uri FROM NodeList").then(result => {
let nodes = {}
for (let i in result)
nodes[result[i].floID] = result[i].uri;
//update dependents
backup.nodeList = nodes;
resolve(nodes);
}).catch(error => reject(error))
})
}
loadDataFromDB.assetList = function () {
return new Promise((resolve, reject) => {
DB.query("SELECT asset, isTradeable FROM AssetList").then(result => {
let assets = [], tradeAssets = [];
for (let i in result) {
assets.push(result[i].asset);
if (result[i].isTradeable)
tradeAssets.push(result[i].asset);
}
//update dependents
keys.assets.set_list(assets);
keys.assets.set_tradeable(tradeAssets);
resolve(assets);
}).catch(error => reject(error))
})
}
module.exports = function startServer() {
let _pass, _I = "";
for (let arg of process.argv) {
if (/^-I=/.test(arg))
_I = arg.split(/=(.*)/s)[1];
else if (/^-password=/i.test(arg))
_pass = arg.split(/=(.*)/s)[1];
}
const config = require(`../args/config${_I}.json`);
try {
let _tmp = require(`../args/keys${_I}.json`);
_tmp = floCrypto.retrieveShamirSecret(_tmp);
if (!_pass) {
console.error('Password not entered!');
process.exit(1);
}
keys.node_priv = Crypto.AES.decrypt(_tmp, _pass);
} catch (error) {
console.error('Unable to load private key!');
process.exit(1);
}
console.log("Logged in as", keys.node_id);
DB.connect(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(result => {
keys.init().then(result => {
console.log(result);
app = new App(config['secret']);
refreshData(true).then(_ => {
app.start(config['port']).then(result => {
console.log(result);
backup.init(app);
setInterval(() => {
refreshData()
.then(result => console.log(result))
.catch(error => console.error(error))
}, BLOCKCHAIN_REFRESH_INTERVAL);
}).catch(error => console.error(error))
}).catch(error => console.error(error))
}).catch(error => console.error(error))
}).catch(error => console.error(error));
};

458
src/market.js Normal file
View File

@ -0,0 +1,458 @@
'use strict';
const coupling = require('./coupling');
const price = require("./price");
const background = require('./background');
const DB = require("./database");
const blockchain = require('./blockchain');
const keys = require('./keys');
const {
TRADE_HASH_PREFIX,
TRANSFER_HASH_PREFIX
} = require('./_constants')["market"];
const eCode = require('../docs/scripts/floTradeAPI').errorCode;
const pCode = require('../docs/scripts/floTradeAPI').processCode;
const updateBalance = background.updateBalance = coupling.updateBalance;
function login(floID, proxyKey) {
return new Promise((resolve, reject) => {
DB.query("INSERT INTO UserSession (floID, proxyKey) VALUE (?) ON DUPLICATE KEY UPDATE session_time=DEFAULT, proxyKey=?", [[floID, proxyKey], proxyKey])
.then(result => resolve("Login Successful"))
.catch(error => reject(error))
})
}
function logout(floID) {
return new Promise((resolve, reject) => {
DB.query("DELETE FROM UserSession WHERE floID=?", [floID])
.then(result => resolve("Logout successful"))
.catch(error => reject(error))
})
}
function getRateHistory(asset, duration) {
return new Promise((resolve, reject) => {
if (!asset || !keys.assets.isTradeable(asset))
reject(INVALID(eCode.INVALID_ASSET_NAME, `Invalid asset(${asset})`));
else
price.getHistory(asset, duration)
.then(result => resolve(result))
.catch(error => reject(error))
})
}
function getBalance(floID, asset) {
return new Promise((resolve, reject) => {
if (floID && !floCrypto.validateAddr(floID))
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID(${floID})`));
else if (asset && !keys.assets.includes(asset))
reject(INVALID(eCode.INVALID_ASSET_NAME, `Invalid asset(${asset})`));
else if (!floID && !asset)
reject(INVALID(eCode.MISSING_PARAMETER, 'Missing parameters: requires atleast one (floID, asset)'));
else {
var promise;
if (floID && asset)
promise = getBalance.floID_asset(floID, asset);
else if (floID)
promise = getBalance.floID(floID);
else if (asset)
promise = getBalance.asset(asset);
promise.then(result => resolve(result)).catch(error => reject(error))
}
})
}
getBalance.floID_asset = (floID, asset) => new Promise((resolve, reject) => {
DB.query("SELECT quantity AS balance FROM UserBalance WHERE floID=? AND asset=?", [floID, asset]).then(result => resolve({
floID,
asset,
balance: result.length ? global.toStandardDecimal(result[0].balance) : 0
})).catch(error => reject(error))
});
getBalance.floID = (floID) => new Promise((resolve, reject) => {
DB.query("SELECT asset, quantity AS balance FROM UserBalance WHERE floID=?", [floID]).then(result => {
let response = {
floID,
balance: {}
};
for (let row of result)
response.balance[row.asset] = global.toStandardDecimal(row.balance);
resolve(response);
}).catch(error => reject(error))
});
getBalance.asset = (asset) => new Promise((resolve, reject) => {
DB.query("SELECT floID, quantity AS balance FROM UserBalance WHERE asset=?", [asset]).then(result => {
let response = {
asset: asset,
balance: {}
};
for (let row of result)
response.balance[row.floID] = global.toStandardDecimal(row.balance);
resolve(response);
}).catch(error => reject(error))
});
const getAssetBalance = (floID, asset) => new Promise((resolve, reject) => {
let promises = [];
promises.push(DB.query("SELECT IFNULL(SUM(quantity), 0) AS balance FROM UserBalance WHERE floID=? AND asset=?", [floID, asset]));
promises.push(asset === floGlobals.currency ?
DB.query("SELECT IFNULL(SUM(quantity*maxPrice), 0) AS locked FROM BuyOrder WHERE floID=?", [floID]) :
DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=?", [floID, asset])
);
Promise.all(promises).then(result => resolve({
total: result[0][0].balance,
locked: result[1][0].locked,
net: result[0][0].balance - result[1][0].locked
})).catch(error => reject(error))
});
getAssetBalance.check = (floID, asset, amount) => new Promise((resolve, reject) => {
getAssetBalance(floID, asset).then(balance => {
if (balance.total < amount)
reject(INVALID(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset}`));
else if (balance.net < amount)
reject(INVALID(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset} (Some are locked in orders)`));
else
resolve(true);
}).catch(error => reject(error))
});
function addSellOrder(floID, asset, quantity, min_price) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
else if (typeof quantity !== "number" || quantity <= 0)
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
else if (typeof min_price !== "number" || min_price <= 0)
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid min_price (${min_price})`));
else if (!keys.assets.isTradeable(asset))
return reject(INVALID(eCode.INVALID_ASSET_NAME, `Invalid asset (${asset})`));
getAssetBalance.check(floID, asset, quantity).then(_ => {
DB.query("INSERT INTO SellOrder(floID, asset, quantity, price) VALUES (?)", [[floID, asset, quantity, min_price]]).then(result => {
resolve('Sell Order placed successfully');
console.debug("sell order placed");
coupling.initiate(asset);
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
function addBuyOrder(floID, asset, quantity, max_price) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
else if (typeof quantity !== "number" || quantity <= 0)
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
else if (typeof max_price !== "number" || max_price <= 0)
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid max_price (${max_price})`));
else if (!keys.assets.isTradeable(asset))
return reject(INVALID(eCode.INVALID_ASSET_NAME, `Invalid asset (${asset})`));
getAssetBalance.check(floID, floGlobals.currency, quantity * max_price).then(_ => {
DB.query("INSERT INTO BuyOrder(floID, asset, quantity, maxPrice) VALUES (?)", [[floID, asset, quantity, max_price]]).then(result => {
console.debug("before resolve")
resolve('Buy Order placed successfully');
console.debug("buy order placed");
coupling.initiate(asset);
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
function cancelOrder(type, id, floID) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
let tableName;
if (type === "buy")
tableName = "BuyOrder";
else if (type === "sell")
tableName = "SellOrder";
else
return reject(INVALID(eCode.INVALID_TYPE, "Invalid Order type! Order type must be buy (or) sell"));
DB.query("SELECT floID, asset FROM ?? WHERE id=?", [tableName, id]).then(result => {
if (result.length < 1)
return reject(INVALID(eCode.NOT_FOUND, "Order not found!"));
else if (result[0].floID !== floID)
return reject(INVALID(eCode.NOT_OWNER, "Order doesnt belong to the current user"));
let asset = result[0].asset;
//Delete the order
DB.query("DELETE FROM ?? WHERE id=?", [tableName, id]).then(result => {
resolve(tableName + "#" + id + " cancelled successfully");
coupling.initiate(asset);
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
function getAccountDetails(floID) {
return new Promise((resolve, reject) => {
let promises = [
DB.query("SELECT asset, quantity FROM UserBalance WHERE floID=?", [floID]),
DB.query("SELECT id, asset, quantity, price, time_placed FROM SellOrder WHERE floID=?", [floID]),
DB.query("SELECT id, asset, quantity, maxPrice, time_placed FROM BuyOrder WHERE floID=?", [floID])
];
Promise.allSettled(promises).then(results => {
let response = {
floID: floID,
time: Date.now()
};
results.forEach((a, i) => {
if (a.status === "rejected")
console.error(a.reason);
else
switch (i) {
case 0:
response.tokenBalance = a.value;
break;
case 1:
response.sellOrders = a.value;
break;
case 2:
response.buyOrders = a.value;
break;
}
});
DB.query("SELECT * FROM TradeTransactions WHERE seller=? OR buyer=?", [floID, floID])
.then(result => response.transactions = result)
.catch(error => console.error(error))
.finally(_ => resolve(response));
});
});
}
function getUserTransacts(floID) {
return new Promise((resolve, reject) => {
DB.query("SELECT mode, asset, amount, txid, locktime, r_status FROM VaultTransactions WHERE floID=?", [floID])
.then(result => resolve(result))
.catch(error => reject(error))
})
}
/*
function getTransactionDetails(txid) {
return new Promise((resolve, reject) => {
let tableName, type;
if (txid.startsWith(TRANSFER_HASH_PREFIX)) {
tableName = 'TransferTransactions';
type = 'transfer';
} else if (txid.startsWith(TRADE_HASH_PREFIX)) {
tableName = 'TradeTransactions';
type = 'trade';
} else
return reject(INVALID(eCode.INVALID_TX_ID, "Invalid TransactionID"));
DB.query("SELECT * FROM ?? WHERE txid=?", [tableName, txid]).then(result => {
if (result.length) {
let details = result[0];
details.type = type;
if (tableName === 'TransferTransactions') //As json object is stored for receiver in transfer (to support one-to-many)
details.receiver = JSON.parse(details.receiver);
resolve(details);
} else
reject(INVALID(eCode.NOT_FOUND, "Transaction not found"));
}).catch(error => reject(error))
})
}
*/
/*
function transferAsset(sender, receivers, asset) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(sender))
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid sender (${sender})`));
else if (!keys.assets.includes(asset))
reject(INVALID(eCode.INVALID_ASSET_NAME, `Invalid asset (${asset})`));
else {
let invalidIDs = [],
totalAmount = 0;
for (let floID in receivers)
if (!floCrypto.validateAddr(floID))
invalidIDs.push(floID);
else
totalAmount += receivers[floID];
if (invalidIDs.length)
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid receiver (${invalidIDs})`));
else getAssetBalance.check(sender, asset, totalAmount).then(_ => {
let txQueries = [];
txQueries.push(updateBalance.consume(sender, asset, totalAmount));
for (let floID in receivers)
txQueries.push(updateBalance.add(floID, asset, receivers[floID]));
let time = Date.now();
let hash = TRANSFER_HASH_PREFIX + Crypto.SHA256(JSON.stringify({
sender: sender,
receiver: receivers,
asset: asset,
totalAmount: totalAmount,
tx_time: time,
}));
txQueries.push([
"INSERT INTO TransferTransactions (sender, receiver, asset, totalAmount, tx_time, txid) VALUE (?)",
[[sender, JSON.stringify(receivers), asset, totalAmount, new Date(time), hash]]
]);
DB.transaction(txQueries)
.then(result => resolve(hash))
.catch(error => reject(error))
}).catch(error => reject(error))
}
})
}
*/
function depositAsset(floID, asset, txid) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
else if (!keys.assets.includes(asset))
return reject(INVALID(eCode.INVALID_ASSET_NAME, "Invalid Asset"));
var f;
switch (asset) {
case "FLO": f = depositFLO(floID, txid); break;
case "BTC": f = depositBTC(floID, txid); break;
default: f = depositToken(floID, txid); break;
}
f.then(result => resolve(result)).catch(error => reject(error));
})
}
function withdrawAsset(floID, asset, amount) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(floID))
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
else if (!keys.assets.includes(asset))
return reject(INVALID(eCode.INVALID_ASSET_NAME, "Invalid Asset"));
else if (typeof amount !== "number" || amount <= 0)
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
var f;
switch (asset) {
case "FLO": f = withdrawFLO(floID, txid); break;
case "BTC": f = withdrawBTC(floID, txid); break;
default: f = withdrawToken(floID, asset, txid); break;
}
f.then(result => resolve(result)).catch(error => reject(error));
})
}
function depositFLO(floID, txid) {
return new Promise((resolve, reject) => {
DB.query("SELECT r_status FROM VaultTransactions WHERE txid=? AND floID=? AND asset=?", [txid, floID, "FLO"]).then(result => {
if (result.length) {
switch (result[0].r_status) {
case pCode.STATUS_PENDING:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
case pCode.STATUS_REJECTED:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected"));
case pCode.STATUS_SUCCESS:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add coins"));
}
} else
DB.query("INSERT INTO VaultTransactions(floID, mode, asset_type, asset, txid, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_COIN, "FLO", txid, pCode.STATUS_PENDING]])
.then(result => resolve("Deposit request in process"))
.catch(error => reject(error));
}).catch(error => reject(error))
});
}
function withdrawFLO(floID, amount) {
return new Promise((resolve, reject) => {
getAssetBalance.check(floID, "FLO", amount).then(_ => {
let txQueries = [];
txQueries.push(updateBalance.consume(floID, "FLO", amount));
DB.transaction(txQueries).then(result => {
blockchain.withdrawAsset.init(floID, "FLO", amount);
resolve("Withdrawal request is in process");
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
function depositBTC(floID, txid) {
return new Promise((resolve, reject) => {
DB.query("SELECT r_status FROM VaultTransactions WHERE txid=? AND floID=? AND asset=?", [txid, floID, "BTC"]).then(result => {
if (result.length) {
switch (result[0].r_status) {
case pCode.STATUS_PENDING:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
case pCode.STATUS_REJECTED:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected"));
case pCode.STATUS_SUCCESS:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add coins"));
}
} else
DB.query("INSERT INTO VaultTransactions(floID, mode, asset_type, asset, txid, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_COIN, "BTC", txid, pCode.STATUS_PENDING]])
.then(result => resolve("Deposit request in process"))
.catch(error => reject(error));
}).catch(error => reject(error))
});
}
function withdrawBTC(floID, amount) {
return new Promise((resolve, reject) => {
getAssetBalance.check(floID, "BTC", amount).then(_ => {
let txQueries = [];
txQueries.push(updateBalance.consume(floID, "BTC", amount));
DB.transaction(txQueries).then(result => {
blockchain.withdrawAsset.init(floID, "BTC", amount);
resolve("Withdrawal request is in process");
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
function depositToken(floID, txid) {
return new Promise((resolve, reject) => {
DB.query("SELECT r_status FROM VaultTransactions WHERE txid=? AND floID=? AND asset_type=?", [txid, floID, pCode.ASSET_TYPE_TOKEN]).then(result => {
if (result.length) {
switch (result[0].r_status) {
case pCode.STATUS_PENDING:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
case pCode.STATUS_REJECTED:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected"));
case pCode.STATUS_SUCCESS:
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add tokens"));
}
} else
DB.query("INSERT INTO VaultTransactions(floID, mode, asset_type, txid, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_TOKEN, txid, pCode.STATUS_PENDING]])
.then(result => resolve("Deposit request in process"))
.catch(error => reject(error));
}).catch(error => reject(error))
});
}
function withdrawToken(floID, token, amount) {
return new Promise((resolve, reject) => {
//Check for FLO balance (transaction fee)
let required_flo = floGlobals.sendAmt + floGlobals.fee;
getAssetBalance.check(floID, "FLO", required_flo).then(_ => {
getAssetBalance.check(floID, token, amount).then(_ => {
let txQueries = [];
txQueries.push(updateBalance.consume(floID, "FLO", required_flo));
txQueries.push(updateBalance.consume(floID, token, amount));
DB.transaction(txQueries).then(result => {
//Send Token to user via token API
blockchain.withdrawAsset.init(floID, token, amount);
resolve("Withdrawal request is in process");
}).catch(error => reject(error));
}).catch(error => reject(error));
}).catch(error => reject(error));
});
}
module.exports = {
login,
logout,
addBuyOrder,
addSellOrder,
cancelOrder,
getRateHistory,
getBalance,
getAccountDetails,
getUserTransacts,
//getTransactionDetails,
//transferAsset,
depositAsset,
withdrawAsset
};

27
src/pairing.js Normal file
View File

@ -0,0 +1,27 @@
const getBestSeller = (asset) => new Promise((resolve, reject) => {
DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = SellOrder.floID AND UserBalance.asset = SellOrder.asset" +
" WHERE UserBalance.quantity >= SellOrder.quantity AND SellOrder.asset = ?" +
" ORDER BY SellOrder.price ASC, SellOrder.time_placed ASC" +
" LIMIT 1", [asset]
).then(result => {
if (result.length)
resolve(result[0]);
else
reject(null);
}).catch(error => reject(error))
});
const getBestBuyer = (asset) => new Promise((resolve, reject) => {
DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = BuyOrder.floID AND UserBalance.asset = ?" +
" WHERE UserBalance.quantity >= BuyOrder.maxPrice * BuyOrder.quantity AND BuyOrder.asset = ?" +
" ORDER BY BuyOrder.maxPrice DESC, BuyOrder.time_placed ASC" +
" LIMIT 1", [floGlobals.currency, asset]
).then(result => {
if (result.length)
resolve(result[0]);
else
reject(null);
}).catch(error => reject(error))
});

61
src/price.js Normal file
View File

@ -0,0 +1,61 @@
'use strict';
const DB = require("./database");
var currentRate = {}; //container for FLO price (from API or by model)
//store FLO price in database every 1 hr
function storeHistory(asset, rate) {
DB.query("INSERT INTO PriceHistory (asset, rate) VALUE (?)", [[asset, global.toStandardDecimal(rate)]])
.then(_ => null).catch(error => console.error(error))
}
function getHistory(asset, duration = '') {
return new Promise((resolve, reject) => {
let { statement, values } = getHistory.getRateStatement(asset, duration);
DB.query(statement, values)
.then(result => resolve(result))
.catch(error => reject(error))
});
}
getHistory.statement = {
'all-time': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? GROUP BY time ORDER BY time",
'year': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? year GROUP BY time ORDER BY time",
'month': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? month GROUP BY time ORDER BY time",
'week': "SELECT rec_time AS time, rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? week ORDER BY time",
'day': "SELECT rec_time AS time, rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? day ORDER BY time"
}
getHistory.getRateStatement = (asset, duration) => {
let n = duration.match(/\d+/g),
d = duration.match(/\D+/g);
n = n ? n[0] || 1 : 1;
d = d ? d[0].replace(/[-\s]/g, '') : "";
switch (d.toLowerCase()) {
case "day":
case "days":
return { statement: getHistory.statement['day'], values: [asset, n] };
case "week":
case "weeks":
return { statement: getHistory.statement['week'], values: [asset, n] };
case "month":
case "months":
return { statement: getHistory.statement['month'], values: [asset, n] };
case "year":
case "years":
return { statement: getHistory.statement['year'], values: [asset, n] };
case "alltime":
return { statement: getHistory.statement['all-time'], values: [asset] };
default:
return { statement: getHistory.statement['day'], values: [asset, 1] };
}
}
module.exports = {
getHistory,
storeHistory,
get currentRates() {
return Object.assign({}, currentRate);
}
}

483
src/request.js Normal file
View File

@ -0,0 +1,483 @@
'use strict';
const DB = require("./database");
const market = require("./market");
const background = require("./background");
const sink = require("./backup/head").sink;
const keys = require("./keys");
const {
SIGN_EXPIRE_TIME,
MAX_SESSION_TIMEOUT
} = require("./_constants")["request"];
const eCode = require('../docs/scripts/floTradeAPI').errorCode;
const serviceList = require('../docs/scripts/floTradeAPI').serviceList;
var secret; //containers for secret
global.INVALID = function (ecode, message) {
if (!(this instanceof INVALID))
return new INVALID(ecode, message);
this.message = message;
this.ecode = ecode;
}
INVALID.e_code = 400;
INVALID.prototype.toString = function () {
return "E" + this.ecode + ": " + this.message;
}
INVALID.str = (ecode, message) => INVALID(ecode, message).toString();
global.INTERNAL = function INTERNAL(message) {
if (!(this instanceof INTERNAL))
return new INTERNAL(message);
this.message = message;
}
INTERNAL.e_code = 500;
INTERNAL.prototype.toString = function () {
return "E" + eCode.INTERNAL_ERROR + ": " + this.message;
}
INTERNAL.str = (ecode, message) => INTERNAL(ecode, message).toString();
const INCORRECT_SERVER_ERROR = INVALID(eCode.INCORRECT_SERVER, "Incorrect server");
var serving;
function validateRequest(request, sign, floID, pubKey) {
return new Promise((resolve, reject) => {
if (!serving)
reject(INCORRECT_SERVER_ERROR);
else if (!request.timestamp)
reject(INVALID(eCode.MISSING_PARAMETER, "Timestamp parameter missing"));
else if (Date.now() - SIGN_EXPIRE_TIME > request.timestamp)
reject(INVALID(eCode.EXPIRED_SIGNATURE, "Signature Expired"));
else if (!floCrypto.validateAddr(floID))
reject(INVALID(eCode.INVALID_FLO_ID, "Invalid floID"));
else if (typeof request !== "object")
reject(INVALID(eCode.INVALID_REQUEST_FORMAT, "Request is not an object"));
else validateRequest.getSignKey(floID, pubKey).then(signKey => {
let req_str = Object.keys(request).sort().map(r => r + ":" + request[r]).join("|");
try {
if (!floCrypto.verifySign(req_str, sign, signKey))
reject(INVALID(eCode.INVALID_SIGNATURE, "Invalid request signature"));
else validateRequest.checkIfSignUsed(sign)
.then(result => resolve(req_str))
.catch(error => reject(error))
} catch {
reject(INVALID(eCode.INVALID_SIGNATURE, "Corrupted sign/key"));
}
}).catch(error => reject(error));
});
}
validateRequest.getSignKey = (floID, pubKey) => new Promise((resolve, reject) => {
if (!pubKey)
DB.query("SELECT session_time, proxyKey FROM UserSession WHERE floID=?", [floID]).then(result => {
if (result.length < 1)
reject(INVALID(eCode.SESSION_INVALID, "Session not active"));
else if (result[0].session_time + MAX_SESSION_TIMEOUT < Date.now())
reject(INVALID(eCode.SESSION_EXPIRED, "Session Expired! Re-login required"));
else
resolve(result[0].proxyKey);
}).catch(error => reject(error));
else if (floCrypto.getFloID(pubKey) === floID)
resolve(pubKey);
else
reject(INVALID(eCode.INVALID_PUBLIC_KEY, "Invalid pubKey"));
});
validateRequest.checkIfSignUsed = sign => new Promise((resolve, reject) => {
DB.query("SELECT id FROM RequestLog WHERE sign=?", [sign]).then(result => {
if (result.length)
reject(INVALID(eCode.DUPLICATE_SIGNATURE, "Duplicate signature"));
else
resolve(true);
}).catch(error => reject(error))
});
function logRequest(floID, req_str, sign, proxy = false) {
//console.debug(floID, req_str);
DB.query("INSERT INTO RequestLog (floID, request, sign, proxy) VALUES (?)", [[floID, req_str, sign, proxy]])
.then(_ => null).catch(error => console.error(error));
}
function processRequest(res, floID, pubKey, sign, rText, validateObj, marketFn, log = true) {
validateRequest(validateObj, sign, floID, pubKey).then(req_str => {
marketFn().then(result => {
if (log) logRequest(floID, req_str, sign, !pubKey);
res.send(result);
}).catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str(rText + " failed! Try again later!"));
}
})
}).catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Request processing failed! Try again later!"));
}
})
}
/* User Requests */
function Account(req, res) {
let data = req.body;
validateRequest({
type: "get_account",
timestamp: data.timestamp
}, data.sign, data.floID, data.pubKey).then(req_str => {
market.getAccountDetails(data.floID).then(result => {
res.send(result);
});
}).catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Request processing failed! Try again later!"));
}
});
}
function Login(req, res) {
let data = req.body;
if (!data.code || data.hash != Crypto.SHA1(data.code + secret))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_LOGIN_CODE, "Invalid Code"));
else 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, "Login", {
type: "login",
random: data.code,
proxyKey: data.proxyKey,
timestamp: data.timestamp
}, () => market.login(data.floID, data.proxyKey));
}
function Logout(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Logout", {
type: "logout",
timestamp: data.timestamp
}, () => market.logout(data.floID));
}
function PlaceSellOrder(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Sell order placement", {
type: "sell_order",
asset: data.asset,
quantity: data.quantity,
min_price: data.min_price,
timestamp: data.timestamp
}, () => market.addSellOrder(data.floID, data.asset, data.quantity, data.min_price));
}
function PlaceBuyOrder(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Buy order placement", {
type: "buy_order",
asset: data.asset,
quantity: data.quantity,
max_price: data.max_price,
timestamp: data.timestamp
}, () => market.addBuyOrder(data.floID, data.asset, data.quantity, data.max_price));
}
function CancelOrder(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Order cancellation", {
type: "cancel_order",
order: data.orderType,
id: data.orderID,
timestamp: data.timestamp
}, () => market.cancelOrder(data.orderType, data.orderID, data.floID));
}
/*Currently not used
function TransferAsset(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Token Transfer", {
type: "transfer_token",
receiver: JSON.stringify(data.receiver),
asset: data.asset,
timestamp: data.timestamp
}, () => market.transferAsset(data.floID, data.receiver, data.asset));
}
*/
function DepositAsset(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Deposit Asset", {
type: "deposit_asset",
txid: data.txid,
asset: data.asset,
timestamp: data.timestamp
}, () => market.depositAsset(data.floID, data.asset, data.txid));
}
function WithdrawAsset(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "Withdraw Asset", {
type: "withdraw_asset",
asset: data.asset,
amount: data.amount,
timestamp: data.timestamp
}, () => market.withdrawAsset(data.floID, data.asset, data.amount));
}
function GetUserTransacts(req, res) {
let data = req.body;
processRequest(res, data.floID, data.pubKey, data.sign, "User Transacts", {
type: "get_transact",
timestamp: data.timestamp
}, () => market.getUserTransacts(data.floID));
}
function GenerateSink(req, res) {
let data = req.body;
if (data.floID !== floGlobals.adminID)
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
else 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, "Generate Sink", {
type: "generate_sink",
group: data.group,
timestamp: data.timestamp
}, () => sink.generate(data.group));
}
function ReshareSink(req, res) {
let data = req.body;
console.debug(data)
if (data.floID !== floGlobals.adminID)
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
else if (!data.pubKey)
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
else if (!floCrypto.validateAddr(data.id))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_VALUE, `Invalid ID ${data.id}`));
else processRequest(res, data.floID, data.pubKey, data.sign, "Reshare Sink", {
type: "reshare_sink",
id: data.id,
timestamp: data.timestamp
}, () => sink.reshare(data.id));
}
function DiscardSink(req, res) {
let data = req.body;
if (data.floID !== floGlobals.adminID)
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
else 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, "Discard Sink", {
type: "discard_sink",
id: data.id,
timestamp: data.timestamp
}, () => sink.discard(data.id));
}
/* Public Requests */
function GetLoginCode(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let randID = floCrypto.randString(8, true) + Math.round(Date.now() / 1000);
let hash = Crypto.SHA1(randID + secret);
res.send({
code: randID,
hash: hash
});
}
}
function ListSellOrders(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let asset = req.query.asset;
if (asset && !market.keys.assets.isTradeable(asset))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_ASSET_NAME, "Invalid asset parameter"));
else
DB.query("SELECT SellOrder.floID, SellOrder.asset, SellOrder.price, SellOrder.quantity, SellOrder.time_placed FROM SellOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = SellOrder.floID AND UserBalance.asset = SellOrder.asset" +
" WHERE UserBalance.quantity >= SellOrder.quantity" +
(asset ? " AND SellOrder.asset = ?" : "") +
//" GROUP BY SellOrder.id" +
" ORDER BY SellOrder.price ASC, SellOrder.time_placed ASC" +
" LIMIT 100", [asset || null])
.then(result => res.send(result))
.catch(error => {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
});
}
}
function ListBuyOrders(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let asset = req.query.asset;
if (asset && !market.keys.assets.isTradeable(asset))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_ASSET_NAME, "Invalid asset parameter"));
else
DB.query("SELECT BuyOrder.floID, BuyOrder.asset, BuyOrder.maxPrice, BuyOrder.quantity, BuyOrder.time_placed FROM BuyOrder" +
" INNER JOIN UserBalance ON UserBalance.floID = BuyOrder.floID AND UserBalance.asset = ?" +
" WHERE UserBalance.quantity >= BuyOrder.maxPrice * BuyOrder.quantity" +
(asset ? " AND BuyOrder.asset = ?" : "") +
//" GROUP BY BuyOrder.id" +
" ORDER BY BuyOrder.maxPrice DESC, BuyOrder.time_placed ASC" +
" LIMIT 100", [floGlobals.currency, asset || null])
.then(result => res.send(result))
.catch(error => {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
});
}
}
function ListTradeTransactions(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let asset = req.query.asset;
if (asset && !market.keys.assets.isTradeable(asset))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_ASSET_NAME, "Invalid asset parameter"));
else
DB.query("SELECT * FROM TradeTransactions" +
(asset ? " WHERE asset = ?" : "") +
" ORDER BY tx_time DESC LIMIT 1000", [asset || null])
.then(result => res.send(result))
.catch(error => {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
});
}
}
function GetSink(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let service = req.query.service;
if (!service)
service = serviceList.TRADE; //By default use TRADE service
else if (!(Object.values(serviceList).includes(service)))
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_VALUE, "Invalid service parameter"));
else {
let group;
switch (service) {
case serviceList.TRADE: group = keys.sink_groups.TRADE; break;
}
res.send(keys.sink_chest.active_pick(group));
}
}
}
function GetRateHistory(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let asset = req.query.asset,
duration = req.query.duration || "";
market.getRateHistory(asset, duration)
.then(result => res.send(result))
.catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
}
});
}
}
/*
function GetTransaction(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let txid = req.query.txid;
if (!txid)
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "txid (transactionID) parameter missing"));
else market.getTransactionDetails(txid)
.then(result => res.send(result))
.catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
}
});
}
}
*/
/*
function GetBalance(req, res) {
if (!serving)
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
else {
let floID = req.query.floID || req.query.addr,
asset = req.query.asset || req.query.asset;
market.getBalance(floID, asset)
.then(result => res.send(result))
.catch(error => {
if (error instanceof INVALID)
res.status(INVALID.e_code).send(error.toString());
else {
console.error(error);
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
}
});
}
}
*/
module.exports = {
GetLoginCode,
Login,
Logout,
PlaceBuyOrder,
PlaceSellOrder,
CancelOrder,
//TransferAsset,
ListSellOrders,
ListBuyOrders,
ListTradeTransactions,
GetRateHistory,
//GetTransaction,
//GetBalance,
GetSink,
Account,
DepositAsset,
WithdrawAsset,
GetUserTransacts,
//admin control
GenerateSink,
ReshareSink,
DiscardSink,
set secret(s) {
secret = s;
},
pause() {
serving = false;
background.periodicProcess.stop();
},
resume() {
serving = true;
background.periodicProcess.start();
}
};

28
src/set_globals.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
//fetch for node js (used in floBlockchainAPI.js)
global.fetch = require("node-fetch");
//Set browser paramaters from param.json (or param-default.json)
var param;
try {
param = require('../args/param.json');
} catch {
param = require('../args/param-default.json');
} finally {
for (let p in param)
global[p] = param[p];
}
global.toStandardDecimal = num => parseFloat((parseInt(num * 1e8) * 1e-8).toFixed(8))
if (!process.argv.includes("--debug"))
global.console.debug = () => null;
/*
//Trace the debug logs in node js
var debug = console.debug;
console.debug = function() {
debug.apply(console, arguments);
console.trace();
};
*/

2
start.js Normal file
View File

@ -0,0 +1,2 @@
const start = require('./src/main');
start();