- Added get rates function - Added interval for match buy n sell function - Added checks for buy and sell order placement - Fixed minor bugs
313 lines
14 KiB
JavaScript
313 lines
14 KiB
JavaScript
var net_FLO_price; //container for FLO price (from API or by model)
|
|
var DB; //container for database
|
|
const REFRESH_INTERVAL = 60 * 1000; //1 min
|
|
|
|
function getRates() {
|
|
return new Promise((resolve, reject) => {
|
|
getRates.FLO_USD().then(FLO_rate => {
|
|
getRates.USD_INR().then(INR_rate => {
|
|
net_FLO_price = FLO_rate * INR_rate;
|
|
console.debug('Rates:', FLO_rate, INR_rate, net_FLO_price);
|
|
resolve(net_FLO_price);
|
|
}).catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
getRates.FLO_USD = function() {
|
|
return new Promise((resolve, reject) => {
|
|
fetch('https://api.coinlore.net/api/ticker/?id=67').then(response => {
|
|
if (response.ok) {
|
|
response.json()
|
|
.then(result => resolve(result[0].price_usd))
|
|
.catch(error => reject(error));
|
|
} else
|
|
reject(response.status);
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
getRates.USD_INR = function() {
|
|
return new Promise((resolve, reject) => {
|
|
fetch('https://api.exchangerate-api.com/v4/latest/usd').then(response => {
|
|
if (response.ok) {
|
|
response.json()
|
|
.then(result => resolve(result.rates['INR']))
|
|
.catch(error => reject(error));
|
|
} else
|
|
reject(response.status);
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
function addSellOrder(floID, quantity, min_price) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!floID || !floCrypto.validateAddr(floID))
|
|
return reject(INVALID("Invalid FLO ID"));
|
|
else if (typeof quantity !== "number" || quantity <= 0)
|
|
return reject(INVALID(`Invalid quantity (${quantity})`));
|
|
else if (typeof min_price !== "number" || min_price <= 0)
|
|
return reject(INVALID(`Invalid min_price (${min_price})`));
|
|
DB.query("SELECT SUM(quantity) as total FROM Vault WHERE floID=?", [floID]).then(result => {
|
|
let total = result.pop()["total"] || 0;
|
|
if (total < quantity)
|
|
return reject(INVALID("Insufficient FLO"));
|
|
DB.query("SELECT SUM(quantity) as locked FROM SellOrder WHERE floID=?", [floID]).then(result => {
|
|
let locked = result.pop()["locked"] || 0;
|
|
let available = total - locked;
|
|
console.debug(total, locked, available);
|
|
if (available < quantity)
|
|
return reject(INVALID("Insufficient FLO (Some FLO are locked in another sell order)"));
|
|
DB.query("INSERT INTO SellOrder(floID, quantity, minPrice) VALUES (?, ?, ?)", [floID, quantity, min_price])
|
|
.then(result => resolve("Added SellOrder to DB"))
|
|
.catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
/*
|
|
function addSellOrder(floID, quantity, min_price) {
|
|
return new Promise((resolve, reject) => {
|
|
DB.query("SELECT id, base, (quantity - locked) as available FROM Vault WHERE floID=? ORDER BY base", [floID]).then(result => {
|
|
console.debug(result);
|
|
let rem = quantity,
|
|
sell_base = 0,
|
|
txQueries = [];
|
|
for (let i = 0; i < result.length && rem > 0; i++) {
|
|
var lock = (rem < result[i].available ? rem : result[i].available);
|
|
rem -= lock;
|
|
sell_base += (lock * result[i].base);
|
|
txQueries.push(["UPDATE Vault SET locked=locked-? WHERE id=?", [lock, result[i].id]]);
|
|
}
|
|
if (rem > 0)
|
|
return reject(INVALID("Insufficient FLO"));
|
|
sell_base = sell_base / quantity;
|
|
Promise.all(txQueries.map(a => DB.query(a[0], a[1]))).then(results => {
|
|
DB.query("INSERT INTO SellOrder(floID, quantity, minPrice, sellBase) VALUES (?, ?, ?)", [floID, quantity, min_price, sell_base])
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
*/
|
|
|
|
function addBuyOrder(floID, quantity, max_price) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!floID || !floCrypto.validateAddr(floID))
|
|
return reject(INVALID("Invalid FLO ID"));
|
|
else if (typeof quantity !== "number" || quantity <= 0)
|
|
return reject(INVALID(`Invalid quantity (${quantity})`));
|
|
else if (typeof max_price !== "number" || max_price <= 0)
|
|
return reject(INVALID(`Invalid max_price (${max_price})`));
|
|
DB.query("SELECT rupeeBalance FROM Users WHERE floID=?", [floID]).then(result => {
|
|
if (result.length < 1)
|
|
return reject(INVALID("FLO ID not registered"));
|
|
let total = result.pop()["rupeeBalance"];
|
|
if (total < quantity * max_price)
|
|
return reject(INVALID("Insufficient Rupee balance"));
|
|
DB.query("SELECT SUM(maxPrice * quantity) as locked FROM BuyOrder WHERE floID=?", [floID]).then(result => {
|
|
let locked = result.pop()["locked"] || 0;
|
|
let available = total - locked;
|
|
console.debug(total, locked, available);
|
|
if (available < quantity * max_price)
|
|
return reject(INVALID("Insufficient Rupee balance (Some rupee tokens are locked in another buy order)"));
|
|
DB.query("INSERT INTO BuyOrder(floID, quantity, maxPrice) VALUES (?, ?, ?)", [floID, quantity, max_price])
|
|
.then(result => resolve("Added BuyOrder to DB"))
|
|
.catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
function matchBuyAndSell() {
|
|
let cur_price = net_FLO_price;
|
|
//get the best buyer
|
|
getBestBuyer(cur_price).then(buyer_best => {
|
|
//get the best seller
|
|
getBestSeller(buyer_best.quantity, cur_price).then(result => {
|
|
let seller_best = result.sellOrder,
|
|
txQueries = result.txQueries;
|
|
console.debug("Sell:", seller_best.id, "Buy:", buyer_best.id);
|
|
|
|
//process the Txn
|
|
var tx_quantity;
|
|
if (seller_best.quantity > buyer_best.quantity)
|
|
tx_quantity = processBuyOrder(seller_best, buyer_best, txQueries);
|
|
else if (seller_best.quantity < buyer_best.quantity)
|
|
tx_quantity = processSellOrder(seller_best, buyer_best, txQueries);
|
|
else
|
|
tx_quantity = processBuyAndSellOrder(seller_best, buyer_best, txQueries);
|
|
updateBalance(seller_best, buyer_best, txQueries, cur_price, tx_quantity);
|
|
//process txn query in SQL
|
|
DB.TxQuery(txQueries).then(results => {
|
|
console.log(`Transaction was successful! BuyOrder:${buyer_best.id}| SellOrder:${seller_best.id}`);
|
|
//Since a tx was successful, match again
|
|
matchBuyAndSell();
|
|
}).catch(error => console.error(error));
|
|
}).catch(error => console.error(error));
|
|
}).catch(error => console.error(error));
|
|
}
|
|
|
|
function getBestBuyer(cur_price, n = 0) {
|
|
return new Promise((resolve, reject) => {
|
|
DB.query("SELECT * FROM BuyOrder WHERE maxPrice >= ? ORDER BY time_placed LIMIT ?,1", [cur_price, n]).then(result => {
|
|
let buyOrder = result.shift();
|
|
if (!buyOrder)
|
|
return reject("No valid buyers available");
|
|
DB.query("SELECT rupeeBalance as bal FROM Users WHERE floID=?", [buyOrder.floID]).then(result => {
|
|
if (result[0].bal < cur_price * buyOrder.quantity) {
|
|
//This should not happen unless a buy order is placed when user doesnt have enough rupee balance
|
|
console.warn(`Buy order ${buyOrder.id} is active, but rupee# is insufficient`);
|
|
getBestBuyer(cur_price, n + 1)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error));
|
|
} else
|
|
resolve(buyOrder);
|
|
}).catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
function getBestSeller(maxQuantity, cur_price, n = 0) {
|
|
return new Promise((resolve, reject) => {
|
|
//TODO: Add order conditions for priority.
|
|
DB.query("SELECT * FROM SellOrder WHERE minPrice <=? ORDER BY time_placed LIMIT ?,1", [cur_price, n]).then(result => {
|
|
let sellOrder = result.shift();
|
|
if (!sellOrder)
|
|
return reject("No valid sellers available");
|
|
DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => {
|
|
let rem = Math.min(sellOrder.quantity, maxQuantity),
|
|
sell_base = 0,
|
|
base_quantity = 0,
|
|
txQueries = [];
|
|
for (let i = 0; i < result.length && rem > 0; i++) {
|
|
if (rem < result[i].quantity) {
|
|
txQueries.push(["UPDATE Vault SET quantity=quantity-? WHERE id=?", [rem, result[i].id]]);
|
|
if (result[i].base) {
|
|
sell_base += (rem * result[i].base);
|
|
base_quantity += rem
|
|
}
|
|
rem = 0;
|
|
} else {
|
|
txQueries.push(["DELETE FROM Vault WHERE id=?", [result[i].id]]);
|
|
if (result[i].base) {
|
|
sell_base += (result[i].quantity * result[i].base);
|
|
base_quantity += result[i].quantity
|
|
}
|
|
rem -= result[i].quantity;
|
|
}
|
|
}
|
|
if (base_quantity)
|
|
sell_base = sell_base / base_quantity;
|
|
if (rem > 0 || sell_base > cur_price) {
|
|
//1st condition (rem>0) should not happen (sell order placement was success when insufficient FLO).
|
|
if (rem > 0)
|
|
console.warn(`Sell order ${sellOrder.id} is active, but FLO is insufficient`);
|
|
getBestSeller(maxQuantity, cur_price, n + 1)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error));
|
|
} else
|
|
resolve({
|
|
sellOrder,
|
|
txQueries
|
|
});
|
|
}).catch(error => reject(error));
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
function processBuyOrder(seller_best, buyer_best, txQueries) {
|
|
let quantity = buyer_best.quantity;
|
|
//Buy order is completed, sell order is partially done.
|
|
txQueries.push(["DELETE FROM BuyOrder WHERE id=?", [buyer_best.id]]);
|
|
txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]);
|
|
return quantity;
|
|
}
|
|
|
|
function processSellOrder(seller_best, buyer_best, txQueries) {
|
|
let quantity = seller_best.quantity;
|
|
//Sell order is completed, buy order is partially done.
|
|
txQueries.push(["DELETE FROM SellOrder WHERE id=?", [seller_best.id]]);
|
|
txQueries.push(["UPDATE BuyOrder SET quantity=quantity-? WHERE id=?", [quantity, buyer_best.id]]);
|
|
return quantity;
|
|
}
|
|
|
|
function processBuyAndSellOrder(seller_best, buyer_best, txQueries) {
|
|
//Both sell order and buy order is completed
|
|
txQueries.push(["DELETE FROM SellOrder WHERE id=?", [seller_best.id]]);
|
|
txQueries.push(["DELETE FROM BuyOrder WHERE id=?", [buyer_best.id]]);
|
|
return seller_best.quantity;
|
|
}
|
|
|
|
function updateBalance(seller_best, buyer_best, txQueries, cur_price, quantity) {
|
|
//Update rupee balance for seller and buyer
|
|
let totalAmount = cur_price * quantity;
|
|
txQueries.push(["UPDATE Users SET rupeeBalance=rupeeBalance+? WHERE floID=?", [totalAmount, seller_best.floID]]);
|
|
txQueries.push(["UPDATE Users SET rupeeBalance=rupeeBalance-? WHERE floID=?", [totalAmount, buyer_best.floID]]);
|
|
//Add coins to Buyer
|
|
txQueries.push(["INSERT INTO Vault(floID, base, quantity) VALUES (?, ?, ?)", [buyer_best.floID, cur_price, quantity]])
|
|
//Record transaction
|
|
txQueries.push(["INSERT INTO Transactions (seller, buyer, quantity, unitValue) VALUES (?, ?, ?, ?)", [seller_best.floID, buyer_best.floID, quantity, cur_price]]);
|
|
return;
|
|
}
|
|
|
|
function getAccountDetails(floID) {
|
|
return new Promise((resolve, reject) => {
|
|
let select = [];
|
|
select.push(["rupeeBalance", "Users"]);
|
|
select.push(["base, quantity", "Vault"]);
|
|
select.push(["id, quantity, minPrice, time_placed", "SellOrder"]);
|
|
select.push(["id, quantity, maxPrice, time_placed", "BuyOrder"]);
|
|
let promises = select.map(a => DB.query("SELECT " + a[0] + " FROM " + a[1] + " 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.rupee_total = a.value[0].rupeeBalance;
|
|
break;
|
|
case 1:
|
|
response.coins = a.value;
|
|
break;
|
|
case 2:
|
|
response.sellOrders = a.value;
|
|
break;
|
|
case 3:
|
|
response.buyOrders = a.value;
|
|
break;
|
|
}
|
|
});
|
|
DB.query("SELECT * FROM Transactions WHERE seller=? OR buyer=?", [floID, floID])
|
|
.then(result => response.transactions = result)
|
|
.catch(error => console.error(error))
|
|
.finally(_ => resolve(response));
|
|
});
|
|
});
|
|
}
|
|
|
|
function intervalFunction() {
|
|
let old_rate = net_FLO_price;
|
|
getRates().then(cur_rate => {
|
|
matchBuyAndSell();
|
|
}).catch(error => console.error(error));
|
|
}
|
|
|
|
intervalFunction();
|
|
|
|
let refresher = setInterval(intervalFunction, REFRESH_INTERVAL);
|
|
|
|
module.exports = {
|
|
addBuyOrder,
|
|
addSellOrder,
|
|
getAccountDetails,
|
|
set DB(db) {
|
|
DB = db;
|
|
}
|
|
}; |