diff --git a/args/schema.sql b/args/schema.sql index 1034969..0e15a73 100644 --- a/args/schema.sql +++ b/args/schema.sql @@ -114,7 +114,7 @@ CREATE TABLE BuyOrder ( FOREIGN KEY (asset) REFERENCES AssetList(asset) ); -CREATE TABLE InputCoin ( +CREATE TABLE DepositCoin ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128) NOT NULL, floID CHAR(34) NOT NULL, @@ -124,7 +124,7 @@ CREATE TABLE InputCoin ( PRIMARY KEY(id) ); -CREATE TABLE OutputCoin ( +CREATE TABLE WithdrawCoin ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128), floID CHAR(34) NOT NULL, @@ -134,7 +134,7 @@ CREATE TABLE OutputCoin ( PRIMARY KEY(id) ); -CREATE TABLE InputToken ( +CREATE TABLE DepositToken ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128) NOT NULL, floID CHAR(34) NOT NULL, @@ -144,7 +144,7 @@ CREATE TABLE InputToken ( PRIMARY KEY(id) ); -CREATE TABLE OutputToken ( +CREATE TABLE WithdrawToken ( id INT NOT NULL AUTO_INCREMENT, txid VARCHAR(128), floID CHAR(34) NOT NULL, @@ -212,6 +212,21 @@ CREATE TABLE AuditTrade( FOREIGN KEY (asset) REFERENCES AssetList(asset) ); +/* External Service */ + +CREATE TABLE DirectConvert( + id INT NOT NULL AUTO_INCREMENT, + floID CHAR(34) NOT NULL, + amount DECIMAL(16, 8), + coin VARCHAR(8) NOT NULL, + quantity DECIMAL(16, 8), + mode BIT NOT NULL, + in_txid VARCHAR(128), + out_txid VARCHAR(128), + locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status VARCHAR(50) NOT NULL, +); + /* Backup Feature (Tables & Triggers) */ CREATE TABLE _backup ( @@ -279,33 +294,40 @@ FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('BuyOrder', NEW.id) ON DUP CREATE TRIGGER BuyOrder_D AFTER DELETE ON BuyOrder FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('BuyOrder', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; -CREATE TRIGGER InputCoin_I AFTER INSERT ON InputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('InputCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER InputCoin_U AFTER UPDATE ON InputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('InputCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER InputCoin_D AFTER DELETE ON InputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('InputCoin', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; +CREATE TRIGGER DepositCoin_I AFTER INSERT ON DepositCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DepositCoin_U AFTER UPDATE ON DepositCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DepositCoin_D AFTER DELETE ON DepositCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositCoin', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; -CREATE TRIGGER OutputCoin_I AFTER INSERT ON OutputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('OutputCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER OutputCoin_U AFTER UPDATE ON OutputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('OutputCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; -CREATE TRIGGER OutputCoin_D AFTER DELETE ON OutputCoin -FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('OutputCoin', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; +CREATE TRIGGER WithdrawCoin_I AFTER INSERT ON WithdrawCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER WithdrawCoin_U AFTER UPDATE ON WithdrawCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawCoin', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER WithdrawCoin_D AFTER DELETE ON WithdrawCoin +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawCoin', 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 DepositToken_I AFTER INSERT ON DepositToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DepositToken_U AFTER UPDATE ON DepositToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DepositToken_D AFTER DELETE ON DepositToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DepositToken', 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 WithdrawToken_I AFTER INSERT ON WithdrawToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER WithdrawToken_U AFTER UPDATE ON WithdrawToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawToken', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER WithdrawToken_D AFTER DELETE ON WithdrawToken +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('WithdrawToken', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; + +CREATE TRIGGER DirectConvert_I AFTER INSERT ON DirectConvert +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DirectConvert', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DirectConvert_U AFTER UPDATE ON DirectConvert +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DirectConvert', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; +CREATE TRIGGER DirectConvert_D AFTER DELETE ON DirectConvert +FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('DirectConvert', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, timestamp=DEFAULT; CREATE TRIGGER UserTag_I AFTER INSERT ON UserTag FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserTag', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, timestamp=DEFAULT; diff --git a/args/truncateAll.sql b/args/truncateAll.sql index f32563d..8c85d43 100644 --- a/args/truncateAll.sql +++ b/args/truncateAll.sql @@ -4,10 +4,11 @@ TRUNCATE _backupCache; TRUNCATE AuditTrade; TRUNCATE BuyOrder; TRUNCATE Distributors; -TRUNCATE InputCoin; -TRUNCATE InputToken; -TRUNCATE OutputCoin; -TRUNCATE OutputToken; +TRUNCATE DepositCoin; +TRUNCATE DepositToken; +TRUNCATE WithdrawCoin; +TRUNCATE WithdrawToken; +TRUNCATE DirectConvert; TRUNCATE PriceHistory; TRUNCATE RequestLog; TRUNCATE SellOrder; diff --git a/src/_constants.js b/src/_constants.js index 3ad6334..e6650ad 100644 --- a/src/_constants.js +++ b/src/_constants.js @@ -30,5 +30,9 @@ module.exports = { BACKUP_INTERVAL: 5 * 60 * 1000, //5 min BACKUP_SYNC_TIMEOUT: 10 * 60 * 1000, //10 mins CHECKSUM_INTERVAL: 100, //times of BACKUP_INTERVAL + }, + sql: { + CONVERT_MODE_GET: 1, + CONVERT_MODE_PUT: 0, } } \ No newline at end of file diff --git a/src/background.js b/src/background.js index 421ab41..101bfda 100644 --- a/src/background.js +++ b/src/background.js @@ -1,26 +1,25 @@ 'use strict'; const blockchain = require('./blockchain'); +const conversion = require('./conversion'); const { LAUNCH_SELLER_TAG, MAXIMUM_LAUNCH_SELL_CHIPS, } = require('./_constants')["market"]; -function checkTag(floID, tag) { - return new Promise((resolve, reject) => { - DB.query("SELECT id FROM UserTag WHERE floID=? AND tag=?", [floID, tag]) - .then(result => resolve(result.length ? true : false)) - .catch(error => reject(error)) - }) -} +const _sql = require('./_constants').sql; + +var updateBalance; // container for updateBalance function + +const verifyTx = {}; function confirmDepositFLO() { - DB.query("SELECT id, floID, txid FROM InputCoin WHERE coin=? AND status=?", ["FLO", "PENDING"]).then(results => { + DB.query("SELECT id, floID, txid FROM DepositCoin WHERE coin=? AND status=?", ["FLO", "PENDING"]).then(results => { results.forEach(req => { - verifyDepositFLO(req.floID, req.txid).then(amount => { + verifyTx.FLO(req.floID, req.txid).then(amount => { addSellChipsIfLaunchSeller(req.floID, amount).then(txQueries => { txQueries.push(updateBalance.add(req.floID, "FLO", amount)); - txQueries.push(["UPDATE InputCoin SET status=?, amount=? WHERE id=?", ["SUCCESS", amount, req.id]]); + txQueries.push(["UPDATE DepositCoin SET status=?, amount=? WHERE id=?", ["SUCCESS", amount, req.id]]); DB.transaction(txQueries) .then(result => console.debug("FLO deposited:", req.floID, amount)) .catch(error => console.error(error)) @@ -28,14 +27,14 @@ function confirmDepositFLO() { }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE InputCoin SET status=? WHERE id=?", ["REJECTED", req.id]) - .then(_ => null).catch(error => console.error(error)); + DB.query("UPDATE DepositCoin SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); }); }) }).catch(error => console.error(error)) } -function verifyDepositFLO(sender, txid) { +verifyTx.FLO = function (sender, txid) { return new Promise((resolve, reject) => { floBlockchainAPI.getTx(txid).then(tx => { let vin_sender = tx.vin.filter(v => v.addr === sender) @@ -56,6 +55,14 @@ function verifyDepositFLO(sender, txid) { }) } +function checkTag(floID, tag) { + return new Promise((resolve, reject) => { + DB.query("SELECT id FROM UserTag WHERE floID=? AND tag=?", [floID, tag]) + .then(result => resolve(result.length ? true : false)) + .catch(error => reject(error)) + }) +} + function addSellChipsIfLaunchSeller(floID, quantity) { return new Promise((resolve, reject) => { checkTag(floID, LAUNCH_SELLER_TAG).then(result => { @@ -71,9 +78,7 @@ function addSellChipsIfLaunchSeller(floID, quantity) { let remLaunchChips = MAXIMUM_LAUNCH_SELL_CHIPS - (sold + chips) + brought; quantity = Math.min(quantity, remLaunchChips); if (quantity > 0) - resolve([ - ["INSERT INTO SellChips(floID, asset, quantity) VALUES (?, ?, ?)", [floID, 'FLO', quantity]] - ]); + resolve([["INSERT INTO SellChips(floID, asset, quantity) VALUES (?, ?, ?)", [floID, 'FLO', quantity]]]); else resolve([]); }).catch(error => reject(error)) @@ -84,77 +89,76 @@ function addSellChipsIfLaunchSeller(floID, quantity) { } function confirmDepositToken() { - DB.query("SELECT id, floID, txid FROM InputToken WHERE status=?", ["PENDING"]).then(results => { + DB.query("SELECT id, floID, txid FROM DepositToken WHERE status=?", ["PENDING"]).then(results => { results.forEach(req => { - verifyDepositToken(req.floID, req.txid).then(amounts => { - DB.query("SELECT id FROM InputCoin where floID=? AND coin=? AND txid=?", [req.floID, "FLO", req.txid]).then(result => { - let txQueries = [], - token_name = amounts[0], - amount_token = amounts[1]; + verifyTx.token(req.floID, req.txid).then(({ token, amount, flo_amount }) => { + DB.query("SELECT id FROM DepositCoin where floID=? AND coin=? AND txid=?", [req.floID, "FLO", req.txid]).then(result => { + let txQueries = []; //Add the FLO balance if necessary if (!result.length) { - let amount_flo = amounts[2]; - txQueries.push(updateBalance.add(req.floID, "FLO", amount_flo)); - txQueries.push(["INSERT INTO InputCoin(txid, floID, coin, amount, status) VALUES (?, ?, ?, ?, ?)", [req.txid, req.floID, "FLO", amount_flo, "SUCCESS"]]); + txQueries.push(updateBalance.add(req.floID, "FLO", flo_amount)); + txQueries.push(["INSERT INTO DepositCoin(txid, floID, coin, amount, status) VALUES (?, ?, ?, ?, ?)", [req.txid, req.floID, "FLO", flo_amount, "SUCCESS"]]); } - txQueries.push(["UPDATE InputToken SET status=?, token=?, amount=? WHERE id=?", ["SUCCESS", token_name, amount_token, req.id]]); - txQueries.push(updateBalance.add(req.floID, token_name, amount_token)); + txQueries.push(["UPDATE DepositToken SET status=?, token=?, amount=? WHERE id=?", ["SUCCESS", token, amount, req.id]]); + txQueries.push(updateBalance.add(req.floID, token, amount)); DB.transaction(txQueries) - .then(result => console.debug("Token deposited:", req.floID, token_name, amount_token)) + .then(result => console.debug("Token deposited:", req.floID, token, amount)) .catch(error => console.error(error)); }).catch(error => console.error(error)); }).catch(error => { console.error(error); if (error[0]) - DB.query("UPDATE InputToken SET status=? WHERE id=?", ["REJECTED", req.id]) - .then(_ => null).catch(error => console.error(error)); + DB.query("UPDATE DepositToken SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); }); }) }).catch(error => console.error(error)) } -function verifyDepositToken(sender, txid) { +verifyTx.token = function (sender, txid, currencyOnly = false) { return new Promise((resolve, reject) => { floTokenAPI.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'"]); - var token_name = tx.parsedFloData.tokenIdentification, - amount_token = tx.parsedFloData.tokenAmount; - if ((!assetList.includes(token_name) && token_name !== floGlobals.currency) || token_name === "FLO") + var token = tx.parsedFloData.tokenIdentification, + amount = tx.parsedFloData.tokenAmount; + if (currencyOnly && token !== floGlobals.currency) + return reject([true, "Token not currency"]); + else if (!currencyOnly && ((!assetList.includes(token) && token !== floGlobals.currency) || token === "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"]); - let amount_flo = tx.transactionDetails.vout.reduce((a, v) => blockchain.chests.includes(v.scriptPubKey.addresses[0]) ? a + v.value : a, 0); - if (amount_flo == 0) + let flo_amount = tx.transactionDetails.vout.reduce((a, v) => blockchain.chests.includes(v.scriptPubKey.addresses[0]) ? a + v.value : a, 0); + if (flo_amount == 0) return reject([true, "Transaction receiver is not market ID"]); //Maybe reject as false? (to compensate delay in chestsList loading from other nodes) else - resolve([token_name, amount_token, amount_flo]); + resolve({ token, amount, flo_amount }); }).catch(error => reject([false, error])) }) } function retryWithdrawalCoin() { - DB.query("SELECT id, floID, coin, amount FROM OutputCoin WHERE status=?", ["PENDING"]).then(results => { + DB.query("SELECT id, floID, coin, amount FROM WithdrawCoin WHERE status=?", ["PENDING"]).then(results => { results.forEach(req => blockchain.sendCoin.retry(req.floID, req.coin, req.amount, req.id)); - }).catch(error => reject(error)); + }).catch(error => console.error(error)); } function retryWithdrawalToken() { - DB.query("SELECT id, floID, token, amount FROM OutputToken WHERE status=?", ["PENDING"]).then(results => { + DB.query("SELECT id, floID, token, amount FROM WithdrawToken WHERE status=?", ["PENDING"]).then(results => { results.forEach(req => blockchain.sendToken.retry(req.floID, req.token, req.amount, req.id)); - }).catch(error => reject(error)); + }).catch(error => console.error(error)); } function confirmWithdrawalFLO() { - DB.query("SELECT id, floID, amount, txid FROM OutputCoin WHERE coin=? AND status=?", ["FLO", "WAITING_CONFIRMATION"]).then(results => { + DB.query("SELECT id, floID, amount, txid FROM WithdrawCoin WHERE coin=? AND status=?", ["FLO", "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 OutputCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) + DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) .then(result => console.debug("FLO withdrawed:", req.floID, req.amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)); @@ -163,12 +167,12 @@ function confirmWithdrawalFLO() { } function confirmWithdrawalBTC() { - DB.query("SELECT id, floID, amount, txid FROM OutputCoin WHERE coin=? AND status=?", ["BTC", "WAITING_CONFIRMATION"]).then(results => { + DB.query("SELECT id, floID, amount, txid FROM WithdrawCoin WHERE coin=? AND status=?", ["BTC", "WAITING_CONFIRMATION"]).then(results => { results.forEach(req => { btcOperator.getTx(req.txid).then(tx => { if (!tx.blockhash || !tx.confirmations) //Still not confirmed return; - DB.query("UPDATE OutputCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) + DB.query("UPDATE WithdrawCoin SET status=? WHERE id=?", ["SUCCESS", req.id]) .then(result => console.debug("BTC withdrawed:", req.floID, req.amount)) .catch(error => console.error(error)) }).catch(error => console.error(error)); @@ -177,10 +181,10 @@ function confirmWithdrawalBTC() { } function confirmWithdrawalToken() { - DB.query("SELECT id, floID, token, amount, txid FROM OutputToken WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { + DB.query("SELECT id, floID, token, amount, txid FROM WithdrawToken WHERE status=?", ["WAITING_CONFIRMATION"]).then(results => { results.forEach(req => { floTokenAPI.getTx(req.txid).then(tx => { - DB.query("UPDATE OutputToken SET status=? WHERE id=?", ["SUCCESS", req.id]) + DB.query("UPDATE WithdrawToken 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)); @@ -188,6 +192,90 @@ function confirmWithdrawalToken() { }).catch(error => console.error(error)); } +verifyTx.BTC = function (sender, txid) { + return new Promise((resolve, reject) => { + btcOperator.getTx(txid).then(tx => { + let vin_sender = tx.inputs.filter(v => floCrypto.isSameAddr(v.address, sender)) + 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.block_no) + return reject([false, "Transaction not included in any block yet"]); + if (!tx.confirmations) + return reject([false, "Transaction not confirmed yet"]); + let amount = tx.outputs.reduce((a, v) => + blockchain.chests.includes(floCrypto.toFloID(v.address, { bech: [coinjs.bech32.version] })) ? a + parseFloat(v.value) : a, 0); + if (amount == 0) + return reject([true, "Transaction receiver is not market ID"]); //Maybe reject as false? (to compensate delay in chestsList loading from other nodes) + else + resolve(amount); + }).catch(error => reject([false, error])) + }) +} + +function verifyConvert() { + DB.query("SELECT id, floID, mode, in_txid FROM DirectConvert WHERE status=? AND coin=?", ["PENDING", "BTC"]).then(results => { + results.forEach(req => { + if (mode == _sql.CONVERT_MODE_GET) { + verifyTx.token(req.floID, req.in_txid, true).then(({ amount }) => { + conversion.getRate().then(rate => { + blockchain.convertToCoin.init(req.floID, "BTC", amount, amount / rate, req.id) + }).catch(error => console.error(error)) + }).catch(error => { + console.error(error); + if (error[0]) + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); + }); + } else if (mode == _sql.CONVERT_MODE_PUT) { + verifyTx.BTC(req.floID, req.in_txid).then(quantity => { + conversion.getRate().then(rate => { + blockchain.convertFromCoin.init(req.floID, quantity * rate, quantity, req.id) + }).catch(error => console.error(error)) + }).catch(error => { + console.error(error); + if (error[0]) + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["REJECTED", req.id]) + .then(_ => null).catch(error => console.error(error)); + }); + } + }) + }).catch(error => console.error(error)) +} + +function retryConvert() { + DB.query("SELECT id, floID, mode, amount, quantity FROM DirectConvert WHERE status=? AND coin=?", ["PROCESSING", "BTC"]).then(results => { + results.forEach(req => { + if (mode == _sql.CONVERT_MODE_GET) + blockchain.convertToCoin.retry(req.floID, "BTC", req.quantity, req.id); + else if (mode == _sql.CONVERT_MODE_PUT) + blockchain.convertFromCoin.retry(req.floID, req.amount, req.id) + }) + }).catch(error => console.error(error)) +} + +function confirmConvert() { + DB.query("SELECT id, floID, mode, amount, quantity, out_txid FROM DirectConvert WHERE status=? AND coin=?", ["WAITING_CONFIRMATION", "BTC"]).then(results => { + results.forEach(req => { + if (mode == _sql.CONVERT_MODE_GET) + btcOperator.getTx(req.out_txid).then(tx => { + if (!tx.blockhash || !tx.confirmations) //Still not confirmed + return; + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", req.id]) + .then(result => console.debug(`${req.floID} converted ${amount} to ${req.quantity} BTC`)) + .catch(error => console.error(error)) + }).catch(error => console.error(error)); + else if (mode == _sql.CONVERT_MODE_PUT) + floTokenAPI.getTx(req.out_txid).then(tx => { + DB.query("UPDATE DirectConvert SET status=? WHERE id=?", ["SUCCESS", req.id]) + .then(result => console.debug(`${req.floID} converted ${req.quantity} BTC to ${amount}`)) + .catch(error => console.error(error)); + }).catch(error => console.error(error)); + }) + }).catch(error => console.error(error)); +} + module.exports = { blockchain, confirmDepositFLO, @@ -196,5 +284,11 @@ module.exports = { retryWithdrawalToken, confirmWithdrawalFLO, confirmWithdrawalBTC, - confirmWithdrawalToken + confirmWithdrawalToken, + verifyConvert, + retryConvert, + confirmConvert, + set updateBalance(f) { + updateBalance = f; + } } \ No newline at end of file diff --git a/src/blockchain.js b/src/blockchain.js index 8146250..812258e 100644 --- a/src/blockchain.js +++ b/src/blockchain.js @@ -3,13 +3,17 @@ var collectAndCall; //container for collectAndCall function from backup module var chests; //container for blockchain ids (where assets are stored) -const WITHDRAWAL_MESSAGE = "(withdrawal from market)"; +const WITHDRAWAL_MESSAGE = "(withdrawal from market)", + TYPE_TOKEN = "TOKEN", + TYPE_COIN = "COIN", + TYPE_CONVERT = "CONVERT"; const balance_locked = {}, balance_cache = {}, callbackCollection = { - Coin: {}, - token: {} + [TYPE_COIN]: {}, + [TYPE_TOKEN]: {}, + [TYPE_CONVERT]: {} }; function getBalance(sinkID, asset) { @@ -24,11 +28,11 @@ function getBalance(sinkID, asset) { } } -function getSinkID(amount, asset, sinkList = null) { +function getSinkID(quantity, asset, sinkList = null) { return new Promise((resolve, reject) => { if (!sinkList) sinkList = chests.list.map(s => [s, s in balance_cache ? balance_cache[s][asset] || 0 : 0]) //TODO: improve sorting - .sort((a, b) => b[1] - a[1]).map(x => x[0]); + .sort((a, b) => b[1] - a[1]).map(x => x[0]); if (!sinkList.length) return reject(`Insufficient balance in chests for asset(${asset})`); let sinkID = sinkList.shift(); @@ -36,99 +40,121 @@ function getSinkID(amount, asset, sinkList = null) { if (!(sinkID in balance_cache)) balance_cache[sinkID] = {}; balance_cache[sinkID][asset] = balance; - if (balance > (amount + (sinkID in balance_locked ? balance_locked[sinkID][asset] || 0 : 0))) + if (balance > (quantity + (sinkID in balance_locked ? balance_locked[sinkID][asset] || 0 : 0))) return resolve(sinkID); else - getSinkID(amount, asset, sinkList) - .then(result => resolve(result)) - .catch(error => reject(error)) + getSinkID(quantity, asset, sinkList) + .then(result => resolve(result)) + .catch(error => reject(error)) }).catch(error => { console.error(error); - getSinkID(amount, asset, sinkList) + getSinkID(quantity, asset, sinkList) .then(result => resolve(result)) .catch(error => reject(error)) }); }) } -function sendTx(floID, coin, amount, sinkID, sinkKey) { - switch (coin) { +function sendTx(floID, asset, quantity, sinkID, sinkKey) { + switch (asset) { case "FLO": - return floBlockchainAPI.sendTx(sinkID, floID, amount, sinkKey, WITHDRAWAL_MESSAGE); + return floBlockchainAPI.sendTx(sinkID, floID, quantity, sinkKey, WITHDRAWAL_MESSAGE); case "BTC": + let btc_sinkID = btcOperator.convert.legacy2bech(sinkID), + btc_receiver = btcOperator.convert.legacy2bech(floID); + return btcOperator.sendTx(btc_sinkID, sinkKey, btc_receiver, quantity, null); + default: + return floTokenAPI.sendToken(sinkKey, quantity, floID, WITHDRAWAL_MESSAGE, asset); } } -function sendCoin(floID, coin, amount, id) { - getSinkID(amount, coin).then(sinkID => { +const tableUpdate = { + [TYPE_COIN]: (id, txid) => { + DB.query("UPDATE WithdrawCoin SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) + .then(_ => null).catch(error => console.error(error)) + }, + [TYPE_TOKEN]: (id, txid) => { + DB.query("UPDATE WithdrawToken SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) + .then(_ => null).catch(error => console.error(error)) + }, + [TYPE_CONVERT]: (id, txid) => { + DB.query("UPDATE DirectConvert SET status=?, out_txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) + .then(_ => null).catch(error => console.error(error)); + } +}; + +function sendAsset(floID, asset, quantity, type, id) { + getSinkID(quantity, asset).then(sinkID => { let callback = (sinkKey) => { - //Send Coin to user via blockchain API - sendTx(floID, coin, amount, sinkID, sinkKey).then(txid => { + //Send asset to user via API + sendTx(floID, asset, quantity, sinkID, sinkKey).then(txid => { if (!txid) - throw Error("Transaction not successful"); - //Transaction was successful, Add in DB - DB.query("UPDATE OutputCoin SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) - .then(_ => null).catch(error => console.error(error)); + console.error("Transaction not successful"); + else //Transaction was successful, Add in DB + tableUpdate[type](id, txid); }).catch(error => console.error(error)).finally(_ => { - delete callbackCollection.Coin[id]; - balance_locked[sinkID][coin] -= amount; + delete callbackCollection[type][id]; + balance_locked[sinkID][asset] -= quantity; }); } collectAndCall(sinkID, callback); - callbackCollection.Coin[id] = callback; + callbackCollection[type][id] = callback; if (!(sinkID in balance_locked)) balance_locked[sinkID] = {}; - balance_locked[sinkID][coin] = (balance_locked[sinkID][coin] || 0) + amount; + balance_locked[sinkID][asset] = (balance_locked[sinkID][asset] || 0) + quantity; }).catch(error => console.error(error)) } -function sendCoin_init(floID, coin, amount) { - DB.query("INSERT INTO OutputCoin (floID, coin, amount, status) VALUES (?, ?, ?, ?)", [floID, coin, amount, "PENDING"]) - .then(result => sendCoin(floID, coin, amount, result.insertId)) +function sendCoin_init(floID, coin, quantity) { + DB.query("INSERT INTO WithdrawCoin (floID, coin, amount, status) VALUES (?, ?, ?, ?)", [floID, coin, quantity, "PENDING"]) + .then(result => sendAsset(floID, coin, quantity, TYPE_COIN, result.insertId)) .catch(error => console.error(error)) } -function sendCoin_retry(floID, coin, amount, id) { - if (id in callbackCollection.Coin) - console.debug("A callback is already pending for this FLO transfer"); +function sendCoin_retry(floID, coin, quantity, id) { + if (id in callbackCollection[TYPE_COIN]) + console.debug("A callback is already pending for this Coin transfer"); else - sendCoin(floID, coin, amount, id); + sendAsset(floID, coin, quantity, TYPE_COIN, id); } -function sendToken(floID, token, amount, id) { - getSinkID(amount, token).then(sinkID => { - let callback = (sinkKey) => { - //Send Token to user via token API - floTokenAPI.sendToken(sinkKey, amount, floID, WITHDRAWAL_MESSAGE, token).then(txid => { - if (!txid) - throw Error("Transaction not successful"); - //Transaction was successful, Add in DB - DB.query("UPDATE OutputToken SET status=?, txid=? WHERE id=?", ["WAITING_CONFIRMATION", txid, id]) - .then(_ => null).catch(error => console.error(error)); - }).catch(error => console.error(error)).finally(_ => { - delete callbackCollection.token[id]; - balance_locked[sinkID][token] -= amount; - }); - } - collectAndCall(sinkID, callback); - callbackCollection.token[id] = callback; - if (!(sinkID in balance_locked)) - balance_locked[sinkID] = {}; - balance_locked[sinkID][token] = (balance_locked[sinkID][token] || 0) + amount; - }).catch(error => console.error(error)) -} - -function sendToken_init(floID, token, amount) { - DB.query("INSERT INTO OutputToken (floID, token, amount, status) VALUES (?, ?, ?, ?)", [floID, token, amount, "PENDING"]) - .then(result => sendToken(floID, amount, result.insertId)) +function sendToken_init(floID, token, quantity) { + DB.query("INSERT INTO WithdrawToken (floID, token, amount, status) VALUES (?, ?, ?, ?)", [floID, token, quantity, "PENDING"]) + .then(result => sendAsset(floID, quantity, TYPE_TOKEN, result.insertId)) .catch(error => console.error(error)) } -function sendToken_retry(floID, token, amount, id) { - if (id in callbackCollection.token) - console.debug("A callback is already pending for this token transfer"); +function sendToken_retry(floID, token, quantity, id) { + if (id in callbackCollection[TYPE_TOKEN]) + console.debug("A callback is already pending for this Token transfer"); else - sendToken(floID, token, amount, id); + sendAsset(floID, token, quantity, TYPE_TOKEN, id); +} + +function convertToCoin_init(floID, coin, currency_amount, coin_quantity, id) { + DB.query("UPDATE DirectConvert SET amount=?, quantity=?, status=?, locktime=DEFAULT WHERE id=?", [currency_amount, coin_quantity, "PROCESSING", id]) + .then(result => sendAsset(floID, coin, coin_quantity, TYPE_CONVERT, id)) + .catch(error => console.error(error)) +} + +function convertToCoin_retry(floID, coin, coin_quantity, id) { + if (id in callbackCollection[TYPE_CONVERT]) + console.debug("A callback is already pending for this Coin Convert"); + else + sendAsset(floID, coin, coin_quantity, TYPE_CONVERT, id); +} + +function convertFromCoin_init(floID, currency_amount, coin_quantity, id) { + DB.query("UPDATE DirectConvert SET amount=?, quantity=?, status=?, locktime=DEFAULT WHERE id=?", [currency_amount, coin_quantity, "PROCESSING", id]) + .then(result => sendAsset(floID, floGlobals.currency, currency_amount, TYPE_CONVERT, id)) + .catch(error => console.error(error)) +} + +function convertFromCoin_retry(floID, current_amount, id) { + if (id in callbackCollection[TYPE_CONVERT]) + console.debug("A callback is already pending for this Coin Convert"); + else + sendAsset(floID, floGlobals.currency, current_amount, TYPE_CONVERT, id); } module.exports = { @@ -148,5 +174,13 @@ module.exports = { sendToken: { init: sendToken_init, retry: sendToken_retry + }, + convertToCoin: { + init: convertToCoin_init, + retry: convertToCoin_retry + }, + convertFromCoin: { + init: convertFromCoin_init, + retry: convertFromCoin_retry } } \ No newline at end of file diff --git a/src/conversion.js b/src/conversion.js new file mode 100644 index 0000000..f8829d7 --- /dev/null +++ b/src/conversion.js @@ -0,0 +1,72 @@ +const _sql = require('./_constants').sql; +const allowedConversion = ["BTC"]; + +function getRate() { + return new Promise((resolve, reject) => { + getRate.BTC_USD().then(BTC_rate => { + getRate.USD_INR().then(INR_rate => { + resolve(BTC_rate * INR_rate); + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) +} + +getRate.BTC_USD = function () { + return new Promise((resolve, reject) => { + fetch('https://api.coinlore.net/api/ticker/?id=90').then(response => { + if (response.ok) { + response.json() + .then(result => resolve(result[0].price_usd)) + .catch(error => reject(error)); + } else + reject(response.status); + }).catch(error => reject(error)); + }); +} + +getRate.USD_INR = function () { + return new Promise((resolve, reject) => { + fetch('https://api.exchangerate-api.com/v4/latest/usd').then(response => { + if (response.ok) { + response.json() + .then(result => resolve(result.rates['INR'])) + .catch(error => reject(error)); + } else + reject(response.status); + }).catch(error => reject(error)); + }); +} + +function convertToCoin(floID, txid, coin) { + return new Promise((resolve, reject) => { + if (!allowedConversion.includes(coin)) + return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`)); + DB.query("SELECT status FROM DirectConvert WHERE in_txid=? AND floID=? mode=?", [txid, floID, _sql.CONVERT_MODE_GET]).then(result => { + if (result.length) + return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process")); + else + DB.query("INSERT INTO DirectConvert(floID, in_txid, mode, coin, status) VALUES (?, ?, ?, ?, ?)", [floID, txid, _sql.CONVERT_MODE_GET, coin, "PENDING"]) + .then(result => resolve("Conversion request in process")) + .catch(error => reject(error)); + }).catch(error => reject(error)) + }); +} + +function convertFromCoin(floID, txid, coin) { + if (!allowedConversion.includes(coin)) + return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`)); + DB.query("SELECT status FROM DirectConvert WHERE in_txid=? AND floID=? mode=?", [txid, floID, _sql.CONVERT_MODE_PUT]).then(result => { + if (result.length) + return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process")); + else + DB.query("INSERT INTO DirectConvert(floID, in_txid, mode, coin, status) VALUES (?, ?, ?, ?, ?)", [floID, txid, _sql.CONVERT_MODE_PUT, coin, "PENDING"]) + .then(result => resolve("Conversion request in process")) + .catch(error => reject(error)); + }).catch(error => reject(error)) +} + +module.exports = { + getRate, + convertToCoin, + convertFromCoin +} \ No newline at end of file diff --git a/src/market.js b/src/market.js index cc9dfa3..8f96f7d 100644 --- a/src/market.js +++ b/src/market.js @@ -13,7 +13,7 @@ const { const eCode = require('../docs/scripts/floExchangeAPI').errorCode; -const updateBalance = coupling.updateBalance; +const updateBalance = background.updateBalance = coupling.updateBalance; var DB, assetList; //container for database and allowed assets @@ -256,10 +256,10 @@ function getAccountDetails(floID) { function getUserTransacts(floID) { return new Promise((resolve, reject) => { - DB.query("(SELECT 'deposit' as type, txid, token, amount, status FROM InputToken WHERE floID=?)" + - "UNION (SELECT 'deposit' as type, txid, coin as token, amount, status FROM InputCoin WHERE floID=?)" + - "UNION (SELECT 'withdraw' as type, txid, token, amount, status FROM OutputToken WHERE floID=?)" + - "UNION (SELECT 'withdraw' as type, txid, coin as token, amount, status FROM OutputCoin WHERE floID=?)", + DB.query("(SELECT 'deposit' as type, txid, token, amount, status FROM DepositToken WHERE floID=?)" + + "UNION (SELECT 'deposit' as type, txid, coin as token, amount, status FROM DepositCoin WHERE floID=?)" + + "UNION (SELECT 'withdraw' as type, txid, token, amount, status FROM WithdrawToken WHERE floID=?)" + + "UNION (SELECT 'withdraw' as type, txid, coin as token, amount, status FROM WithdrawCoin WHERE floID=?)", [floID, floID, floID, floID]) .then(result => resolve(result)) .catch(error => reject(error)) @@ -338,7 +338,7 @@ function transferToken(sender, receivers, token) { function depositFLO(floID, txid) { return new Promise((resolve, reject) => { - DB.query("SELECT status FROM InputCoin WHERE txid=? AND floID=? AND coin=?", [txid, floID, "FLO"]).then(result => { + DB.query("SELECT status FROM DepositCoin WHERE txid=? AND floID=? AND coin=?", [txid, floID, "FLO"]).then(result => { if (result.length) { switch (result[0].status) { case "PENDING": @@ -349,7 +349,7 @@ function depositFLO(floID, txid) { return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add coins")); } } else - DB.query("INSERT INTO InputCoin(txid, floID, coin, status) VALUES (?, ?, ?, ?)", [txid, floID, "FLO", "PENDING"]) + DB.query("INSERT INTO DepositCoin(txid, floID, coin, status) VALUES (?, ?, ?, ?)", [txid, floID, "FLO", "PENDING"]) .then(result => resolve("Deposit request in process")) .catch(error => reject(error)); }).catch(error => reject(error)) @@ -375,7 +375,7 @@ function withdrawFLO(floID, amount) { function depositToken(floID, txid) { return new Promise((resolve, reject) => { - DB.query("SELECT status FROM InputToken WHERE txid=? AND floID=?", [txid, floID]).then(result => { + DB.query("SELECT status FROM DepositToken WHERE txid=? AND floID=?", [txid, floID]).then(result => { if (result.length) { switch (result[0].status) { case "PENDING": @@ -386,7 +386,7 @@ function depositToken(floID, txid) { return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add tokens")); } } else - DB.query("INSERT INTO InputToken(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) + DB.query("INSERT INTO DepositToken(txid, floID, status) VALUES (?, ?, ?)", [txid, floID, "PENDING"]) .then(result => resolve("Deposit request in process")) .catch(error => reject(error)); }).catch(error => reject(error)) @@ -472,19 +472,17 @@ function checkDistributor(floID, asset) { }) } -function periodicProcess() { - blockchainReCheck(); -} +//Periodic Process -periodicProcess.start = function() { - periodicProcess.stop(); +function periodicProcess_start() { + periodicProcess_stop(); periodicProcess(); assetList.forEach(asset => coupling.initiate(asset, true)); coupling.price.storeHistory.start(); periodicProcess.instance = setInterval(periodicProcess, PERIOD_INTERVAL); -}; +} -periodicProcess.stop = function() { +function periodicProcess_stop() { if (periodicProcess.instance !== undefined) { clearInterval(periodicProcess.instance); delete periodicProcess.instance; @@ -495,13 +493,13 @@ periodicProcess.stop = function() { var lastSyncBlockHeight = 0; -function blockchainReCheck() { - if (blockchainReCheck.timeout !== undefined) { - clearTimeout(blockchainReCheck.timeout); - delete blockchainReCheck.timeout; +function periodicProcess() { + if (periodicProcess.timeout !== undefined) { + clearTimeout(periodicProcess.timeout); + delete periodicProcess.timeout; } if (!blockchain.chests.list.length) - return blockchainReCheck.timeout = setTimeout(blockchainReCheck, WAIT_TIME); + return periodicProcess.timeout = setTimeout(periodicProcess, WAIT_TIME); floBlockchainAPI.promisedAPI('api/blocks?limit=1').then(result => { if (lastSyncBlockHeight < result.blocks[0].height) { @@ -513,6 +511,9 @@ function blockchainReCheck() { background.confirmWithdrawalFLO(); background.confirmWithdrawalBTC(); background.confirmWithdrawalToken(); + background.verifyConvert(); + background.retryConvert(); + background.confirmConvert(); console.debug("Last Block :", lastSyncBlockHeight); } }).catch(error => console.error(error)); @@ -550,7 +551,10 @@ module.exports = { removeTag, addDistributor, removeDistributor, - periodicProcess, + periodicProcess: { + start: periodicProcess_start, + stop: periodicProcess_stop + }, set DB(db) { DB = db; coupling.DB = db;