Deposit and withdraw request processing

- Processing of Deposit/Withdraw request for FLO/Rupee.
- renamed database's TxQuery to transaction
This commit is contained in:
sairajzero 2021-09-16 03:17:33 +05:30
parent 84b914bc9b
commit 20fd7aaeb1
5 changed files with 513 additions and 31 deletions

View File

@ -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;
}

View File

@ -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 => {

View File

@ -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;

View File

@ -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;
}

View File

@ -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;