From bb0d49614bf22e7639978da065a0a546ecdb747c Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 30 Oct 2021 00:24:38 +0530 Subject: [PATCH 1/9] Create group.js --- src/group.js | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/group.js diff --git a/src/group.js b/src/group.js new file mode 100644 index 0000000..9fda360 --- /dev/null +++ b/src/group.js @@ -0,0 +1,151 @@ +var DB; //container for database + +function addTag(floID, tag) { + return new Promise((resolve, reject) => { + DB.query("INSERT INTO Tags (floID, tag) VALUE (?,?)", [floID, tag]) + .then(result => resolve(`Added ${floID} to ${tag}`)) + .catch(error => { + if (error.code === "ER_DUP_ENTRY") + reject(`${floID} already in ${tag}`); + else + reject(error); + }); + }); +} + +function getBest() { + return new Promise((resolve, reject) => { + DB.query("SELECT tag FROM TagList ORDER BY priority DESC").then(result => { + let tags = result.map(r => r.tag); + recursiveGetBest(tags) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + }) +} + +function recursiveGetBest(tags) { + return new Promise((resolve, reject) => { + let tag = tags.shift(); + getBestInTag(tag) + .then(result => resolve(result)) + .catch(error => { + if (error !== false) + return reject(error); + else + recursiveGetBest(tags) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + }) +} + +function getBestInTag(tag, cur_price) { + return new Promise((resolve, reject) => { + DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + + " INNER JOIN Tags ON Tags.floID = SellOrder.floID" + + " WHERE Tags.tag = ? AND minPrice <=? ORDER BY time_placed", [tag, cur_price]).then(orders => { + if (orders.length === 0) + reject(false); + else if (orders.length === 1) + checkMinimumGain(orders[0]) + .then(result => resolve(result)) + .catch(error => reject(error)) + else + getPointsFromAPI(orders.map(o => o.floID)).then(points => { + let orders_sorted = orders.map(o => [o, points[o.floID]]) + .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) //sort by points (ascending) + .map(x => x[0]); + getTopValidOrder(orders_sorted) + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + }); +} + +function getPointsFromAPI(floIDs) { + floIDs = Array.from(new Set(floIDs)); + return new Promise((resolve, reject) => { + DB.query("SELECT api FROM TagList WHERE tag=?", [tag]).then(result => { + let api = result[0].api; + Promise.allSettled(floIDs.map(id => fetch_api(api, id))).then(result => { + let points = {}; + for (let i in result) + if (result[i].status === "fulfilled") + points[floIDs[i]] = result[i]; + resolve(points); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); +} + +function fetch_api(api, id) { + return new Promise((resolve, reject) => { + //TODO: fetch data from API + }) +} + +function getTopValidOrder(orders) { + return new Promise((resolve, reject) => { + if (!orders.length) + return reject(false) + checkMinimumGain(orders.pop()) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + .then(result => resolve(result)) + .catch(error => { + if (error !== false) + return reject(error); + else + getTopValidOrder(orders) + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + }); +} + +function checkMinimumGain(sellOrder) { + return new Promise((resolve, reject) => { + 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`); + reject(false); + } else + resolve({ + sellOrder, + txQueries + }); + }).catch(error => reject(error)); + }) + +} + +module.exports = { + set DB(db) { + DB = db; + } +}; \ No newline at end of file From 097b64b82f2e7df6267a563ada2a28673a078f75 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 30 Oct 2021 03:39:10 +0530 Subject: [PATCH 2/9] Update group.js - Adding getBestPairs - best pair of orders (buy, sell) can be fetched using the new functions --- src/group.js | 119 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 36 deletions(-) diff --git a/src/group.js b/src/group.js index 9fda360..55a8e9a 100644 --- a/src/group.js +++ b/src/group.js @@ -13,54 +13,100 @@ function addTag(floID, tag) { }); } -function getBest() { +function getBestPairs(currentRate) { return new Promise((resolve, reject) => { - DB.query("SELECT tag FROM TagList ORDER BY priority DESC").then(result => { - let tags = result.map(r => r.tag); - recursiveGetBest(tags) - .then(result => resolve(result)) - .catch(error => reject(error)) + DB.query("SELECT tag FROM TagList ORDER BY priority").then(result => { + let tags = result.map(r => r.tag) //Sorted in Ascending (ie, stack; pop for highest) + resolve(new bestPair(tags, currentRate)); + }).catch(error => reject(error)) + }) +} + +const bestPair = function(tags, currentRate) { + + const getSellOrder = () => { + let cache = getSellOrder.cache; + return new Promise((resolve, reject) => { + if (cache.cur_order) { //If cache already has a pending order + checkMinimumGain(cache.cur_order, currentRate).then(result => { + cache.cur_order = result.sellOrder; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //Order not valid (minimum gain) + cache.cur_order = null; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority + getTopValidOrder(cache.orders, currentRate).then(result => { + cache.cur_order = result.sellOrder; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //No valid order found in current tag + cache.orders = null; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.tags.length) { //If cache has remaining tags + cache.cur_tag = cache.tags.pop(); + getOrdersInTag(cache.cur_tag, currentRate).then(orders => { + cache.orders = orders; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } else if (!cache.end) { //Un-tagged floID's orders (do only once) + getUntaggedOrders(currentRate).then(orders => { + cache.orders = orders; + cache.end = true; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } }) - }) + }; + getSeller.cache = { + tags: Array.from(tags) + }; + + const getBuyOrder = () => {}; + getBuyOrder.cache = { + tags: Array.from(tags) //Maybe diff for buy and sell ? + }; } -function recursiveGetBest(tags) { +function getUntaggedOrders(cur_price) { return new Promise((resolve, reject) => { - let tag = tags.shift(); - getBestInTag(tag) - .then(result => resolve(result)) - .catch(error => { - if (error !== false) - return reject(error); - else - recursiveGetBest(tags) - .then(result => resolve(result)) - .catch(error => reject(error)) - }) + DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + + " LEFT JOIN Tags ON Tags.floID = SellOrder.floID" + + " WHERE Tags.floID IS NULL AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed", [cur_price]) + .then(orders => resolve(orders)) + .catch(error => reject(error)) }) } -function getBestInTag(tag, cur_price) { +function getOrdersInTag(tag, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + " INNER JOIN Tags ON Tags.floID = SellOrder.floID" + - " WHERE Tags.tag = ? AND minPrice <=? ORDER BY time_placed", [tag, cur_price]).then(orders => { - if (orders.length === 0) - reject(false); - else if (orders.length === 1) - checkMinimumGain(orders[0]) - .then(result => resolve(result)) - .catch(error => reject(error)) + " WHERE Tags.tag = ? AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed", [tag, cur_price]).then(orders => { + if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. + resolve(orders); else getPointsFromAPI(orders.map(o => o.floID)).then(points => { let orders_sorted = orders.map(o => [o, points[o.floID]]) .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) //sort by points (ascending) .map(x => x[0]); - getTopValidOrder(orders_sorted) - .then(result => resolve(result)) - .catch(error => reject(error)) + resolve(orders_sorted); }).catch(error => reject(error)) - }) + }).catch(error => reject(error)) }); } @@ -86,24 +132,23 @@ function fetch_api(api, id) { }) } -function getTopValidOrder(orders) { +function getTopValidOrder(orders, cur_price) { return new Promise((resolve, reject) => { if (!orders.length) return reject(false) - checkMinimumGain(orders.pop()) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + checkMinimumGain(orders.pop(), cur_price) //pop: as the orders are sorted in ascending (highest point should be checked 1st) .then(result => resolve(result)) .catch(error => { if (error !== false) return reject(error); - else - getTopValidOrder(orders) + getTopValidOrder(orders, cur_price) .then(result => resolve(result)) .catch(error => reject(error)); }); }); } -function checkMinimumGain(sellOrder) { +function checkMinimumGain(sellOrder, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { let rem = Math.min(sellOrder.quantity, maxQuantity), @@ -145,6 +190,8 @@ function checkMinimumGain(sellOrder) { } module.exports = { + addTag, + getBestPairs, set DB(db) { DB = db; } From 0e34dcb5e3cd306084c5eceeeb073129a27244ac Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 30 Oct 2021 20:13:48 +0530 Subject: [PATCH 3/9] Update group.js - Adding functions to get best buy-order. - Renamed some fns of get-sell-order for easier identification. --- src/group.js | 210 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 156 insertions(+), 54 deletions(-) diff --git a/src/group.js b/src/group.js index 55a8e9a..3cfb5ed 100644 --- a/src/group.js +++ b/src/group.js @@ -24,65 +24,110 @@ function getBestPairs(currentRate) { const bestPair = function(tags, currentRate) { - const getSellOrder = () => { + const getSellOrder = () => new Promise((resolve, reject) => { let cache = getSellOrder.cache; - return new Promise((resolve, reject) => { - if (cache.cur_order) { //If cache already has a pending order - checkMinimumGain(cache.cur_order, currentRate).then(result => { - cache.cur_order = result.sellOrder; - resolve(result); - }).catch(error => { - if (error !== false) - return reject(error); - //Order not valid (minimum gain) - cache.cur_order = null; - getSellOrder() - .then(result => resolve(result)) - .catch(error => reject(error)) - }) - } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority - getTopValidOrder(cache.orders, currentRate).then(result => { - cache.cur_order = result.sellOrder; - resolve(result); - }).catch(error => { - if (error !== false) - return reject(error); - //No valid order found in current tag - cache.orders = null; - getSellOrder() - .then(result => resolve(result)) - .catch(error => reject(error)) - }) - } else if (cache.tags.length) { //If cache has remaining tags - cache.cur_tag = cache.tags.pop(); - getOrdersInTag(cache.cur_tag, currentRate).then(orders => { - cache.orders = orders; - getSellOrder() - .then(result => resolve(result)) - .catch(error => reject(error)) - }).catch(error => reject(error)); - } else if (!cache.end) { //Un-tagged floID's orders (do only once) - getUntaggedOrders(currentRate).then(orders => { - cache.orders = orders; - cache.end = true; - getSellOrder() - .then(result => resolve(result)) - .catch(error => reject(error)) - }).catch(error => reject(error)); - } - }) - }; + if (cache.cur_order) { //If cache already has a pending order + verifySellOrder(cache.cur_order, currentRate).then(result => { + cache.cur_order = result.sellOrder; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //Order not valid (minimum gain) + cache.cur_order = null; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority + getTopValidSellOrder(cache.orders, currentRate).then(result => { + cache.cur_order = result.sellOrder; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //No valid order found in current tag + cache.orders = null; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.tags.length) { //If cache has remaining tags + cache.cur_tag = cache.tags.pop(); + getSellOrdersInTag(cache.cur_tag, currentRate).then(orders => { + cache.orders = orders; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } else if (!cache.end) { //Un-tagged floID's orders (do only once) + getUntaggedSellOrders(currentRate).then(orders => { + cache.orders = orders; + cache.end = true; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } else + reject(false); + }); getSeller.cache = { tags: Array.from(tags) }; - const getBuyOrder = () => {}; + const getBuyOrder = () => new Promise((resolve, reject) => { + let cache = getBuyOrder.cache; + if (cache.cur_order) { //If cache already has a pending order + verifyBuyOrder(cache.cur_order, currentRate).then(result => { + cache.cur_order = result; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //Order not valid (cash not available) + cache.cur_order = null; + getBuyOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority + getTopValidBuyOrder(cache.orders, currentRate).then(result => { + cache.cur_order = result; + resolve(result); + }).catch(error => { + if (error !== false) + return reject(error); + //No valid order found in current tag + cache.orders = null; + getBuyOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }) + } else if (cache.tags.length) { //If cache has remaining tags + cache.cur_tag = cache.tags.pop(); + getBuyOrdersInTag(cache.cur_tag, currentRate).then(orders => { + cache.orders = orders; + getBuyOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } else if (!cache.end) { //Un-tagged floID's orders (do only once) + getUntaggedBuyOrders(currentRate).then(orders => { + cache.orders = orders; + cache.end = true; + getBuyOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) + }).catch(error => reject(error)); + } else + reject(false); + }); getBuyOrder.cache = { tags: Array.from(tags) //Maybe diff for buy and sell ? }; } -function getUntaggedOrders(cur_price) { +function getUntaggedSellOrders(cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + " LEFT JOIN Tags ON Tags.floID = SellOrder.floID" + @@ -92,7 +137,17 @@ function getUntaggedOrders(cur_price) { }) } -function getOrdersInTag(tag, cur_price) { +function getUntaggedBuyOrders(cur_price) { + return new Promise((resolve, reject) => { + DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" + + " LEFT JOIN Tags ON Tags.floID = BuyOrder.floID" + + " WHERE Tags.floID IS NULL AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed", [cur_price]) + .then(orders => resolve(orders)) + .catch(error => reject(error)) + }) +} + +function getSellOrdersInTag(tag, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + " INNER JOIN Tags ON Tags.floID = SellOrder.floID" + @@ -110,6 +165,24 @@ function getOrdersInTag(tag, cur_price) { }); } +function getBuyOrdersInTag(tag, cur_price) { + return new Promise((resolve, reject) => { + DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" + + " INNER JOIN Tags ON Tags.floID = BuyOrder.floID" + + " WHERE Tags.tag = ? AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed", [tag, cur_price]).then(orders => { + if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. + resolve(orders); + else + getPointsFromAPI(orders.map(o => o.floID)).then(points => { + let orders_sorted = orders.map(o => [o, points[o.floID]]) + .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) //sort by points (ascending) + .map(x => x[0]); + resolve(orders_sorted); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }); +} + function getPointsFromAPI(floIDs) { floIDs = Array.from(new Set(floIDs)); return new Promise((resolve, reject) => { @@ -132,23 +205,23 @@ function fetch_api(api, id) { }) } -function getTopValidOrder(orders, cur_price) { +function getTopValidSellOrder(orders, cur_price) { return new Promise((resolve, reject) => { if (!orders.length) return reject(false) - checkMinimumGain(orders.pop(), cur_price) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + verifySellOrder(orders.pop(), cur_price) //pop: as the orders are sorted in ascending (highest point should be checked 1st) .then(result => resolve(result)) .catch(error => { if (error !== false) return reject(error); - getTopValidOrder(orders, cur_price) + getTopValidSellOrder(orders, cur_price) .then(result => resolve(result)) .catch(error => reject(error)); }); }); } -function checkMinimumGain(sellOrder, cur_price) { +function verifySellOrder(sellOrder, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { let rem = Math.min(sellOrder.quantity, maxQuantity), @@ -189,6 +262,35 @@ function checkMinimumGain(sellOrder, cur_price) { } +function getTopValidBuyOrder(orders, cur_price) { + return new Promise((resolve, reject) => { + if (!orders.length) + return reject(false) + verifyBuyOrder(orders.pop(), cur_price) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + .then(result => resolve(result)) + .catch(error => { + if (error !== false) + return reject(error); + getTopValidBuyOrder(orders, cur_price) + .then(result => resolve(result)) + .catch(error => reject(error)); + }); + }); +} + +function verifyBuyOrder(buyOrder, cur_price) { + return new Promise((resolve, reject) => { + DB.query("SELECT rupeeBalance AS bal FROM Cash 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`); + reject(false); + } else + resolve(buyOrder); + }).catch(error => reject(error)); + }) +} + module.exports = { addTag, getBestPairs, From c14f10eade3d7b3cae31ba8d8859ba06bc062d2c Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 30 Oct 2021 21:35:18 +0530 Subject: [PATCH 4/9] Use Group priority to get best buy/sell order --- src/group.js | 43 +++++++++++++++----- src/market.js | 108 ++++++++++++++++---------------------------------- 2 files changed, 66 insertions(+), 85 deletions(-) diff --git a/src/group.js b/src/group.js index 3cfb5ed..43d2a1d 100644 --- a/src/group.js +++ b/src/group.js @@ -24,11 +24,38 @@ function getBestPairs(currentRate) { const bestPair = function(tags, currentRate) { + this.get = () => new Promise((resolve, reject) => { + Promise.all([getBuyOrder(), getSellOrder()]).then(results => { + resolve({ + buyOrder: results[0], + sellOrder: results[1] + }) + }).catch(error => reject(error)) + }); + + this.next = () => { + let buy = getBuyOrder.cache, + sell = getSellOrder.cache; + if (buy.cur_order && sell.cur_order) { + if (buy.cur_order.quantity > sell.cur_order.quantity) { + buy.cur_order.quantity -= sell.cur_order.quantity; + sell.cur_order = null; + } else if (buy.cur_order.quantity < sell.cur_order.quantity) { + sell.cur_order.quantity -= buy.cur_order.quantity; + buy.cur_order = null; + } else { + sell.cur_order = null; + buy.cur_order = null; + } + } else + throw Error("No current order found"); + }; + const getSellOrder = () => new Promise((resolve, reject) => { let cache = getSellOrder.cache; if (cache.cur_order) { //If cache already has a pending order verifySellOrder(cache.cur_order, currentRate).then(result => { - cache.cur_order = result.sellOrder; + cache.cur_order = result; resolve(result); }).catch(error => { if (error !== false) @@ -41,7 +68,7 @@ const bestPair = function(tags, currentRate) { }) } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority getTopValidSellOrder(cache.orders, currentRate).then(result => { - cache.cur_order = result.sellOrder; + cache.cur_order = result; resolve(result); }).catch(error => { if (error !== false) @@ -224,20 +251,17 @@ function getTopValidSellOrder(orders, cur_price) { function verifySellOrder(sellOrder, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { - let rem = Math.min(sellOrder.quantity, maxQuantity), + let rem = sellOrder.quantity, sell_base = 0, - base_quantity = 0, - txQueries = []; + base_quantity = 0; 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; @@ -253,10 +277,7 @@ function verifySellOrder(sellOrder, cur_price) { console.warn(`Sell order ${sellOrder.id} is active, but FLO is insufficient`); reject(false); } else - resolve({ - sellOrder, - txQueries - }); + resolve(sellOrder); }).catch(error => reject(error)); }) diff --git a/src/market.js b/src/market.js index 8fa2044..70150d8 100644 --- a/src/market.js +++ b/src/market.js @@ -1,3 +1,5 @@ +const group = require("./group"); + var net_FLO_price; //container for FLO price (from API or by model) var DB; //container for database @@ -168,16 +170,18 @@ function cancelOrder(type, id, floID) { }); } -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); +function initiateCoupling() { + group.getBestPairs() + .then(bestPairQueue => processCoupling(bestPairQueue)) + .catch(error => reject(error)) +} +function processCoupling(bestPairQueue) { + bestPairQueue.get().then(result => { + let buyer_best = result.buyOrder, + seller_best = result.sellOrder; + console.debug("Sell:", seller_best.id, "Buy:", buyer_best.id); + spendFLO(buyer_best, seller_best).then(txQueries => { //process the Txn var tx_quantity; if (seller_best.quantity > buyer_best.quantity) @@ -189,80 +193,35 @@ function matchBuyAndSell() { updateBalance(seller_best, buyer_best, txQueries, cur_price, tx_quantity); //process txn query in SQL DB.transaction(txQueries).then(results => { + bestPairQueue.next(); console.log(`Transaction was successful! BuyOrder:${buyer_best.id}| SellOrder:${seller_best.id}`); //Since a tx was successful, match again - matchBuyAndSell(); + processCoupling(bestPairQueue); }).catch(error => console.error(error)); }).catch(error => console.error(error)); }).catch(error => console.error(error)); } -function getBestBuyer(cur_price, n = 0) { +function spendFLO(buyOrder, sellOrder) { 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 Cash 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; - } + DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { + let rem = Math.min(buyOrder.quantity, sellOrder.quantity), + 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]]); + rem = 0; + } else { + txQueries.push(["DELETE FROM Vault WHERE id=?", [result[i].id]]); + 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)); + } + if (rem > 0) + reject(`Sell order ${sellOrder.id} is active, but FLO is insufficient`); + else + resolve(txQueries); }).catch(error => reject(error)); - }); + }) } function processBuyOrder(seller_best, buyer_best, txQueries) { @@ -643,7 +602,7 @@ function periodicProcess() { let old_rate = net_FLO_price; getRates().then(cur_rate => { transactionReCheck(); - matchBuyAndSell(); + initiateCoupling(); }).catch(error => console.error(error)); } @@ -669,5 +628,6 @@ module.exports = { periodicProcess, set DB(db) { DB = db; + group.DB = db; } }; \ No newline at end of file From d435157bd2577e47829ca6b815fb141e3f3af3f9 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Tue, 2 Nov 2021 03:39:07 +0530 Subject: [PATCH 5/9] Different group/tag priority for sell and buy --- src/group.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/group.js b/src/group.js index 43d2a1d..f1ea62b 100644 --- a/src/group.js +++ b/src/group.js @@ -15,14 +15,16 @@ function addTag(floID, tag) { function getBestPairs(currentRate) { return new Promise((resolve, reject) => { - DB.query("SELECT tag FROM TagList ORDER BY priority").then(result => { - let tags = result.map(r => r.tag) //Sorted in Ascending (ie, stack; pop for highest) - resolve(new bestPair(tags, currentRate)); + DB.query("SELECT tag, sellPriority, buyPriority FROM TagList").then(result => { + //Sorted in Ascending (ie, stack; pop for highest) + let tags_buy = result.sort((a, b) => a.buyPriority > b.buyPriority).map(r => r.tag); + let tags_sell = result.sort((a, b) => a.sellPriority > b.sellPriority).map(r => r.tag); + resolve(new bestPair(currentRate, tags_buy, tags_sell)); }).catch(error => reject(error)) }) } -const bestPair = function(tags, currentRate) { +const bestPair = function(currentRate, tags_buy, tags_sell) { this.get = () => new Promise((resolve, reject) => { Promise.all([getBuyOrder(), getSellOrder()]).then(results => { @@ -99,7 +101,7 @@ const bestPair = function(tags, currentRate) { reject(false); }); getSeller.cache = { - tags: Array.from(tags) + tags: tags_sell }; const getBuyOrder = () => new Promise((resolve, reject) => { @@ -150,7 +152,7 @@ const bestPair = function(tags, currentRate) { reject(false); }); getBuyOrder.cache = { - tags: Array.from(tags) //Maybe diff for buy and sell ? + tags: tags_buy }; } From 744979fb2a3afc8e3443c466c398bd471e755285 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Fri, 5 Nov 2021 21:43:15 +0530 Subject: [PATCH 6/9] Adding tables for Tags (groups) --- args/schema.sql | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/args/schema.sql b/args/schema.sql index 055e8e6..c06584d 100644 --- a/args/schema.sql +++ b/args/schema.sql @@ -100,6 +100,24 @@ status VARCHAR(50) NOT NULL, PRIMARY KEY(id) ); +CREATE TABLE TagList ( +id INT NOT NULL AUTO_INCREMENT, +tag VARCHAR(50) NOT NULL, +sellPriority INT, +buyPriority INT, +api TINYTEXT, +PRIMARY KEY(tag), +KEY (id) +); + +CREATE TABLE Tags ( +id INT NOT NULL AUTO_INCREMENT, +floID CHAR(34) NOT NULL, +tag VARCHAR(50) NOT NULL, +PRIMARY KEY(floID, tag), +KEY (id) +); + /* Backup feature (Table and Triggers) */ CREATE TABLE _backup ( @@ -164,4 +182,18 @@ FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputRupee', NEW.id) ON CREATE TRIGGER outputRupee_U AFTER UPDATE ON outputRupee FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputRupee', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; CREATE TRIGGER outputRupee_D AFTER DELETE ON outputRupee -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputRupee', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; \ No newline at end of file +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputRupee', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; + +CREATE TRIGGER TagList_I AFTER INSERT ON TagList +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TagList', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER TagList_U AFTER UPDATE ON TagList +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TagList', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER TagList_D AFTER DELETE ON TagList +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TagList', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; + +CREATE TRIGGER Tags_I AFTER INSERT ON Tags +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Tags', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER Tags_U AFTER UPDATE ON Tags +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Tags', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER Tags_D AFTER DELETE ON Tags +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Tags', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; \ No newline at end of file From b5cde90e89cbf3ed7d2056dba7e04a05a37f47a1 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Fri, 5 Nov 2021 21:43:26 +0530 Subject: [PATCH 7/9] Bug fix --- src/group.js | 45 ++++++++++++++++++++++++++++++--------------- src/market.js | 16 +++++++++++----- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/group.js b/src/group.js index f1ea62b..859007e 100644 --- a/src/group.js +++ b/src/group.js @@ -17,14 +17,19 @@ function getBestPairs(currentRate) { return new Promise((resolve, reject) => { DB.query("SELECT tag, sellPriority, buyPriority FROM TagList").then(result => { //Sorted in Ascending (ie, stack; pop for highest) - let tags_buy = result.sort((a, b) => a.buyPriority > b.buyPriority).map(r => r.tag); - let tags_sell = result.sort((a, b) => a.sellPriority > b.sellPriority).map(r => r.tag); + let tags_buy = result.sort((a, b) => a.buyPriority > b.buyPriority ? 1 : -1).map(r => r.tag); + let tags_sell = result.sort((a, b) => a.sellPriority > b.sellPriority ? 1 : -1).map(r => r.tag); resolve(new bestPair(currentRate, tags_buy, tags_sell)); }).catch(error => reject(error)) }) } -const bestPair = function(currentRate, tags_buy, tags_sell) { +const bestPair = function(cur_rate, tags_buy, tags_sell) { + const currentRate = cur_rate; + + Object.defineProperty(this, 'cur_rate', { + get: () => currentRate + }); this.get = () => new Promise((resolve, reject) => { Promise.all([getBuyOrder(), getSellOrder()]).then(results => { @@ -92,6 +97,7 @@ const bestPair = function(currentRate, tags_buy, tags_sell) { } else if (!cache.end) { //Un-tagged floID's orders (do only once) getUntaggedSellOrders(currentRate).then(orders => { cache.orders = orders; + cache.cur_tag = null; cache.end = true; getSellOrder() .then(result => resolve(result)) @@ -100,7 +106,7 @@ const bestPair = function(currentRate, tags_buy, tags_sell) { } else reject(false); }); - getSeller.cache = { + getSellOrder.cache = { tags: tags_sell }; @@ -143,6 +149,7 @@ const bestPair = function(currentRate, tags_buy, tags_sell) { } else if (!cache.end) { //Un-tagged floID's orders (do only once) getUntaggedBuyOrders(currentRate).then(orders => { cache.orders = orders; + cache.cur_tag = null; cache.end = true; getBuyOrder() .then(result => resolve(result)) @@ -160,7 +167,7 @@ function getUntaggedSellOrders(cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + " LEFT JOIN Tags ON Tags.floID = SellOrder.floID" + - " WHERE Tags.floID IS NULL AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed", [cur_price]) + " WHERE Tags.floID IS NULL AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed DESC", [cur_price]) .then(orders => resolve(orders)) .catch(error => reject(error)) }) @@ -170,7 +177,7 @@ function getUntaggedBuyOrders(cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" + " LEFT JOIN Tags ON Tags.floID = BuyOrder.floID" + - " WHERE Tags.floID IS NULL AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed", [cur_price]) + " WHERE Tags.floID IS NULL AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed DESC", [cur_price]) .then(orders => resolve(orders)) .catch(error => reject(error)) }) @@ -180,13 +187,13 @@ function getSellOrdersInTag(tag, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity FROM SellOrder" + " INNER JOIN Tags ON Tags.floID = SellOrder.floID" + - " WHERE Tags.tag = ? AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed", [tag, cur_price]).then(orders => { + " WHERE Tags.tag = ? AND SellOrder.minPrice <=? ORDER BY SellOrder.time_placed DESC", [tag, cur_price]).then(orders => { if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. resolve(orders); else - getPointsFromAPI(orders.map(o => o.floID)).then(points => { + getPointsFromAPI(tag, orders.map(o => o.floID)).then(points => { let orders_sorted = orders.map(o => [o, points[o.floID]]) - .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) //sort by points (ascending) + .sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0) //sort by points (ascending) .map(x => x[0]); resolve(orders_sorted); }).catch(error => reject(error)) @@ -198,13 +205,13 @@ function getBuyOrdersInTag(tag, cur_price) { return new Promise((resolve, reject) => { DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" + " INNER JOIN Tags ON Tags.floID = BuyOrder.floID" + - " WHERE Tags.tag = ? AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed", [tag, cur_price]).then(orders => { + " WHERE Tags.tag = ? AND BuyOrder.maxPrice >=? ORDER BY BuyOrder.time_placed DESC", [tag, cur_price]).then(orders => { if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. resolve(orders); else - getPointsFromAPI(orders.map(o => o.floID)).then(points => { + getPointsFromAPI(tag, orders.map(o => o.floID)).then(points => { let orders_sorted = orders.map(o => [o, points[o.floID]]) - .sort((a, b) => a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) //sort by points (ascending) + .sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0) //sort by points (ascending) .map(x => x[0]); resolve(orders_sorted); }).catch(error => reject(error)) @@ -212,7 +219,7 @@ function getBuyOrdersInTag(tag, cur_price) { }); } -function getPointsFromAPI(floIDs) { +function getPointsFromAPI(tag, floIDs) { floIDs = Array.from(new Set(floIDs)); return new Promise((resolve, reject) => { DB.query("SELECT api FROM TagList WHERE tag=?", [tag]).then(result => { @@ -221,7 +228,7 @@ function getPointsFromAPI(floIDs) { let points = {}; for (let i in result) if (result[i].status === "fulfilled") - points[floIDs[i]] = result[i]; + points[floIDs[i]] = result[i].value; resolve(points); }).catch(error => reject(error)) }).catch(error => reject(error)) @@ -231,6 +238,15 @@ function getPointsFromAPI(floIDs) { function fetch_api(api, id) { return new Promise((resolve, reject) => { //TODO: fetch data from API + let url = api.replace('', id); + global.fetch(url).then(response => { + if (response.ok) + response.text() + .then(result => resolve(result)) + .catch(error => reject(error)) + else + reject(response); + }).catch(error => reject(error)) }) } @@ -282,7 +298,6 @@ function verifySellOrder(sellOrder, cur_price) { resolve(sellOrder); }).catch(error => reject(error)); }) - } function getTopValidBuyOrder(orders, cur_price) { diff --git a/src/market.js b/src/market.js index 70150d8..ffcbf97 100644 --- a/src/market.js +++ b/src/market.js @@ -171,16 +171,17 @@ function cancelOrder(type, id, floID) { } function initiateCoupling() { - group.getBestPairs() + group.getBestPairs(net_FLO_price) .then(bestPairQueue => processCoupling(bestPairQueue)) - .catch(error => reject(error)) + .catch(error => console.error("initiateCoupling", error)) } function processCoupling(bestPairQueue) { bestPairQueue.get().then(result => { let buyer_best = result.buyOrder, seller_best = result.sellOrder; - console.debug("Sell:", seller_best.id, "Buy:", buyer_best.id); + console.debug("Sell:", seller_best); + console.debug("Buy:", buyer_best); spendFLO(buyer_best, seller_best).then(txQueries => { //process the Txn var tx_quantity; @@ -190,7 +191,7 @@ function processCoupling(bestPairQueue) { 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); + updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.cur_rate, tx_quantity); //process txn query in SQL DB.transaction(txQueries).then(results => { bestPairQueue.next(); @@ -199,7 +200,12 @@ function processCoupling(bestPairQueue) { processCoupling(bestPairQueue); }).catch(error => console.error(error)); }).catch(error => console.error(error)); - }).catch(error => console.error(error)); + }).catch(error => { + if (error !== false) + console.error(error); + else + console.log("No valid orders."); + }); } function spendFLO(buyOrder, sellOrder) { From 5ac3316008166eb66a8d50178a34d400a4fe839a Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 18 Nov 2021 15:58:40 +0530 Subject: [PATCH 8/9] Allow trusted IDs to add/rm user tags via API --- public/fn.js | 56 +++++++++++++++++++++++++ setup/configure-settings.js | 49 ++++++++++++++++++---- src/app.js | 7 +++- src/group.js | 11 ++++- src/main.js | 2 +- src/market.js | 1 + src/request.js | 83 +++++++++++++++++++++++++++++++++++-- 7 files changed, 196 insertions(+), 13 deletions(-) diff --git a/public/fn.js b/public/fn.js index 25c4b4c..f288367 100644 --- a/public/fn.js +++ b/public/fn.js @@ -405,4 +405,60 @@ function withdrawRupee(quantity, proxySecret) { .catch(error => reject(error))) .catch(error => reject(error)) }) +} + +function addUserTag(floID, tag, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + user: floID, + tag: tag, + timestamp: Date.now() + }; + request.sign = signRequest({ + command: "add_Tag", + user: request.user, + tag: request.tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch('/add-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) +} + +function removeUserTag(floID, tag, proxySecret) { + return new Promise((resolve, reject) => { + let request = { + user: floID, + tag: tag, + timestamp: Date.now() + }; + request.sign = signRequest({ + command: "remove_Tag", + user: request.user, + tag: request.tag, + timestamp: request.timestamp + }, proxySecret); + console.debug(request); + + fetch('/remove-tag', { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(request) + }).then(result => responseParse(result, false) + .then(result => resolve(result)) + .catch(error => reject(error))) + .catch(error => reject(error)) + }) } \ No newline at end of file diff --git a/setup/configure-settings.js b/setup/configure-settings.js index 17f57f1..4d7d0a5 100644 --- a/setup/configure-settings.js +++ b/setup/configure-settings.js @@ -16,7 +16,9 @@ try { "sql_host": "localhost", "backup-port": "8081", - "backup-floIDs": [] + "backup-floIDs": [], + + "trusted-floIDs": [] }; flag_new = true; } @@ -32,14 +34,14 @@ function flaggedYesOrNo(text) { }) } -function getBackupIDs(ids) { +function get_IDs(ids) { return new Promise((resolve, reject) => { - getInput("", "continue").then(id => { + getInput.Text("", "continue").then(id => { if (id === "continue") resolve(Array.from(new Set(ids))); else { ids.push(id); - getBackupIDs(ids) + get_IDs(ids) .then(result => resolve(result)) .catch(error => reject(error)); } @@ -55,8 +57,8 @@ function configureBackup() { return resolve(true); getInput.YesOrNo('Do you want to add/remove backup floIDs?').then(value => { if (value) { - console("Enter floIDs to add as backup: "); - getBackupIDs(config["backup-floIDs"]).then(ids => { + console.log("Enter floIDs to add as backup: "); + get_IDs(config["backup-floIDs"]).then(ids => { //delete backup IDs let tmp_obj = {}; for (let i in ids) { @@ -84,12 +86,45 @@ function configureBackup() { }) } +function configureTrustedIDs() { + return new Promise((resolve, reject) => { + getInput.YesOrNo('Do you want to add/remove trusted floIDs?').then(value => { + if (value) { + console.log("Enter floIDs to add as trusted: "); + get_IDs(config["trusted-floIDs"]).then(ids => { + //delete trusted IDs + let tmp_obj = {}; + for (let i in ids) { + console.log(i + 1, ":", ids[i]); + tmp_obj[i + 1] = ids[i]; + } + getInput.Text("Enter numbers to delete (seperated by comma)", "continue").then(ri => { + if (ri === "continue") + config["trusted-floIDs"] = ids; + else { + for (let i of ri.split(",")) + delete tmp_obj[parseInt(i)]; + let tmp_array = []; + for (let id of tmp_obj) + tmp_array.push(id); + config["trusted-floIDs"] = tmp_array; + } + resolve(true); + }) + }) + } else + resolve(true); + }) + }) +} + function configurePort() { return new Promise(resolve => { getInput.Text('Enter port', config["port"]).then(port => { config["port"] = port; configureBackup() - .then(result => resolve(true)) + .then(_ => configureTrustedIDs() + .then(_ => resolve(true))); }) }) } diff --git a/src/app.js b/src/app.js index 57fe489..8f2be44 100644 --- a/src/app.js +++ b/src/app.js @@ -5,7 +5,7 @@ const Request = require('./request'); const REFRESH_INTERVAL = 60 * 1000; //1 min -module.exports = function App(secret, DB) { +module.exports = function App(secret, trustedIDs, DB) { const app = express(); //session middleware @@ -63,6 +63,11 @@ module.exports = function App(secret, DB) { app.post('/deposit-rupee', Request.DepositRupee); app.post('/withdraw-rupee', Request.WithdrawRupee); + //Manage user tags (Access to trusted IDs only) + Request.trustedIDs = trustedIDs; + app.post('/add-tag', Request.addUserTag); + app.post('/remove-tag', Request.removeUserTag); + Request.DB = DB; Request.periodicProcess(); let refresher = setInterval(Request.periodicProcess, REFRESH_INTERVAL); diff --git a/src/group.js b/src/group.js index 859007e..be09158 100644 --- a/src/group.js +++ b/src/group.js @@ -6,13 +6,21 @@ function addTag(floID, tag) { .then(result => resolve(`Added ${floID} to ${tag}`)) .catch(error => { if (error.code === "ER_DUP_ENTRY") - reject(`${floID} already in ${tag}`); + reject(INVALID(`${floID} already in ${tag}`)); else reject(error); }); }); } +function removeTag(floID, tag) { + return new Promise((resolve, reject) => { + DB.query("DELETE FROM Tags WHERE floID=? AND tag=?", [floID, tag]) + .then(result => resolve(`Removed ${floID} from ${tag}`)) + .catch(error => reject(error)); + }) +} + function getBestPairs(currentRate) { return new Promise((resolve, reject) => { DB.query("SELECT tag, sellPriority, buyPriority FROM TagList").then(result => { @@ -331,6 +339,7 @@ function verifyBuyOrder(buyOrder, cur_price) { module.exports = { addTag, + removeTag, getBestPairs, set DB(db) { DB = db; diff --git a/src/main.js b/src/main.js index 431357e..ba5005a 100644 --- a/src/main.js +++ b/src/main.js @@ -36,7 +36,7 @@ module.exports = function startServer(public_dir) { console.debug(PUBLIC_DIR, global.myFloID); Database(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(DB => { - const app = App(config['secret'], DB); + const app = App(config['secret'], config['trusted-floIDs'], DB); app.listen(PORT, () => console.log(`Server Running at port ${PORT}`)); //start backup if (config["backup-port"] && config["backup-floIDs"].length) { diff --git a/src/market.js b/src/market.js index ffcbf97..eb40b4c 100644 --- a/src/market.js +++ b/src/market.js @@ -632,6 +632,7 @@ module.exports = { depositRupee, withdrawRupee, periodicProcess, + group, set DB(db) { DB = db; group.DB = db; diff --git a/src/request.js b/src/request.js index 9acb65c..7507038 100644 --- a/src/request.js +++ b/src/request.js @@ -1,5 +1,5 @@ const market = require("./market"); -var DB; //container for database +var DB, trustedIDs; //container for database global.INVALID = function(message) { if (!(this instanceof INVALID)) @@ -276,8 +276,11 @@ function Account(req, res) { setLogin("Session Expired! Re-login required"); else { let floID = req.session.user_id; - market.getAccountDetails(floID) - .then(result => res.send(result)); + market.getAccountDetails(floID).then(result => { + if (trustedIDs.includes(floID)) + result.subAdmin = true; + res.send(result) + }); } }).catch(_ => res.status(INTERNAL.e_code).send("Try again later!")); } @@ -407,6 +410,75 @@ function WithdrawRupee(req, res) { }); } +function addUserTag(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + else if (!trustedIDs.includes(session.user_id)) + return res.status(INVALID.e_code).send("Access Denied"); + validateRequestFromFloID({ + command: "add_Tag", + user: data.user, + tag: data.tag, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.group.addTag(data.user, data.tag).then(result => { + storeRequest(session.user_id, req_str, data.sign); + res.send(result); + }).catch(error => { + if (error instanceof INVALID) + res.status(INVALID.e_code).send(error.message); + else { + console.error(error); + res.status(INTERNAL.e_code).send("Request processing failed! Try again later!"); + } + }); + }).catch(error => { + if (error instanceof INVALID) + res.status(INVALID.e_code).send(error.message); + else { + console.error(error); + res.status(INTERNAL.e_code).send("Request processing failed! Try again later!"); + } + }); +} + +function removeUserTag(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + else if (!trustedIDs.includes(session.user_id)) + return res.status(INVALID.e_code).send("Access Denied"); + else + validateRequestFromFloID({ + command: "remove_Tag", + user: data.user, + tag: data.tag, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.group.removeTag(data.user, data.tag).then(result => { + storeRequest(session.user_id, req_str, data.sign); + res.send(result); + }).catch(error => { + if (error instanceof INVALID) + res.status(INVALID.e_code).send(error.message); + else { + console.error(error); + res.status(INTERNAL.e_code).send("Request processing failed! Try again later!"); + } + }); + }).catch(error => { + if (error instanceof INVALID) + res.status(INVALID.e_code).send(error.message); + else { + console.error(error); + res.status(INTERNAL.e_code).send("Request processing failed! Try again later!"); + } + }); +} + module.exports = { SignUp, Login, @@ -424,6 +496,11 @@ module.exports = { DepositRupee, WithdrawRupee, periodicProcess: market.periodicProcess, + addUserTag, + removeUserTag, + set trustedIDs(ids) { + trustedIDs = ids; + }, set DB(db) { DB = db; market.DB = db; From a7c96e9ce255f1fae74afd8ee4287d3cc9650710 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 25 Nov 2021 05:10:39 +0530 Subject: [PATCH 9/9] Priority case: FLO brought from outside FLO brought from outside the source will be processed after those brought from source-market. --- src/group.js | 102 ++++++++++++++++++++++++++++++-------------------- src/market.js | 94 +++++++++++++++++++++------------------------- 2 files changed, 105 insertions(+), 91 deletions(-) diff --git a/src/group.js b/src/group.js index be09158..f6da65f 100644 --- a/src/group.js +++ b/src/group.js @@ -43,25 +43,35 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { Promise.all([getBuyOrder(), getSellOrder()]).then(results => { resolve({ buyOrder: results[0], - sellOrder: results[1] + sellOrder: results[1], + null_base: getSellOrder.cache.mode_null }) }).catch(error => reject(error)) }); - this.next = () => { + this.next = (tx_quantity, incomplete_sell, flag_sell) => { let buy = getBuyOrder.cache, sell = getSellOrder.cache; if (buy.cur_order && sell.cur_order) { - if (buy.cur_order.quantity > sell.cur_order.quantity) { - buy.cur_order.quantity -= sell.cur_order.quantity; - sell.cur_order = null; - } else if (buy.cur_order.quantity < sell.cur_order.quantity) { - sell.cur_order.quantity -= buy.cur_order.quantity; + //buy order + if (tx_quantity < buy.cur_order.quantity) + buy.cur_order.quantity -= tx_quantity; + else if (tx_quantity == buy.cur_order.quantity) buy.cur_order = null; - } else { + else + throw Error("Tx quantity cannot be more than order quantity"); + //sell order + if (tx_quantity < sell.cur_order.quantity) { + sell.cur_order.quantity -= tx_quantity; + if (incomplete_sell) { + if (!sell.mode_null && flag_sell) + sell.null_queue.push(sell.cur_order); + sell.cur_order = null; + } + } else if (tx_quantity == sell.cur_order.quantity) sell.cur_order = null; - buy.cur_order = null; - } + else + throw Error("Tx quantity cannot be more than order quantity"); } else throw Error("No current order found"); }; @@ -69,7 +79,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { const getSellOrder = () => new Promise((resolve, reject) => { let cache = getSellOrder.cache; if (cache.cur_order) { //If cache already has a pending order - verifySellOrder(cache.cur_order, currentRate).then(result => { + verifySellOrder(cache.cur_order, currentRate, cache.mode_null).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -82,7 +92,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { .catch(error => reject(error)) }) } else if (cache.orders && cache.orders.length) { //If cache already has orders in priority - getTopValidSellOrder(cache.orders, currentRate).then(result => { + getTopValidSellOrder(cache.orders, currentRate, cache.mode_null).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -111,11 +121,20 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { .then(result => resolve(result)) .catch(error => reject(error)) }).catch(error => reject(error)); + } else if (!mode_null) { //Lowest priority Coins (FLO Brought from other sources) + cache.orders = cache.null_queue.reverse(); //Reverse it so that we can pop the highest priority + cache.mode_null = true; + cache.null_queue = null; + getSellOrder() + .then(result => resolve(result)) + .catch(error => reject(error)) } else reject(false); }); getSellOrder.cache = { - tags: tags_sell + tags: tags_sell, + null_queue: [], + mode_null: false }; const getBuyOrder = () => new Promise((resolve, reject) => { @@ -258,53 +277,56 @@ function fetch_api(api, id) { }) } -function getTopValidSellOrder(orders, cur_price) { +function getTopValidSellOrder(orders, cur_price, mode_null) { return new Promise((resolve, reject) => { if (!orders.length) return reject(false) - verifySellOrder(orders.pop(), cur_price) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + verifySellOrder(orders.pop(), cur_price, mode_null) //pop: as the orders are sorted in ascending (highest point should be checked 1st) .then(result => resolve(result)) .catch(error => { if (error !== false) return reject(error); - getTopValidSellOrder(orders, cur_price) + getTopValidSellOrder(orders, cur_price, mode_null) .then(result => resolve(result)) .catch(error => reject(error)); }); }); } -function verifySellOrder(sellOrder, cur_price) { +function verifySellOrder(sellOrder, cur_price, mode_null) { return new Promise((resolve, reject) => { - DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { - let rem = sellOrder.quantity, - sell_base = 0, - base_quantity = 0; - for (let i = 0; i < result.length && rem > 0; i++) { - if (rem < result[i].quantity) { - if (result[i].base) { + if (!mode_null) + DB.query("SELECT quantity, base FROM Vault WHERE floID=? AND base IS NOT NULL ORDER BY base", [sellOrder.floID]).then(result => { + let rem = sellOrder.quantity, + sell_base = 0, + base_quantity = 0; + for (let i = 0; i < result.length && rem > 0; i++) { + if (rem < result[i].quantity) { sell_base += (rem * result[i].base); base_quantity += rem; - } - rem = 0; - } else { - if (result[i].base) { + rem = 0; + } else { sell_base += (result[i].quantity * result[i].base); base_quantity += result[i].quantity; + rem -= 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`); - reject(false); - } else - resolve(sellOrder); - }).catch(error => reject(error)); + if (base_quantity) + sell_base = sell_base / base_quantity; + if (sell_base > cur_price) + reject(false); + else + resolve(sellOrder); + }).catch(error => reject(error)); + else if (mode_null) + DB.query("SELECT SUM(quantity) as total FROM Vault WHERE floID=?", [sellOrder.floID]).then(result => { + if (result.total < sellOrder.quantity) + console.warn(`Sell Order ${sellOrder.id} was made without enough FLO. This should not happen`); + if (result.total > 0) + resolve(sellOrder); + else + reject(false); + }).catch(error => reject(error)) }) } diff --git a/src/market.js b/src/market.js index eb40b4c..3044b70 100644 --- a/src/market.js +++ b/src/market.js @@ -177,24 +177,20 @@ function initiateCoupling() { } function processCoupling(bestPairQueue) { - bestPairQueue.get().then(result => { - let buyer_best = result.buyOrder, - seller_best = result.sellOrder; + bestPairQueue.get().then(pair_result => { + let buyer_best = pair_result.buyOrder, + seller_best = pair_result.sellOrder; console.debug("Sell:", seller_best); console.debug("Buy:", buyer_best); - spendFLO(buyer_best, seller_best).then(txQueries => { - //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); + spendFLO(buyer_best, seller_best, pair_result.null_base).then(spend_result => { + let tx_quantity = spend_result.quantity, + txQueries = spend_result.txQueries, + clear_sell = spend_result.incomplete && !spend_result.flag_baseNull; //clear_sell can be true only if an order is placed without enough FLO + processOrders(seller_best, buyer_best, txQueries, tx_quantity, clear_sell); updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.cur_rate, tx_quantity); //process txn query in SQL - DB.transaction(txQueries).then(results => { - bestPairQueue.next(); + DB.transaction(txQueries).then(_ => { + bestPairQueue.next(quantity, spend_result.incomplete, spend_result.flag_baseNull); console.log(`Transaction was successful! BuyOrder:${buyer_best.id}| SellOrder:${seller_best.id}`); //Since a tx was successful, match again processCoupling(bestPairQueue); @@ -208,49 +204,46 @@ function processCoupling(bestPairQueue) { }); } -function spendFLO(buyOrder, sellOrder) { +function spendFLO(buyOrder, sellOrder, null_base) { return new Promise((resolve, reject) => { DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY base", [sellOrder.floID]).then(result => { let rem = Math.min(buyOrder.quantity, sellOrder.quantity), - 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]]); - rem = 0; - } else { - txQueries.push(["DELETE FROM Vault WHERE id=?", [result[i].id]]); - rem -= result[i].quantity; - } - } - if (rem > 0) - reject(`Sell order ${sellOrder.id} is active, but FLO is insufficient`); - else - resolve(txQueries); + txQueries = [] + flag_baseNull = false; + for (let i = 0; i < result.length && rem > 0; i++) + if (result[i].base || null_base) { + if (rem < result[i].quantity) { + txQueries.push(["UPDATE Vault SET quantity=quantity-? WHERE id=?", [rem, result[i].id]]); + rem = 0; + } else { + txQueries.push(["DELETE FROM Vault WHERE id=?", [result[i].id]]); + rem -= result[i].quantity; + } + } else + flag_baseNull = true; + resolve({ + quantity: Math.min(buyOrder.quantity, sellOrder.quantity) - rem, + txQueries, + incomplete: rem > 0, + flag_baseNull + }); }).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 processOrders(seller_best, buyer_best, txQueries, quantity, clear_sell) { + 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 || clear_sell) + txQueries.push(["DELETE FROM SellOrder WHERE id=?", [seller_best.id]]); + else + txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]); } function updateBalance(seller_best, buyer_best, txQueries, cur_price, quantity) { @@ -262,7 +255,6 @@ function updateBalance(seller_best, buyer_best, txQueries, cur_price, quantity) 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) {