From 67c31d79a91734e3ca17b688063c9070baf4764e Mon Sep 17 00:00:00 2001 From: sairajzero Date: Wed, 2 Feb 2022 04:17:47 +0530 Subject: [PATCH] Multiple asset support 2 types of property: 1. Cash - main/central currency used for trading 2. Asset - (FLO/tokens) Can be brought or sold in the exchange market . - Allow multiple tokens to act as asset in addition to FLO. - Changes required for the above in deposits, withdraws, placing orders - some code optimization in market.js - Updated SQL schema --- args/schema.sql | 50 ++++--- args/truncateAll.sql | 4 +- public/floGlobals.js | 2 +- src/app.js | 4 +- src/market.js | 323 +++++++++++++++++++++---------------------- src/request.js | 23 +-- src/tokenAPI.js | 4 +- 7 files changed, 206 insertions(+), 204 deletions(-) diff --git a/args/schema.sql b/args/schema.sql index 9945813..bc68aa4 100644 --- a/args/schema.sql +++ b/args/schema.sql @@ -28,7 +28,7 @@ FOREIGN KEY (floID) REFERENCES Users(floID) CREATE TABLE Cash ( id INT NOT NULL AUTO_INCREMENT, floID CHAR(34) NOT NULL UNIQUE, -rupeeBalance DECIMAL(12, 2) DEFAULT 0.00, +balance DECIMAL(12, 2) DEFAULT 0.00, PRIMARY KEY(id), FOREIGN KEY (floID) REFERENCES Users(floID) ); @@ -37,6 +37,7 @@ CREATE TABLE Vault ( id INT NOT NULL AUTO_INCREMENT, floID CHAR(34) NOT NULL, locktime DATETIME DEFAULT CURRENT_TIMESTAMP, +asset VARCHAR(64) NOT NULL, base DECIMAL(10, 2), quantity FLOAT NOT NULL, PRIMARY KEY(id), @@ -93,20 +94,22 @@ PRIMARY KEY(id), FOREIGN KEY (floID) REFERENCES Users(floID) ); -CREATE TABLE inputRupee ( +CREATE TABLE inputToken ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128) NOT NULL, floID CHAR(34) NOT NULL, +token VARCHAR(64), amount FLOAT, status VARCHAR(50) NOT NULL, PRIMARY KEY(id), FOREIGN KEY (floID) REFERENCES Users(floID) ); -CREATE TABLE outputRupee ( +CREATE TABLE outputToken ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128), floID CHAR(34) NOT NULL, +token VARCHAR(64), amount FLOAT NOT NULL, status VARCHAR(50) NOT NULL, PRIMARY KEY(id), @@ -159,16 +162,17 @@ rec_time DATETIME DEFAULT CURRENT_TIMESTAMP, unit_price FLOAT NOT NULL, quantity FLOAT NOT NULL, total_cost FLOAT NOT NULL, +asset VARCHAR(64) NOT NULL, sellerID CHAR(34) NOT NULL, -FLO_seller_old FLOAT NOT NULL, -FLO_seller_new FLOAT NOT NULL, -Rupee_seller_old FLOAT NOT NULL, -Rupee_seller_new FLOAT NOT NULL, +seller_old_asset FLOAT NOT NULL, +seller_new_asset FLOAT NOT NULL, +seller_old_cash FLOAT NOT NULL, +seller_new_cash FLOAT NOT NULL, buyerID CHAR(34) NOT NULL, -FLO_buyer_old FLOAT NOT NULL, -FLO_buyer_new FLOAT NOT NULL, -Rupee_buyer_old FLOAT NOT NULL, -Rupee_buyer_new FLOAT NOT NULL, +buyer_old_asset FLOAT NOT NULL, +buyer_new_asset FLOAT NOT NULL, +buyer_old_cash FLOAT NOT NULL, +buyer_new_cash FLOAT NOT NULL, FOREIGN KEY (sellerID) REFERENCES Users(floID), FOREIGN KEY (buyerID) REFERENCES Users(floID) ); @@ -239,19 +243,19 @@ FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputFLO', NEW.id) ON DU CREATE TRIGGER outputFLO_D AFTER DELETE ON outputFLO FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputFLO', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; -CREATE TRIGGER inputRupee_I AFTER INSERT ON inputRupee -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputRupee', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER inputRupee_U AFTER UPDATE ON inputRupee -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputRupee', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER inputRupee_D AFTER DELETE ON inputRupee -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputRupee', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; +CREATE TRIGGER inputToken_I AFTER INSERT ON inputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER inputToken_U AFTER UPDATE ON inputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER inputToken_D AFTER DELETE ON inputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('inputToken', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; -CREATE TRIGGER outputRupee_I AFTER INSERT 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_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; +CREATE TRIGGER outputToken_I AFTER INSERT ON outputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER outputToken_U AFTER UPDATE ON outputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER outputToken_D AFTER DELETE ON outputToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('outputToken', 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; diff --git a/args/truncateAll.sql b/args/truncateAll.sql index da0d4a6..b404da9 100644 --- a/args/truncateAll.sql +++ b/args/truncateAll.sql @@ -4,9 +4,9 @@ TRUNCATE auditTransaction; TRUNCATE BuyOrder; TRUNCATE Cash; TRUNCATE inputFLO; -TRUNCATE inputRupee; +TRUNCATE inputToken; TRUNCATE outputFLO; -TRUNCATE outputRupee; +TRUNCATE outputToken; TRUNCATE priceHistory; TRUNCATE Request_Log; TRUNCATE SellOrder; diff --git a/public/floGlobals.js b/public/floGlobals.js index 1153a3c..5e04811 100644 --- a/public/floGlobals.js +++ b/public/floGlobals.js @@ -13,7 +13,7 @@ const floGlobals = { sendAmt: 0.001, fee: 0.0005, tokenURL: "https://ranchimallflo.duckdns.org/", - token: "rupee" + currency: "rupee" }; (typeof global !== "undefined" ? global : window).cryptocoin = floGlobals.blockchain; diff --git a/src/app.js b/src/app.js index 590c03d..385580e 100644 --- a/src/app.js +++ b/src/app.js @@ -80,8 +80,8 @@ module.exports = function App(secret, DB) { //withdraw and deposit request app.post('/deposit-flo', Request.DepositFLO); app.post('/withdraw-flo', Request.WithdrawFLO); - app.post('/deposit-rupee', Request.DepositRupee); - app.post('/withdraw-rupee', Request.WithdrawRupee); + app.post('/deposit-token', Request.DepositToken); + app.post('/withdraw-token', Request.WithdrawToken); //Manage user tags (Access to trusted IDs only) diff --git a/src/market.js b/src/market.js index 1458959..f4ed967 100644 --- a/src/market.js +++ b/src/market.js @@ -3,9 +3,59 @@ const coupling = require('./coupling'); const MINIMUM_BUY_REQUIREMENT = 0.1; -var DB; //container for database +var DB, allowedAssets; //container for database and allowed assets -function addSellOrder(floID, quantity, min_price) { +const getAssetBalance = (floID, asset) => new Promise((resolve, reject) => { + let promises = (asset === floGlobals.currency) ? [ + DB.query("SELECT balance FROM Cash WHERE floID=?", [floID]), + DB.query("SELECT SUM(quantity*maxPrice) AS locked FROM BuyOrder WHERE floID=?", [floID]) + ] : [ + DB.query("SELECT SUM(quantity) AS balance FROM Vault WHERE floID=?, asset=?", [floID, asset]), + DB.query("SELECT SUM(quantity) AS locked FROM SellOrder WHERE floID=? AND asset=?", [floID, asset]) + ]; + Promise.all(promises).then(result => resolve({ + total: result[0][0].balance, + locked: result[1][0].locked, + net: result[0][0].balance - result[1][0].locked + })).catch(error => reject(error)) +}); + +getAssetBalance.check = (floID, asset, amount) => new Promise((resolve, reject) => { + getAssetBalance(floID, asset).then(balance => { + if (balance.total < amount) + reject(INVALID(`Insufficient ${asset}`)); + else if (balance.net < amount) + reject(INVALID(`Insufficient ${asset} (Some are locked in orders)`)); + else + resolve(true); + }).catch(error => reject(error)) +}) + +const consumeAsset = (floID, asset, amount, txQueries = []) => new Promise((resolve, reject) => { + //If asset/token is currency update Cash else consume from Vault + if (asset === floGlobals.currency) { + txQueries.push(["UPDATE Cash SET balance=balance-? WHERE floID=?", [amount, floID]]); + return resolve(txQueries); + } else + DB.query("SELECT id, quantity FROM Vault WHERE floID=? AND asset=? ORDER BY locktime", [floID, asset]).then(coins => { + let rem = amount; + for (let i = 0; i < coins.length && rem > 0; i++) { + if (rem < coins[i].quantity) { + txQueries.push(["UPDATE Vault SET quantity=quantity-? WHERE id=?", [rem, coins[i].id]]); + rem = 0; + } else { + txQueries.push(["DELETE FROM Vault WHERE id=?", [coins[i].id]]); + rem -= result[i].quantity; + } + } + if (rem > 0) //should not happen AS the total and net is checked already + reject(INVALID("Insufficient Asset")); + else + resolve(txQueries); + }).catch(error => reject(error)); +}); + +function addSellOrder(floID, asset, quantity, min_price) { return new Promise((resolve, reject) => { if (!floID || !floCrypto.validateAddr(floID)) return reject(INVALID("Invalid FLO ID")); @@ -13,41 +63,33 @@ function addSellOrder(floID, quantity, min_price) { return reject(INVALID(`Invalid quantity (${quantity})`)); else if (typeof min_price !== "number" || min_price <= 0) return reject(INVALID(`Invalid min_price (${min_price})`)); - checkSellRequirement(floID).then(_ => { - 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; - 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)); + else if (!allowedAssets.includes(asset)) + 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]) + .then(result => resolve("Added SellOrder to DB")) + .catch(error => reject(error)); + }).catch(error => reject(error)) }).catch(error => reject(error)); }); } -function checkSellRequirement(floID) { - return new Promise((resolve, reject) => { - DB.query("SELECT * FROM Tags WHERE floID=? AND tag=?", [floID, "MINER"]).then(result => { - if (result.length) - return resolve(true); - DB.query("SELECT SUM(quantity) AS brought FROM Transactions WHERE buyer=?", [floID]).then(result => { - if (result[0].brought >= MINIMUM_BUY_REQUIREMENT) - resolve(true); - else - reject(INVALID(`Sellers required to buy atleast ${MINIMUM_BUY_REQUIREMENT} FLO before placing a sell order unless they are a Miner`)); - }).catch(error => reject(error)) +const checkSellRequirement = floID => new Promise((resolve, reject) => { + DB.query("SELECT * FROM Tags WHERE floID=? AND tag=?", [floID, "MINER"]).then(result => { + if (result.length) + return resolve(true); + //TODO: Should seller need to buy same type of asset before selling? + DB.query("SELECT SUM(quantity) AS brought FROM Transactions WHERE buyer=?", [floID]).then(result => { + if (result[0].brought >= MINIMUM_BUY_REQUIREMENT) + resolve(true); + else + reject(INVALID(`Sellers required to buy atleast ${MINIMUM_BUY_REQUIREMENT} FLO before placing a sell order unless they are a Miner`)); }).catch(error => reject(error)) - }) -} + }).catch(error => reject(error)) +}); -function addBuyOrder(floID, quantity, max_price) { +function addBuyOrder(floID, asset, quantity, max_price) { return new Promise((resolve, reject) => { if (!floID || !floCrypto.validateAddr(floID)) return reject(INVALID("Invalid FLO ID")); @@ -55,21 +97,12 @@ function addBuyOrder(floID, quantity, max_price) { 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 Cash 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; - 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)); + 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]) + .then(result => resolve("Added BuyOrder to DB")) + .catch(error => reject(error)); }).catch(error => reject(error)); }); } @@ -101,11 +134,11 @@ function cancelOrder(type, id, floID) { function getAccountDetails(floID) { return new Promise((resolve, reject) => { let select = []; - select.push(["rupeeBalance", "Cash"]); - 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])); + select.push(["balance", "Cash"]); + select.push(["asset, AVG(base) AS avg_base, SUM(quantity) AS quantity", "Vault", "GROUP BY asset"]); + select.push(["id, asset, quantity, minPrice, time_placed", "SellOrder"]); + select.push(["id, asset, quantity, maxPrice, time_placed", "BuyOrder"]); + let promises = select.map(a => DB.query(`SELECT ${a[0]} FROM ${a[1]} WHERE floID=? ${a[2] || ""}`, [floID])); Promise.allSettled(promises).then(results => { let response = { floID: floID, @@ -117,10 +150,10 @@ function getAccountDetails(floID) { else switch (i) { case 0: - response.rupee_total = a.value[0].rupeeBalance; + response.cash = a.value[0].balance; break; case 1: - response.coins = a.value; + response.vault = a.value; break; case 2: response.sellOrders = a.value; @@ -166,7 +199,7 @@ function confirmDepositFLO() { txQueries.push(["INSERT INTO Vault(floID, quantity) VALUES (?, ?)", [req.floID, amount]]); txQueries.push(["UPDATE inputFLO SET status=?, amount=? WHERE id=?", ["SUCCESS", amount, req.id]]); DB.transaction(txQueries) - .then(result => console.debug("FLO deposited for ", req.floID)) + .then(result => console.debug("FLO deposited:", req.floID, amount)) .catch(error => console.error(error)) }).catch(error => { console.error(error); @@ -206,45 +239,23 @@ function withdrawFLO(floID, amount) { return reject(INVALID("Invalid FLO ID")); else if (typeof amount !== "number" || amount <= 0) return reject(INVALID(`Invalid amount (${amount})`)); - DB.query("SELECT SUM(quantity) AS total FROM Vault WHERE floID=?", [floID]).then(result => { - let total = result.pop()["total"] || 0; - if (total < amount) - 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; - if (available < amount) - return reject(INVALID("Insufficient FLO (Some FLO are locked in sell orders)")); - DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY locktime", [floID]).then(coins => { - let rem = amount, - txQueries = []; - for (let i = 0; i < coins.length && rem > 0; i++) { - if (rem < coins[i].quantity) { - txQueries.push(["UPDATE Vault SET quantity=quantity-? WHERE id=?", [rem, coins[i].id]]); - rem = 0; - } else { - txQueries.push(["DELETE FROM Vault WHERE id=?", [coins[i].id]]); - rem -= coins[i].quantity; - } - } - if (rem > 0) //should not happen AS the total and net is checked already - return reject(INVALID("Insufficient FLO")); - DB.transaction(txQueries).then(result => { - //Send FLO to user via blockchain API - floBlockchainAPI.sendTx(global.myFloID, floID, amount, global.myPrivKey, 'Withdraw FLO Coins from Market').then(txid => { - if (!txid) - throw Error("Transaction not successful"); - //Transaction was successful, Add in DB - DB.query("INSERT INTO outputFLO (floID, amount, txid, status) VALUES (?, ?, ?, ?)", [floID, amount, txid, "WAITING_CONFIRMATION"]) - .then(_ => null).catch(error => console.error(error)) - .finally(_ => resolve("Withdrawal was successful")); - }).catch(error => { - console.debug(error); - DB.query("INSERT INTO outputFLO (floID, amount, status) VALUES (?, ?, ?)", [floID, amount, "PENDING"]) - .then(_ => null).catch(error => console.error(error)) - .finally(_ => resolve("Withdrawal request is in process")); - }); - }).catch(error => reject(error)); + getAssetBalance.check(floID, "FLO", amount).then(_ => { + consumeAsset(floID, "FLO", amount).then(txQueries => { + DB.transaction(txQueries).then(result => { + //Send FLO to user via blockchain API + floBlockchainAPI.sendTx(global.myFloID, floID, amount, global.myPrivKey, 'Withdraw FLO Coins from Market').then(txid => { + if (!txid) + throw Error("Transaction not successful"); + //Transaction was successful, Add in DB + DB.query("INSERT INTO outputFLO (floID, amount, txid, status) VALUES (?, ?, ?, ?)", [floID, amount, txid, "WAITING_CONFIRMATION"]) + .then(_ => null).catch(error => console.error(error)) + .finally(_ => resolve("Withdrawal was successful")); + }).catch(error => { + console.debug(error); + DB.query("INSERT INTO outputFLO (floID, amount, status) VALUES (?, ?, ?)", [floID, amount, "PENDING"]) + .then(_ => null).catch(error => console.error(error)) + .finally(_ => resolve("Withdrawal request is in process")); + }); }).catch(error => reject(error)); }).catch(error => reject(error)); }).catch(error => reject(error)); @@ -266,22 +277,22 @@ function retryWithdrawalFLO() { } function confirmWithdrawalFLO() { - DB.query("SELECT id, floID, txid FROM outputFLO WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { + DB.query("SELECT id, floID, amount, txid FROM outputFLO WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { results.forEach(req => { floBlockchainAPI.getTx(req.txid).then(tx => { if (!tx.blockheight || !tx.confirmations) //Still not confirmed return; DB.query("UPDATE outputFLO SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug("FLO withdrawed for ", req.floID)) + .then(result => console.debug("FLO withdrawed:", req.floID, req.amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)); }) }).catch(error => console.error(error)); } -function depositRupee(floID, txid) { +function depositToken(floID, txid) { return new Promise((resolve, reject) => { - DB.query("SELECT status FROM inputRupee WHERE txid=? AND floID=?", [txid, floID]).then(result => { + DB.query("SELECT status FROM inputToken WHERE txid=? AND floID=?", [txid, floID]).then(result => { if (result.length) { switch (result[0].status) { case "PENDING": @@ -292,53 +303,58 @@ function depositRupee(floID, txid) { return reject(INVALID("Transaction already used to add tokens")); } } else - DB.query("INSERT INTO inputRupee(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) + DB.query("INSERT INTO inputToken(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) .then(result => resolve("Deposit request in process")) .catch(error => reject(error)); }).catch(error => reject(error)) }); } -function confirmDepositRupee() { - DB.query("SELECT id, floID, txid FROM inputRupee WHERE status=?", ["PENDING"]).then(results => { +function confirmDepositToken() { + DB.query("SELECT id, floID, txid FROM inputToken WHERE status=?", ["PENDING"]).then(results => { results.forEach(req => { - confirmDepositRupee.checkTx(req.floID, req.txid).then(amounts => { + confirmDepositToken.checkTx(req.floID, req.txid).then(amounts => { DB.query("SELECT id FROM inputFLO where floID=? AND txid=?", [req.floID, req.txid]).then(result => { let txQueries = [], - amount_rupee = amounts[0]; + token_name = amounts[0], + amount_token = amounts[1]; //Add the FLO balance if necessary if (!result.length) { - let amount_flo = amounts[1]; - txQueries.push(["INSERT INTO Vault(floID, quantity) VALUES (?, ?)", [req.floID, amount_flo]]); + let amount_flo = amounts[2]; + txQueries.push(["INSERT INTO Vault(floID, asset, quantity) VALUES (?, ?, ?)", [req.floID, "FLO", amount_flo]]); txQueries.push(["INSERT INTO inputFLO(txid, floID, amount, status) VALUES (?, ?, ?, ?)", [req.txid, req.floID, amount_flo, "SUCCESS"]]); } - txQueries.push(["UPDATE inputRupee SET status=? WHERE id=?", ["SUCCESS", req.id]]); - txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance+? WHERE floID=?", [amount_rupee, req.floID]]); + txQueries.push(["UPDATE inputToken SET status=?, token=?, amount=? WHERE id=?", ["SUCCESS", token_name, amount_token, req.id]]); + if (token_name === floGlobals.currency) + txQueries.push(["UPDATE Cash SET balance=balance+? WHERE floID=?", [amount_token, req.floID]]); + else + txQueries.push(["INSERT INTO Vault(floID, asset, quantity) VALUES (?, ?, ?)", [req.floID, token_name, amount_token]]); DB.transaction(txQueries) - .then(result => console.debug("Rupee deposited for ", req.floID)) + .then(result => console.debug("Token deposited:", req.floID, token_name, amount_token)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE inputRupee SET status=? WHERE id=?", ["REJECTED", req.id]) + DB.query("UPDATE inputToken SET status=? WHERE id=?", ["REJECTED", req.id]) .then(_ => null).catch(error => console.error(error)); }); }) }).catch(error => console.error(error)) } -confirmDepositRupee.checkTx = function(sender, txid) { +confirmDepositToken.checkTx = function(sender, txid) { return new Promise((resolve, reject) => { - const receiver = global.myFloID; //receiver should be market's floID (ie, adminID) + const receiver = global.sinkID; //receiver should be market's floID (ie, sinkID) tokenAPI.getTx(txid).then(tx => { if (tx.parsedFloData.type !== "transfer") return reject([true, "Transaction type not 'transfer'"]); else if (tx.parsedFloData.transferType !== "token") return reject([true, "Transaction transfer is not 'token'"]); - else if (tx.parsedFloData.tokenIdentification !== "rupee") - return reject([true, "Transaction token is not 'rupee'"]); - var amount_rupee = tx.parsedFloData.tokenAmount; + var token_name = tx.parsedFloData.tokenIdentification, + amount_token = tx.parsedFloData.tokenAmount; + if (!allowedAssets.includes(token_name) || token_name === "FLO") + return reject([true, "Token not authorised"]); let vin_sender = tx.transactionDetails.vin.filter(v => v.addr === sender) if (!vin_sender.length) return reject([true, "Transaction not sent by the sender"]); @@ -346,60 +362,36 @@ confirmDepositRupee.checkTx = function(sender, txid) { if (amount_flo == 0) return reject([true, "Transaction receiver is not market ID"]); else - resolve([amount_rupee, amount_flo]); + resolve([token_name, amount_token, amount_flo]); }).catch(error => reject([false, error])) }) } -function withdrawRupee(floID, amount) { +function withdrawToken(floID, token, amount) { return new Promise((resolve, reject) => { if (!floID || !floCrypto.validateAddr(floID)) return reject(INVALID("Invalid FLO ID")); else if (typeof amount !== "number" || amount <= 0) return reject(INVALID(`Invalid amount (${amount})`)); - DB.query("SELECT SUM(quantity) AS total FROM Vault WHERE floID=?", [floID]).then(result => { - let required_flo = floGlobals.sendAmt + floGlobals.fee, - total = result.pop()["total"] || 0; - if (total < required_flo) - return reject(INVALID(`Insufficient FLO! Required ${required_flo} FLO to withdraw tokens`)); - DB.query("SELECT SUM(quantity) AS locked FROM SellOrder WHERE floID=?", [floID]).then(result => { - let locked = result.pop()["locked"] || 0; - let available = total - locked; - if (available < required_flo) - return reject(INVALID(`Insufficient FLO (Some FLO are locked in sell orders)! Required ${required_flo} FLO to withdraw tokens`)); - DB.query("SELECT rupeeBalance FROM Cash WHERE floID=?", [floID]).then(result => { - if (result.length < 1) - return reject(INVALID(`FLO_ID: ${floID} not registered`)); - if (result[0].rupeeBalance < amount) - return reject(INVALID('Insufficient Rupee balance')); - DB.query("SELECT id, quantity, base FROM Vault WHERE floID=? ORDER BY locktime", [floID]).then(coins => { - let rem = required_flo, - txQueries = []; - for (let i = 0; i < coins.length && rem > 0; i++) { - if (rem < coins[i].quantity) { - txQueries.push(["UPDATE Vault SET quantity=quantity-? WHERE id=?", [rem, coins[i].id]]); - rem = 0; - } else { - txQueries.push(["DELETE FROM Vault WHERE id=?", [coins[i].id]]); - rem -= result[i].quantity; - } - } - if (rem > 0) //should not happen AS the total and net is checked already - return reject(INVALID("Insufficient FLO")); - txQueries.push(["UPDATE Cash SET rupeeBalance=rupeeBalance-? WHERE floID=?", [amount, floID]]); - + else if (!allowedAssets.includes(token) || token === "FLO") + return reject(INVALID("Invalid Token")); + //Check for FLO balance (transaction fee) + const required_flo = floGlobals.sendAmt + floGlobals.fee; + getAssetBalance.check(floID, "FLO", required_flo).then(_ => { + getAssetBalance.check(floID, token, amount).then(_ => { + consumeAsset(floID, "FLO", required_flo).then(txQueries => { + consumeAsset(floID, token, amount, txQueries).then(txQueries => { DB.transaction(txQueries).then(result => { //Send FLO to user via blockchain API - tokenAPI.sendToken(global.myPrivKey, amount, '(withdrawal from market)', floID).then(txid => { - if (!txid) - throw Error("Transaction not successful"); + tokenAPI.sendToken(global.myPrivKey, amount, '(withdrawal from market)', floID, token).then(txid => { + if (!txid) throw Error("Transaction not successful"); //Transaction was successful, Add in DB - DB.query("INSERT INTO outputRupee (floID, amount, txid, status) VALUES (?, ?, ?, ?)", [floID, amount, txid, "WAITING_CONFIRMATION"]) + DB.query("INSERT INTO outputToken (floID, token, amount, txid, status) VALUES (?, ?, ?, ?, ?)", [floID, token, amount, txid, "WAITING_CONFIRMATION"]) .then(_ => null).catch(error => console.error(error)) .finally(_ => resolve("Withdrawal was successful")); }).catch(error => { console.debug(error); - DB.query("INSERT INTO outputRupee (floID, amount, status) VALUES (?, ?, ?)", [floID, amount, "PENDING"]) + DB.query("INSERT INTO outputToken (floID, token, amount, status) VALUES (?, ?, ?, ?)", [floID, token, amount, "PENDING"]) .then(_ => null).catch(error => console.error(error)) .finally(_ => resolve("Withdrawal request is in process")); }); @@ -411,26 +403,26 @@ function withdrawRupee(floID, amount) { }); } -function retryWithdrawalRupee() { - DB.query("SELECT id, floID, amount FROM outputRupee WHERE status=?", ["PENDING"]).then(results => { +function retryWithdrawalToken() { + DB.query("SELECT id, floID, token, amount FROM outputToken WHERE status=?", ["PENDING"]).then(results => { results.forEach(req => { - tokenAPI.sendToken(global.myPrivKey, req.amount, '(withdrawal from market)', req.floID).then(txid => { + tokenAPI.sendToken(global.myPrivKey, req.amount, '(withdrawal from market)', req.floID, req.token).then(txid => { if (!txid) throw Error("Transaction not successful"); //Transaction was successful, Add in DB - DB.query("UPDATE outputRupee SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, req.id]) + DB.query("UPDATE outputToken SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, req.id]) .then(_ => null).catch(error => console.error(error)); }).catch(error => console.error(error)); }); }).catch(error => reject(error)); } -function confirmWithdrawalRupee() { - DB.query("SELECT id, floID, amount, txid FROM outputRupee WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { +function confirmWithdrawalToken() { + DB.query("SELECT id, floID, token, amount, txid FROM outputToken WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { results.forEach(req => { tokenAPI.getTx(req.txid).then(tx => { - DB.query("UPDATE outputRupee SET status=? WHERE id=?", ["SUCCESS", req.id]) - .then(result => console.debug("Rupee withdrawed for ", req.floID)) + DB.query("UPDATE outputToken SET status=? WHERE id=?", ["SUCCESS", req.id]) + .then(result => console.debug("Token withdrawed:", req.floID, req.token, req.amount)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }) @@ -444,11 +436,11 @@ function periodicProcess() { function blockchainReCheck() { confirmDepositFLO(); - confirmDepositRupee(); + confirmDepositToken(); retryWithdrawalFLO(); - retryWithdrawalRupee(); + retryWithdrawalToken(); confirmWithdrawalFLO(); - confirmWithdrawalRupee(); + confirmWithdrawalToken(); } module.exports = { @@ -461,12 +453,15 @@ module.exports = { getAccountDetails, depositFLO, withdrawFLO, - depositRupee, - withdrawRupee, + depositToken, + withdrawToken, periodicProcess, group: coupling.group, set DB(db) { DB = db; coupling.DB = db; + }, + set allowedAssets(assets) { + allowedAssets = assets; } }; \ No newline at end of file diff --git a/src/request.js b/src/request.js index 44c11c3..acec8a5 100644 --- a/src/request.js +++ b/src/request.js @@ -159,11 +159,12 @@ function PlaceSellOrder(req, res) { let data = req.body; validateRequestFromFloID({ type: "sell_order", + asset: data.asset, quantity: data.quantity, min_price: data.min_price, timestamp: data.timestamp }, data.sign, data.floID).then(req_str => { - market.addSellOrder(data.floID, data.quantity, data.min_price) + market.addSellOrder(data.floID, data.asset, data.quantity, data.min_price) .then(result => { storeRequest(data.floID, req_str, data.sign); res.send('Sell Order placed successfully'); @@ -189,11 +190,12 @@ function PlaceBuyOrder(req, res) { let data = req.body; validateRequestFromFloID({ type: "buy_order", + asset: data.asset, quantity: data.quantity, max_price: data.max_price, timestamp: data.timestamp }, data.sign, data.floID).then(req_str => { - market.addBuyOrder(data.floID, data.quantity, data.max_price) + market.addBuyOrder(data.floID, data.asset, data.quantity, data.max_price) .then(result => { storeRequest(data.floID, req_str, data.sign); res.send('Buy Order placed successfully'); @@ -351,14 +353,14 @@ function WithdrawFLO(req, res) { }); } -function DepositRupee(req, res) { +function DepositToken(req, res) { let data = req.body; validateRequestFromFloID({ - type: "deposit_Rupee", + type: "deposit_Token", txid: data.txid, timestamp: data.timestamp }, data.sign, data.floID).then(req_str => { - market.depositRupee(data.floID, data.txid).then(result => { + market.depositToken(data.floID, data.txid).then(result => { storeRequest(data.floID, req_str, data.sign); res.send(result); }).catch(error => { @@ -379,14 +381,15 @@ function DepositRupee(req, res) { }); } -function WithdrawRupee(req, res) { +function WithdrawToken(req, res) { let data = req.body; validateRequestFromFloID({ - type: "withdraw_Rupee", + type: "withdraw_Token", + token: data.token, amount: data.amount, timestamp: data.timestamp }, data.sign, data.floID).then(req_str => { - market.withdrawRupee(data.floID, data.amount).then(result => { + market.withdrawToken(data.floID, data.token, data.amount).then(result => { storeRequest(data.floID, req_str, data.sign); res.send(result); }).catch(error => { @@ -485,8 +488,8 @@ module.exports = { Account, DepositFLO, WithdrawFLO, - DepositRupee, - WithdrawRupee, + DepositToken, + WithdrawToken, periodicProcess: market.periodicProcess, addUserTag, removeUserTag, diff --git a/src/tokenAPI.js b/src/tokenAPI.js index be96954..d786fc9 100644 --- a/src/tokenAPI.js +++ b/src/tokenAPI.js @@ -14,7 +14,7 @@ }).catch(error => reject(error)) }) }, - getBalance: function(floID, token = floGlobals.token) { + getBalance: function(floID, token = floGlobals.currency) { return new Promise((resolve, reject) => { this.fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) .then(result => resolve(result.balance || 0)) @@ -35,7 +35,7 @@ }).catch(error => reject(error)) }) }, - sendToken: function(privKey, amount, message = "", receiverID = floGlobals.adminID, token = floGlobals.token) { + sendToken: function(privKey, amount, message = "", receiverID = floGlobals.adminID, token = floGlobals.currency) { return new Promise((resolve, reject) => { let senderID = floCrypto.getFloID(privKey); if (typeof amount !== "number" || amount <= 0)