diff --git a/src/app.js b/src/app.js index fb52995..7e3b8e4 100644 --- a/src/app.js +++ b/src/app.js @@ -54,6 +54,12 @@ module.exports = function App(secret, DB) { //get account details app.get('/account', Request.Account); + //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); + Request.DB = DB; return app; } \ No newline at end of file diff --git a/src/database.js b/src/database.js index 6649f92..e82ab14 100644 --- a/src/database.js +++ b/src/database.js @@ -30,7 +30,7 @@ function Database(user, password, dbname, host = 'localhost') { }) }); - Object.defineProperty(db, "TxQuery", { + Object.defineProperty(db, "transaction", { value: (queries) => new Promise((resolve, reject) => { db.connect.then(conn => { conn.beginTransaction(err => { diff --git a/src/floGlobals.js b/src/floGlobals.js index f54529d..a13b291 100644 --- a/src/floGlobals.js +++ b/src/floGlobals.js @@ -8,8 +8,8 @@ const floGlobals = { FLO: ['https://livenet.flocha.in/', 'https://flosight.duckdns.org/'], FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/'] }, - //sendAmt: 0.001, - //fee: 0.0005, + sendAmt: 0.001, + fee: 0.0005, }; (typeof global !== "undefined" ? global : window).cryptocoin = floGlobals.blockchain; diff --git a/src/market.js b/src/market.js index f78f108..e132b2c 100644 --- a/src/market.js +++ b/src/market.js @@ -1,7 +1,60 @@ +const floGlobals = require("./floGlobals"); + var net_FLO_price; //container for FLO price (from API or by model) var DB; //container for database const REFRESH_INTERVAL = 60 * 1000; //1 min +const tokenAPI = { + fetch_api: function(apicall) { + return new Promise((resolve, reject) => { + console.log(floGlobals.tokenURL + apicall); + fetch(floGlobals.tokenURL + apicall).then(response => { + if (response.ok) + response.json().then(data => resolve(data)); + else + reject(response) + }).catch(error => reject(error)) + }) + }, + getBalance: function(floID, token = 'rupee') { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`) + .then(result => resolve(result.balance || 0)) + .catch(error => reject(error)) + }) + }, + getTx: function(txID) { + return new Promise((resolve, reject) => { + this.fetch_api(`api/v1.0/getTransactionDetails/${txID}`).then(res => { + if (res.result === "error") + reject(res.description); + else if (!res.parsedFloData) + reject("Data piece (parsedFloData) missing"); + else if (!res.transactionDetails) + reject("Data piece (transactionDetails) missing"); + else + resolve(res); + }).catch(error => reject(error)) + }) + }, + sendToken: function(privKey, amount, message = "", receiverID = floGlobals.adminID, token = 'rupee') { + return new Promise((resolve, reject) => { + let senderID = floCrypto.getFloID(privKey); + if (typeof amount !== "number" || amount <= 0) + return reject("Invalid amount"); + this.getBalance(senderID, token).then(bal => { + if (amount > bal) + return reject("Insufficiant token balance"); + floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }); + } +} + + + function getRates() { return new Promise((resolve, reject) => { getRates.FLO_USD().then(FLO_rate => { @@ -11,7 +64,7 @@ function getRates() { resolve(net_FLO_price); }).catch(error => reject(error)) }).catch(error => reject(error)) - }) + }); } getRates.FLO_USD = function() { @@ -65,32 +118,6 @@ function addSellOrder(floID, quantity, min_price) { }).catch(error => reject(error)); }); } -/* -function addSellOrder(floID, quantity, min_price) { - return new Promise((resolve, reject) => { - DB.query("SELECT id, base, (quantity - locked) as available FROM Vault WHERE floID=? ORDER BY base", [floID]).then(result => { - console.debug(result); - let rem = quantity, - sell_base = 0, - txQueries = []; - for (let i = 0; i < result.length && rem > 0; i++) { - var lock = (rem < result[i].available ? rem : result[i].available); - rem -= lock; - sell_base += (lock * result[i].base); - txQueries.push(["UPDATE Vault SET locked=locked-? WHERE id=?", [lock, result[i].id]]); - } - if (rem > 0) - return reject(INVALID("Insufficient FLO")); - sell_base = sell_base / quantity; - Promise.all(txQueries.map(a => DB.query(a[0], a[1]))).then(results => { - DB.query("INSERT INTO SellOrder(floID, quantity, minPrice, sellBase) VALUES (?, ?, ?)", [floID, quantity, min_price, sell_base]) - .then(result => resolve(result)) - .catch(error => reject(error)); - }).catch(error => reject(error)); - }).catch(error => reject(error)) - }) -} -*/ function addBuyOrder(floID, quantity, max_price) { return new Promise((resolve, reject) => { @@ -164,7 +191,7 @@ function matchBuyAndSell() { tx_quantity = processBuyAndSellOrder(seller_best, buyer_best, txQueries); updateBalance(seller_best, buyer_best, txQueries, cur_price, tx_quantity); //process txn query in SQL - DB.TxQuery(txQueries).then(results => { + DB.transaction(txQueries).then(results => { console.log(`Transaction was successful! BuyOrder:${buyer_best.id}| SellOrder:${seller_best.id}`); //Since a tx was successful, match again matchBuyAndSell(); @@ -316,13 +343,330 @@ function getAccountDetails(floID) { }); } +function depositCoins(floID, txid) { + return new Promise((resolve, reject) => { + DB.query("SELECT status FROM inputFLO WHERE txid=? AND floID=?", [txid, floID]).then(result => { + if (result.length) { + switch (result[0].status) { + case "PENDING": + return reject(INVALID("Transaction already in process")); + case "REJECTED": + return reject(INVALID("Transaction already rejected")); + case "SUCCESS": + return reject(INVALID("Transaction already used to add coins")); + } + } else + DB.query("INSERT INTO inputFLO(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) + .then(result => resolve("Deposit request in process")) + .catch(error => reject(error)); + }).catch(error => reject(error)) + }); +} + +function confirmDepositFLO() { + DB.query("SELECT id, floID, txid FROM inputFLO WHERE status=?", ["PENDING"]).then(results => { + results.forEach(req => { + confirmDepositFLO.checkTx(req.floID, req.txid).then(amount => { + let txQueries = []; + txQueries.push("INSERT INTO Vault(floID, quantity) VALUES (?, ?)", [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 ", floID)) + .catch(error => console.error(error)) + }).catch(error => { + if (!Array.isArray(error)) + console.error(error); + else if (error[0]) + DB.query("UPDATE inputFLO SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); + }); + }) + }).catch(error => console.error(error)) +} + +confirmDepositFLO.checkTx = function(sender, txid) { + return new Promise((resolve, reject) => { + const receiver = globals.myFloID; //receiver should be market's floID (ie, adminID) + floBlockchainAPI.getTx(txid).then(tx => { + let vin_sender = tx.vin.filter(v => v.addr === sender).length + if (!vin_sender.length) + return reject([true, "Transaction not sent by the sender"]); + if (vin_sender.length !== tx.vin.length) + return reject([true, "Transaction input containes other floIDs"]); + if (!tx.blockheight) + return reject([false, "Transaction not included in any block yet"]); + if (!tx.confirmations) + return reject([false, "Transaction not confirmed yet"]); + let amount = tx.vout.reduce(v => v.scriptPubKey.addresses[0] === receiver ? a + v.value : a, 0); + if (amount == 0) + return reject([true, "Transaction receiver is not market ID"]); + else + resolve(amount); + }).catch(error => reject([false, error])) + }) +} + +function withdrawCoins(floID, 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 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 -= result[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(result => { + let txid = result.txid.result; + if (!txid || result.txid.error) + 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 => { + DB.query("INSERT INTO outputFLO (floID, amount, txid, status) VALUES (?, ?, ?, ?)", [floID, amount, txid, "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)); + }).catch(error => reject(error)); + }); +} + +function retryWithdrawalFLO() { + DB.query("SELECT id, floID, amount FROM outputFLO WHERE status=?", ["PENDING"]).then(results => { + results.forEach(req => { + floBlockchainAPI.sendTx(global.myFloID, req.floID, req.amount, global.myPrivKey, 'Withdraw FLO Coins from Market').then(result => { + let txid = result.txid.result; + if (!txid || result.txid.error) { + console.error(result); + return; + } + //Transaction was successful, Add in DB + DB.query("UPDATE outputFLO SET status=? WHERE id=?", ["WAITING_CONFIRMATION", req.id]) + .then(_ => null).catch(error => console.error(error)); + }).catch(error => console.error(error)); + }) + }).catch(error => reject(error)); +} + +function confirmWithdrawalFLO() { + DB.query("SELECT id, floID, 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)) + .catch(error => console.error(error)) + }).catch(error => console.error(error)); + }) + }).catch(error => console.error(error)); +} + +function depositTokens(floID, txid) { + return new Promise((resolve, reject) => { + DB.query("SELECT status FROM inputRupee WHERE txid=? AND floID=?", [txid, floID]).then(result => { + if (result.length) { + switch (result[0].status) { + case "PENDING": + return reject(INVALID("Transaction already in process")); + case "REJECTED": + return reject(INVALID("Transaction already rejected")); + case "SUCCESS": + return reject(INVALID("Transaction already used to add tokens")); + } + } else + DB.query("INSERT INTO inputRupee(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 => { + results.forEach(req => { + confirmDepositRupee.checkTx(req.floID, req.txid).then(amounts => { + DB.query("SELECT id FROM inputFLO where floID=? AND txid=?").then(result => { + let txQueries = [], + amount_rupee = amounts[0]; + //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]); + txQueries.push("INSERT INTO inputFLO(txid, floID, status) VALUES (?, ?, ?)", [req.txid, req.floID, "SUCCESS"]); + } + txQueries.push("UPDATE inputRupee SET status=? WHERE id=?", ["SUCCESS", req.id]); + txQueries.push("UPDATE Users SET rupeeBalance=rupeeBalance+? WHERE floID=?", [amount_rupee, req.floID]); + DB.transaction(txQueries) + .then(result => console.debug("Rupee deposited for ", req.floID)) + .catch(error => console.error(error)); + }).catch(error => reject(error)); + }).catch(error => { + if (!Array.isArray(error)) + console.error(error); + else if (error[0]) + DB.query("UPDATE inputRupee SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); + }); + }) + }).catch(error => console.error(error)) +} + +confirmDepositRupee.checkTx = function(sender, txid) { + return new Promise((resolve, reject) => { + const receiver = globals.myFloID; //receiver should be market's floID (ie, adminID) + 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; + let vin_sender = tx.transactionDetails.vin.filter(v => v.addr === sender).length + if (!vin_sender.length) + return reject([true, "Transaction not sent by the sender"]); + let amount_flo = tx.transactionDetails.vout.reduce(v => v.scriptPubKey.addresses[0] === receiver ? a + v.value : a, 0); + if (amount_flo == 0) + return reject([true, "Transaction receiver is not market ID"]); + else + resolve([amount_rupee, amount_flo]); + }).catch(error => reject([false, error])) + }) +} + +function withdrawTokens(floID, 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 < amount) + return reject(INVALID(`Insufficient FLO (Some FLO are locked in sell orders)! Required ${required_flo} FLO to withdraw tokens`)); + DB.query("SELECT rupeeBalance FROM Users 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 Users SET rupeeBalance=rupeeBalance-? WHERE floID=?", [amount, floID]]); + + DB.transaction(txQueries).then(result => { + //Send FLO to user via blockchain API + tokenAPI.sendToken(global.myPrivKey, amount, '(withdrawal from market)', floID).then(result => { + let txid = result.txid.result; + if (!txid || result.txid.error) + 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"]) + .then(_ => null).catch(error => console.error(error)) + .finally(_ => resolve("Withdrawal was successful")); + }).catch(error => { + DB.query("INSERT INTO outputRupee (floID, amount, txid, status) VALUES (?, ?, ?, ?)", [floID, amount, txid, "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)); + }).catch(error => reject(error)); + }).catch(error => reject(error)); + }); +} + +function retryWithdrawalRupee() { + DB.query("SELECT id, floID, amount FROM outputRupee WHERE status=?", ["PENDING"]).then(results => { + results.forEach(req => { + tokenAPI.sendToken(global.myPrivKey, req.amount, '(withdrawal from market)', req.floID).then(result => { + let txid = result.txid.result; + if (!txid || result.txid.error) { + console.error(result); + return; + } + //Transaction was successful, Add in DB + DB.query("UPDATE outputRupee SET status=? WHERE id=?", ["WAITING_CONFIRMATION", 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 => { + 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)) + .catch(error => console.error(error)); + }).catch(error => console.error(error)); + }) + }).catch(error => console.error(error)); +} + function intervalFunction() { let old_rate = net_FLO_price; getRates().then(cur_rate => { + transactionReCheck(); matchBuyAndSell(); }).catch(error => console.error(error)); } +function transactionReCheck() { + confirmDepositFLO(); + confirmDepositRupee(); + retryWithdrawalFLO(); + retryWithdrawalRupee(); + confirmWithdrawalFLO(); + confirmWithdrawalRupee(); +} + intervalFunction(); let refresher = setInterval(intervalFunction, REFRESH_INTERVAL); @@ -332,6 +676,10 @@ module.exports = { addSellOrder, cancelOrder, getAccountDetails, + depositCoins, + withdrawCoins, + depositTokens, + withdrawTokens, set DB(db) { DB = db; } diff --git a/src/request.js b/src/request.js index d308044..77c7aee 100644 --- a/src/request.js +++ b/src/request.js @@ -273,6 +273,130 @@ function Account(req, res) { } } +function DepositFLO(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + validateRequestFromFloID({ + type: "deposit_FLO", + txid: data.txid, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.depositCoins(session.user_id, data.txid).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 WithdrawFLO(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + validateRequestFromFloID({ + type: "withdraw_FLO", + amount: data.amount, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.withdrawCoins(session.user_id, data.amount).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 DepositRupee(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + validateRequestFromFloID({ + type: "deposit_Rupee", + txid: data.txid, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.depositTokens(session.user_id, data.txid).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 WithdrawRupee(req, res) { + let data = req.body, + session = req.session; + if (!session.user_id) + return res.status(INVALID.e_code).send("Login required"); + validateRequestFromFloID({ + type: "withdraw_Rupee", + amount: data.amount, + timestamp: data.timestamp + }, data.sign, session.user_id).then(req_str => { + market.withdrawTokens(session.user_id, data.amount).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, @@ -284,6 +408,10 @@ module.exports = { ListBuyOrders, ListTransactions, Account, + DepositFLO, + WithdrawFLO, + DepositRupee, + WithdrawRupee, set DB(db) { DB = db; market.DB = db;