From 92caef2c37f2318b37254324092d593485f86431 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Fri, 4 Feb 2022 02:28:02 +0530 Subject: [PATCH] Multi-asset: coupling process --- args/schema.sql | 3 ++ src/coupling.js | 78 ++++++++++++++++++++++++++++--------------------- src/group.js | 67 +++++++++++++++++++++++------------------- src/market.js | 7 +++-- 4 files changed, 89 insertions(+), 66 deletions(-) diff --git a/args/schema.sql b/args/schema.sql index bc68aa4..296c3b4 100644 --- a/args/schema.sql +++ b/args/schema.sql @@ -47,6 +47,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID) CREATE TABLE SellOrder ( id INT NOT NULL AUTO_INCREMENT, floID CHAR(34) NOT NULL, +asset VARCHAR(64) NOT NULL, quantity FLOAT NOT NULL, minPrice DECIMAL(10, 2), time_placed DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -57,6 +58,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID) CREATE TABLE BuyOrder ( id INT NOT NULL AUTO_INCREMENT, floID CHAR(34) NOT NULL, +asset VARCHAR(64) NOT NULL, quantity FLOAT NOT NULL, maxPrice DECIMAL(10, 2) NOT NULL, time_placed DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -67,6 +69,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID) CREATE TABLE Transactions ( seller CHAR(34) NOT NULL, buyer CHAR(34) NOT NULL, +asset VARCHAR(64) NOT NULL, quantity FLOAT NOT NULL, unitValue DECIMAL(10, 2) NOT NULL, tx_time DATETIME DEFAULT CURRENT_TIMESTAMP, diff --git a/src/coupling.js b/src/coupling.js index 72099bd..8f5f63d 100644 --- a/src/coupling.js +++ b/src/coupling.js @@ -3,11 +3,16 @@ const group = require("./group"); const price = require("./price"); -var DB; //container for database +var DB, assetList; //container for database and assetList function initiate() { - price.getRates().then(cur_rate => { - group.getBestPairs(cur_rate) + for (let asset in assetList) + startCouplingForAsset(asset); +} + +function startCouplingForAsset(asset) { + price.getRates(asset).then(cur_rate => { + group.getBestPairs(asset, cur_rate) .then(bestPairQueue => processCoupling(bestPairQueue)) .catch(error => console.error("initiateCoupling", error)) }).catch(error => console.error(error)); @@ -19,14 +24,14 @@ function processCoupling(bestPairQueue) { seller_best = pair_result.sellOrder; console.debug("Sell:", seller_best); console.debug("Buy:", buyer_best); - spendFLO(buyer_best, seller_best, pair_result.null_base).then(spend_result => { + spendAsset(bestPairQueue.asset, 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 + clear_sell = spend_result.incomplete && !spend_result.flag_baseNull; //clear_sell can be true only if an order is placed without enough ASSET processOrders(seller_best, buyer_best, txQueries, tx_quantity, clear_sell); - updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.cur_rate, tx_quantity); + updateBalance(seller_best, buyer_best, txQueries, bestPairQueue.asset, bestPairQueue.cur_rate, tx_quantity); //begin audit - beginAudit(seller_best.floID, buyer_best.floID, bestPairQueue.cur_rate, tx_quantity).then(audit => { + beginAudit(seller_best.floID, buyer_best.floID, bestPairQueue.asset, bestPairQueue.cur_rate, tx_quantity).then(audit => { //process txn query in SQL DB.transaction(txQueries).then(_ => { bestPairQueue.next(tx_quantity, spend_result.incomplete, spend_result.flag_baseNull); @@ -58,13 +63,13 @@ function processCoupling(bestPairQueue) { console.log("No valid sellOrders."); noSell = true; } - price.noOrder(noBuy, noSell); + price.noOrder(bestPairQueue.asset, noBuy, noSell); }); } -function spendFLO(buyOrder, sellOrder, null_base) { +function spendAsset(asset, 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 => { + DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? AND asset=? ORDER BY base", [sellOrder.floID, asset]).then(result => { let rem = Math.min(buyOrder.quantity, sellOrder.quantity), txQueries = [], flag_baseNull = false; @@ -104,49 +109,50 @@ function processOrders(seller_best, buyer_best, txQueries, quantity, clear_sell) txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]); } -function updateBalance(seller_best, buyer_best, txQueries, cur_price, quantity) { - //Update rupee balance for seller and buyer +function updateBalance(seller_best, buyer_best, txQueries, asset, cur_price, quantity) { + //Update cash balance for seller and buyer let totalAmount = cur_price * quantity; - txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance+? WHERE floID=?", [totalAmount, seller_best.floID]]); - txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance-? WHERE floID=?", [totalAmount, buyer_best.floID]]); + txQueries.push(["UPDATE Cash SET balance=balance+? WHERE floID=?", [totalAmount, seller_best.floID]]); + txQueries.push(["UPDATE Cash SET balance=balance-? 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]]) + txQueries.push(["INSERT INTO Vault(floID, asset, base, quantity) VALUES (?, ?, ?, ?)", [buyer_best.floID, asset, cur_price, quantity]]) //Record transaction - txQueries.push(["INSERT INTO Transactions (seller, buyer, quantity, unitValue) VALUES (?, ?, ?, ?)", [seller_best.floID, buyer_best.floID, quantity, cur_price]]); + txQueries.push(["INSERT INTO Transactions (seller, buyer, asset, quantity, unitValue) VALUES (?, ?, ?, ?, ?)", [seller_best.floID, buyer_best.floID, asset, quantity, cur_price]]); } -function beginAudit(sellerID, buyerID, unit_price, quantity) { +function beginAudit(sellerID, buyerID, asset, unit_price, quantity) { return new Promise((resolve, reject) => { - auditBalance(sellerID, buyerID).then(old_bal => resolve({ - end: () => endAudit(sellerID, buyerID, old_bal, unit_price, quantity) + 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, old_bal, unit_price, quantity) { - auditBalance(sellerID, buyerID).then(new_bal => { - DB.query("INSERT INTO auditTransaction (sellerID, buyerID, quantity, unit_price, total_cost, " + - " Rupee_seller_old, Rupee_seller_new, Rupee_buyer_old, Rupee_buyer_new," + - " FLO_seller_old, FLO_seller_new, FLO_buyer_old, FLO_buyer_new) " + - " Value (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [sellerID, buyerID, quantity, unit_price, quantity * unit_price, - old_bal[sellerID].Rupee, new_bal[sellerID].Rupee, old_bal[buyerID].Rupee, new_bal[buyerID].Rupee, - old_bal[sellerID].FLO, new_bal[sellerID].FLO, old_bal[buyerID].FLO, new_bal[buyerID].FLO, +function endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity) { + auditBalance(sellerID, buyerID, asset).then(new_bal => { + DB.query("INSERT INTO auditTransaction (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) { +function auditBalance(sellerID, buyerID, asset) { return new Promise((resolve, reject) => { let balance = { [sellerID]: {}, [buyerID]: {} }; - DB.query("SELECT floID, rupeeBalance FROM Cash WHERE floID IN (?, ?)", [sellerID, buyerID]).then(result => { + DB.query("SELECT floID, balance FROM Cash WHERE floID IN (?, ?)", [sellerID, buyerID]).then(result => { for (let i in result) - balance[result[i].floID].Rupee = result[i].rupeeBalance; - DB.query("SELECT floID, SUM(quantity) as floBal FROM Vault WHERE floID IN (?, ?) GROUP BY floID", [sellerID, buyerID]).then(result => { + balance[result[i].floID].cash = result[i].balance; + DB.query("SELECT floID, SUM(quantity) as asset_balance FROM Vault WHERE asset=? AND floID IN (?, ?) GROUP BY floID", [asset, sellerID, buyerID]).then(result => { for (let i in result) - balance[result[i].floID].FLO = result[i].floBal; + balance[result[i].floID].asset = result[i].asset_balance; resolve(balance); }).catch(error => reject(error)) }).catch(error => reject(error)) @@ -155,11 +161,17 @@ function auditBalance(sellerID, buyerID) { module.exports = { initiate, - group, + group: { + addTag: group.addTag, + removeTag: group.removeTag + }, price, set DB(db) { DB = db; group.DB = db; price.DB = db; + }, + set assetList(list) { + assetList = list; } } \ No newline at end of file diff --git a/src/group.js b/src/group.js index 03d402b..d89f071 100644 --- a/src/group.js +++ b/src/group.js @@ -25,22 +25,25 @@ function removeTag(floID, tag) { }) } -function getBestPairs(currentRate) { +function getBestPairs(asset, cur_rate) { 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 ? 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)); + resolve(new bestPair(asset, cur_rate, tags_buy, tags_sell)); }).catch(error => reject(error)) }) } -const bestPair = function(cur_rate, tags_buy, tags_sell) { - const currentRate = cur_rate; +const bestPair = function(asset, cur_rate, tags_buy, tags_sell) { + + Object.defineProperty(this, 'asset', { + get: () => asset + }); Object.defineProperty(this, 'cur_rate', { - get: () => currentRate + get: () => cur_rate, }); this.get = () => new Promise((resolve, reject) => { @@ -89,7 +92,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, cache.mode_null).then(result => { + verifySellOrder(cache.cur_order, asset, cur_rate, cache.mode_null).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -102,7 +105,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, cache.mode_null).then(result => { + getTopValidSellOrder(cache.orders, asset, cur_rate, cache.mode_null).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -116,14 +119,14 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { }) } else if (cache.tags.length) { //If cache has remaining tags cache.cur_tag = cache.tags.pop(); - getSellOrdersInTag(cache.cur_tag, currentRate).then(orders => { + getSellOrdersInTag(cache.cur_tag, asset, cur_rate).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 => { + getUntaggedSellOrders(asset, cur_rate).then(orders => { cache.orders = orders; cache.cur_tag = null; cache.end = true; @@ -150,7 +153,7 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { 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 => { + verifyBuyOrder(cache.cur_order, cur_rate).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -163,7 +166,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 - getTopValidBuyOrder(cache.orders, currentRate).then(result => { + getTopValidBuyOrder(cache.orders, cur_rate).then(result => { cache.cur_order = result; resolve(result); }).catch(error => { @@ -177,14 +180,14 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { }) } else if (cache.tags.length) { //If cache has remaining tags cache.cur_tag = cache.tags.pop(); - getBuyOrdersInTag(cache.cur_tag, currentRate).then(orders => { + getBuyOrdersInTag(cache.cur_tag, asset, cur_rate).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 => { + getUntaggedBuyOrders(asset, cur_rate).then(orders => { cache.orders = orders; cache.cur_tag = null; cache.end = true; @@ -200,31 +203,34 @@ const bestPair = function(cur_rate, tags_buy, tags_sell) { }; } -function getUntaggedSellOrders(cur_price) { +function getUntaggedSellOrders(asset, 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 DESC", [cur_price]) + " WHERE Tags.floID IS NULL AND SellOrder.asset = ? AND SellOrder.minPrice <=?" + + " ORDER BY SellOrder.time_placed DESC", [asset, cur_price]) .then(orders => resolve(orders)) .catch(error => reject(error)) }) } -function getUntaggedBuyOrders(cur_price) { +function getUntaggedBuyOrders(asset, 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 DESC", [cur_price]) + " WHERE Tags.floID IS NULL AND BuyOrder.asset = ? AND BuyOrder.maxPrice >=? " + + " ORDER BY BuyOrder.time_placed DESC", [asset, cur_price]) .then(orders => resolve(orders)) .catch(error => reject(error)) }) } -function getSellOrdersInTag(tag, cur_price) { +function getSellOrdersInTag(tag, asset, 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 DESC", [tag, cur_price]).then(orders => { + " WHERE Tags.tag = ? AND SellOrder.asset = ? AND SellOrder.minPrice <=?" + + " ORDER BY SellOrder.time_placed DESC", [tag, asset, cur_price]).then(orders => { if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. resolve(orders); else @@ -238,11 +244,12 @@ function getSellOrdersInTag(tag, cur_price) { }); } -function getBuyOrdersInTag(tag, cur_price) { +function getBuyOrdersInTag(tag, asset, 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 DESC", [tag, cur_price]).then(orders => { + " WHERE Tags.tag = ? AND BuyOrder.asset = ? AND BuyOrder.maxPrice >=?" + + " ORDER BY BuyOrder.time_placed DESC", [tag, asset, cur_price]).then(orders => { if (orders.length <= 1) // No (or) Only-one order, hence priority sort not required. resolve(orders); else @@ -287,26 +294,26 @@ function fetch_api(api, id) { }) } -function getTopValidSellOrder(orders, cur_price, mode_null) { +function getTopValidSellOrder(orders, asset, cur_price, mode_null) { return new Promise((resolve, reject) => { if (!orders.length) return reject(false) - verifySellOrder(orders.pop(), cur_price, mode_null) //pop: as the orders are sorted in ascending (highest point should be checked 1st) + verifySellOrder(orders.pop(), asset, 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, mode_null) + getTopValidSellOrder(orders, asset, cur_price, mode_null) .then(result => resolve(result)) .catch(error => reject(error)); }); }); } -function verifySellOrder(sellOrder, cur_price, mode_null) { +function verifySellOrder(sellOrder, asset, cur_price, mode_null) { return new Promise((resolve, reject) => { if (!mode_null) - DB.query("SELECT quantity, base FROM Vault WHERE floID=? AND base IS NOT NULL ORDER BY base", [sellOrder.floID]).then(result => { + DB.query("SELECT quantity, base FROM Vault WHERE floID=? AND asset=? AND base IS NOT NULL ORDER BY base", [sellOrder.floID, asset]).then(result => { let rem = sellOrder.quantity, sell_base = 0, base_quantity = 0; @@ -329,7 +336,7 @@ function verifySellOrder(sellOrder, cur_price, mode_null) { 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 => { + DB.query("SELECT SUM(quantity) as total FROM Vault WHERE floID=? AND asset=?", [sellOrder.floID, asset]).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) @@ -358,10 +365,10 @@ function getTopValidBuyOrder(orders, cur_price) { function verifyBuyOrder(buyOrder, cur_price) { return new Promise((resolve, reject) => { - DB.query("SELECT rupeeBalance AS bal FROM Cash WHERE floID=?", [buyOrder.floID]).then(result => { + DB.query("SELECT balance 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`); + //This should not happen unless a buy order is placed when user doesnt have enough cash balance + console.warn(`Buy order ${buyOrder.id} is active, but Cash is insufficient`); reject(false); } else resolve(buyOrder); diff --git a/src/market.js b/src/market.js index ba4a3f1..132a5b4 100644 --- a/src/market.js +++ b/src/market.js @@ -67,7 +67,7 @@ function addSellOrder(floID, asset, quantity, min_price) { return reject(INVALID(`Invalid asset (${asset})`)); getAssetBalance.check(floID, asset, quantity).then(_ => { checkSellRequirement(floID).then(_ => { - DB.query("INSERT INTO SellOrder(floID, quantity, minPrice) VALUES (?, ?, ?)", [floID, quantity, min_price]) + DB.query("INSERT INTO SellOrder(floID, asset, quantity, minPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, min_price]) .then(result => resolve("Added SellOrder to DB")) .catch(error => reject(error)); }).catch(error => reject(error)) @@ -100,7 +100,7 @@ function addBuyOrder(floID, asset, quantity, max_price) { else if (!allowedAssets.includes(asset)) return reject(INVALID(`Invalid asset (${asset})`)); getAssetBalance.check(floID, asset, quantity).then(_ => { - DB.query("INSERT INTO BuyOrder(floID, quantity, maxPrice) VALUES (?, ?, ?)", [floID, quantity, max_price]) + DB.query("INSERT INTO BuyOrder(floID, asset, quantity, maxPrice) VALUES (?, ?, ?, ?)", [floID, asset, quantity, max_price]) .then(result => resolve("Added BuyOrder to DB")) .catch(error => reject(error)); }).catch(error => reject(error)); @@ -462,6 +462,7 @@ module.exports = { coupling.DB = db; }, set allowedAssets(assets) { - allowedAssets = assets; + allowedAssets = Object.keys(assets); + coupling.assetList = assets; } }; \ No newline at end of file