Workflow updating files of exchangemarket
This commit is contained in:
parent
e027085d55
commit
567d003ae8
21
exchangemarket/LICENSE
Normal file
21
exchangemarket/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Ranchimall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
81
exchangemarket/README.md
Normal file
81
exchangemarket/README.md
Normal file
@ -0,0 +1,81 @@
|
||||
# Exchange-Market
|
||||
Exchange market for trading assets (FLO and tokens) using rupee#
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-requisite
|
||||
- [X] Nodejs `version >= 12.9` (`--lts` recommended)
|
||||
- [X] MySQL Server `version > 8.0`
|
||||
|
||||
### Download
|
||||
Download the repository using git:
|
||||
```
|
||||
git clone https://github.com/ranchimall/exchange-market.git
|
||||
```
|
||||
|
||||
### Install
|
||||
Install using npm:
|
||||
```
|
||||
cd exchange-market
|
||||
npm install
|
||||
```
|
||||
Finish the configuration when prompted
|
||||
|
||||
### Configuration
|
||||
|
||||
#### General Configuration
|
||||
If not finished during installation, or to re-configure use:
|
||||
```
|
||||
npm run configure
|
||||
```
|
||||
- **port**: Port of the server to run on
|
||||
- **session secret**: A random session secret. (Enter `YES` to automatically randomize it)
|
||||
|
||||
- **MySQL host**: Host of the MySQL server (default: ***localhost***)
|
||||
- **Database name**: Database in which the data should be stored (`<database-name>`) (default: ***exchange***)
|
||||
- **MySQL username**: Username for MySQL (`<sql-username>`)
|
||||
- **MySQL password**: Password for MySQL (`<sql-password>`)
|
||||
|
||||
***Recommended*** *(optional)* Create and use a MySQL user instead of root. Remember to give access to the database to the user.
|
||||
|
||||
#### Set/Reset Node key password
|
||||
If not set during installation, or to reset password, use:
|
||||
```
|
||||
npm run reset-password
|
||||
```
|
||||
- **private key**: Private key of the node
|
||||
- **password**: Password to set for the node (`<password>`)
|
||||
|
||||
**Note**: Private key of the node is encrypted using the `<password>`. Thus use a ***strong*** password.
|
||||
|
||||
### Create Database Schema (MySQL)
|
||||
Create database schema in MySQL
|
||||
```
|
||||
CREATE DATABASE <database-name>;
|
||||
USE <database-name>;
|
||||
SOURCE args/schema.sql;
|
||||
```
|
||||
***Recommended*** *(optional)* Create a MySQL user and grant permissions
|
||||
```
|
||||
CREATE USER '<sql-username>'@'localhost' IDENTIFIED WITH mysql_native_password BY '<sql-password>';
|
||||
GRANT ALL PRIVILEGES ON <database-name>.* TO '<sql-username>'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
### More
|
||||
For help or list of all commands, use
|
||||
```
|
||||
npm run help
|
||||
```
|
||||
|
||||
## Starting the Server
|
||||
After successful installation and configuration using the above steps, Exchange-Node can be started using:
|
||||
```
|
||||
npm start -- -PASSWORD=<password>
|
||||
```
|
||||
|
||||
*(Optional)*
|
||||
`console.debug` is now turned off by default. pass argument `--debug` to turn it on
|
||||
```
|
||||
npm start -- -PASSWORD=<password> --debug
|
||||
```
|
||||
37
exchangemarket/args/param-default.json
Normal file
37
exchangemarket/args/param-default.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"screen": {
|
||||
"height": 1160,
|
||||
"width": 2000,
|
||||
"colorDepth": 24,
|
||||
"availHeight": 1080,
|
||||
"availWidth": 1920,
|
||||
"pixelDepth": 24
|
||||
},
|
||||
"navigator": {
|
||||
"userAgent": "Node/14.17.3 (Linux; aarch64; arm)",
|
||||
"plugins": [{
|
||||
"name": "MySQL",
|
||||
"filename": "mysql",
|
||||
"description": "A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed."
|
||||
}, {
|
||||
"name": "WebSocket",
|
||||
"filename": "ws",
|
||||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js"
|
||||
}, {
|
||||
"name": "Node fetch",
|
||||
"filename": "node-fetch",
|
||||
"description": "A light-weight module that brings window.fetch to node.js"
|
||||
}],
|
||||
"mimeTypes": [{
|
||||
"description": "",
|
||||
"type": "application/pdf",
|
||||
"suffixes": "pdf"
|
||||
}],
|
||||
"cookieEnabled": true,
|
||||
"language": "en-US"
|
||||
},
|
||||
"history": {
|
||||
"length": 512
|
||||
},
|
||||
"location": "protocol://subdomain.example.domain/path"
|
||||
}
|
||||
465
exchangemarket/args/schema.sql
Normal file
465
exchangemarket/args/schema.sql
Normal file
@ -0,0 +1,465 @@
|
||||
/* Blockchain Data */
|
||||
|
||||
CREATE TABLE LastTx(
|
||||
floID CHAR(34) NOT NULL,
|
||||
txid VARCHAR(128),
|
||||
PRIMARY KEY(floID)
|
||||
);
|
||||
|
||||
CREATE TABLE NodeList(
|
||||
floID CHAR(34) NOT NULL,
|
||||
uri TINYTEXT,
|
||||
PRIMARY KEY(floID)
|
||||
);
|
||||
|
||||
CREATE TABLE TagList (
|
||||
tag VARCHAR(50) NOT NULL,
|
||||
sellPriority INT,
|
||||
buyPriority INT,
|
||||
PRIMARY KEY(tag)
|
||||
);
|
||||
|
||||
CREATE TABLE AssetList (
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
initialPrice DECIMAL(16, 8),
|
||||
PRIMARY KEY(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE TrustedList(
|
||||
floID CHAR(34) NOT NULL,
|
||||
PRIMARY KEY(floID)
|
||||
);
|
||||
|
||||
/* User Data */
|
||||
|
||||
CREATE TABLE UserSession (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
proxyKey CHAR(66) NOT NULL,
|
||||
session_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY (id),
|
||||
PRIMARY KEY(floID)
|
||||
);
|
||||
|
||||
CREATE TABLE UserBalance (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
token VARCHAR(64) NOT NULL,
|
||||
quantity DECIMAL(16, 8) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(floID, token),
|
||||
KEY(id)
|
||||
);
|
||||
|
||||
CREATE TABLE SellChips (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
base DECIMAL(16, 8) NOT NULL DEFAULT 0,
|
||||
quantity DECIMAL(16, 8) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE UserTag (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
tag VARCHAR(50) NOT NULL,
|
||||
PRIMARY KEY(floID, tag),
|
||||
KEY (id),
|
||||
FOREIGN KEY (tag) REFERENCES TagList(tag)
|
||||
);
|
||||
|
||||
CREATE TABLE Distributors(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
KEY(id),
|
||||
PRIMARY KEY(floID, asset),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
/* User Requests */
|
||||
|
||||
CREATE TABLE RequestLog(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
request TEXT NOT NULL,
|
||||
sign VARCHAR(160) NOT NULL,
|
||||
proxy BOOLEAN NOT NULL,
|
||||
request_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE (sign)
|
||||
);
|
||||
|
||||
CREATE TABLE SellOrder (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
quantity DECIMAL(16, 8) NOT NULL,
|
||||
minPrice DECIMAL(16, 8) NOT NULL,
|
||||
time_placed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE BuyOrder (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
quantity DECIMAL(16, 8) NOT NULL,
|
||||
maxPrice DECIMAL(16, 8) NOT NULL,
|
||||
time_placed TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE VaultTransactions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
mode TINYINT NOT NULL,
|
||||
asset_type TINYINT NOT NULL,
|
||||
asset VARCHAR(32),
|
||||
amount DECIMAL(16, 8),
|
||||
txid VARCHAR(128),
|
||||
locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
r_status TINYINT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
/* Transaction Data */
|
||||
|
||||
CREATE TABLE PriceHistory (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
rate DECIMAL(16, 8) NOT NULL,
|
||||
rec_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE TransferTransactions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
sender CHAR(34) NOT NULL,
|
||||
receiver TEXT NOT NULL,
|
||||
token VARCHAR(64) NOT NULL,
|
||||
totalAmount DECIMAL(16, 8) NOT NULL,
|
||||
tx_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
txid VARCHAR(66) NOT NULL,
|
||||
KEY(id),
|
||||
PRIMARY KEY(txid)
|
||||
);
|
||||
|
||||
CREATE TABLE TradeTransactions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
seller CHAR(34) NOT NULL,
|
||||
buyer CHAR(34) NOT NULL,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
quantity DECIMAL(16, 8) NOT NULL,
|
||||
unitValue DECIMAL(16, 8) NOT NULL,
|
||||
tx_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
txid VARCHAR(66) NOT NULL,
|
||||
KEY(id),
|
||||
PRIMARY KEY(txid),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
CREATE TABLE AuditTrade(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
rec_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
unit_price DECIMAL(16, 8) NOT NULL,
|
||||
quantity DECIMAL(16, 8) NOT NULL,
|
||||
total_cost DECIMAL(16, 8) NOT NULL,
|
||||
asset VARCHAR(64) NOT NULL,
|
||||
sellerID CHAR(34) NOT NULL,
|
||||
seller_old_asset DECIMAL(16, 8) NOT NULL,
|
||||
seller_new_asset DECIMAL(16, 8) NOT NULL,
|
||||
seller_old_cash DECIMAL(16, 8) NOT NULL,
|
||||
seller_new_cash DECIMAL(16, 8) NOT NULL,
|
||||
buyerID CHAR(34) NOT NULL,
|
||||
buyer_old_asset DECIMAL(16, 8) NOT NULL,
|
||||
buyer_new_asset DECIMAL(16, 8) NOT NULL,
|
||||
buyer_old_cash DECIMAL(16, 8) NOT NULL,
|
||||
buyer_new_cash DECIMAL(16, 8) NOT NULL,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY (asset) REFERENCES AssetList(asset)
|
||||
);
|
||||
|
||||
/* External Service */
|
||||
|
||||
CREATE TABLE BlockchainBonds(
|
||||
bond_id VARCHAR(128) NOT NULL,
|
||||
floID CHAR(34) NOT NULL,
|
||||
amount_in DECIMAL(16, 2) NOT NULL,
|
||||
begin_date DATE NOT NULL,
|
||||
btc_base DECIMAL(16, 2) NOT NULL,
|
||||
usd_base DECIMAL(16, 2) NOT NULL,
|
||||
gain_cut DECIMAL(6, 5) NOT NULL,
|
||||
min_ipa DECIMAL(6, 5) NOT NULL,
|
||||
max_period VARCHAR(10) NOT NULL,
|
||||
lockin_period VARCHAR(10) NOT NULL,
|
||||
close_id VARCHAR(128),
|
||||
amount_out DECIMAL(16, 2),
|
||||
PRIMARY KEY(bond_id)
|
||||
);
|
||||
|
||||
CREATE TABLE CloseBondTransact(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
bond_id VARCHAR(128) NOT NULL,
|
||||
floID CHAR(34) NOT NULL,
|
||||
amount DECIMAL(16, 2) NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
ref_sign VARCHAR(180) NOT NULL,
|
||||
btc_net DECIMAL(16, 2) NOT NULL,
|
||||
usd_net DECIMAL(16, 2) NOT NULL,
|
||||
txid VARCHAR(128),
|
||||
close_id VARCHAR(128),
|
||||
r_status TINYINT NOT NULL,
|
||||
KEY(id),
|
||||
PRIMARY KEY(bond_id),
|
||||
FOREIGN KEY (bond_id) REFERENCES BlockchainBonds(bond_id)
|
||||
);
|
||||
|
||||
CREATE TABLE BobsFund(
|
||||
fund_id VARCHAR(128) NOT NULL,
|
||||
begin_date DATE NOT NULL,
|
||||
btc_base DECIMAL(16, 2) NOT NULL,
|
||||
usd_base DECIMAL(16, 2) NOT NULL,
|
||||
fee DECIMAL(6, 5) NOT NULL,
|
||||
duration VARCHAR(10) NOT NULL,
|
||||
tapout_window VARCHAR(10),
|
||||
tapout_interval VARCHAR(40),
|
||||
PRIMARY KEY(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE BobsFundInvestments(
|
||||
fund_id VARCHAR(128) NOT NULL,
|
||||
floID CHAR(34) NOT NULL,
|
||||
amount_in DECIMAL(16, 2) NOT NULL,
|
||||
close_id VARCHAR(128),
|
||||
amount_out DECIMAL(16, 2),
|
||||
PRIMARY KEY(fund_id, floID),
|
||||
FOREIGN KEY (fund_id) REFERENCES BobsFund(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE CloseFundTransact(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
fund_id VARCHAR(128) NOT NULL,
|
||||
floID CHAR(34) NOT NULL,
|
||||
amount DECIMAL(16, 2) NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
ref_sign VARCHAR(180) NOT NULL,
|
||||
btc_net DECIMAL(16, 2) NOT NULL,
|
||||
usd_net DECIMAL(16, 2) NOT NULL,
|
||||
txid VARCHAR(128),
|
||||
close_id VARCHAR(128),
|
||||
r_status TINYINT NOT NULL,
|
||||
KEY(id),
|
||||
PRIMARY KEY(fund_id, floID),
|
||||
FOREIGN KEY (fund_id) REFERENCES BobsFund(fund_id)
|
||||
);
|
||||
|
||||
CREATE TABLE ConvertFund(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
amount DECIMAL(16, 8),
|
||||
coin VARCHAR(8) NOT NULL,
|
||||
quantity DECIMAL(16, 8),
|
||||
mode TINYINT NOT NULL,
|
||||
txid VARCHAR(128),
|
||||
r_status TINYINT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
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 TINYINT NOT NULL,
|
||||
rate DECIMAL(16, 2),
|
||||
in_txid VARCHAR(128),
|
||||
out_txid VARCHAR(128),
|
||||
locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
r_status TINYINT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE TABLE RefundConvert(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
asset_type TINYINT NOT NULL,
|
||||
asset VARCHAR(32) NOT NULL,
|
||||
amount DECIMAL(16, 8),
|
||||
in_txid VARCHAR(128),
|
||||
out_txid VARCHAR(128),
|
||||
locktime TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
r_status TINYINT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
/* Backup Feature (Tables & Triggers) */
|
||||
|
||||
CREATE TABLE _backup (
|
||||
t_name VARCHAR(64),
|
||||
id INT,
|
||||
mode BOOLEAN DEFAULT TRUE,
|
||||
u_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(t_name, id)
|
||||
);
|
||||
|
||||
CREATE table _backupCache(
|
||||
id INT AUTO_INCREMENT,
|
||||
t_name VARCHAR(64),
|
||||
data_cache LONGTEXT,
|
||||
fail BOOLEAN,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE TABLE sinkShares(
|
||||
num INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
share TEXT,
|
||||
time_stored TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(num)
|
||||
);
|
||||
|
||||
CREATE TABLE discardedSinks(
|
||||
id INT AUTO_INCREMENT,
|
||||
floID CHAR(34) NOT NULL,
|
||||
discard_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY(id),
|
||||
PRIMARY KEY(floID)
|
||||
);
|
||||
|
||||
CREATE TRIGGER discardedSinks_I AFTER INSERT ON discardedSinks
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('discardedSinks', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER discardedSinks_U AFTER UPDATE ON discardedSinks
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('discardedSinks', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER discardedSinks_D AFTER DELETE ON discardedSinks
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('discardedSinks', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER RequestLog_I AFTER INSERT ON RequestLog
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RequestLog', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER RequestLog_U AFTER UPDATE ON RequestLog
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RequestLog', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER RequestLog_D AFTER DELETE ON RequestLog
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RequestLog', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER UserSession_I AFTER INSERT ON UserSession
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserSession', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserSession_U AFTER UPDATE ON UserSession
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserSession', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserSession_D AFTER DELETE ON UserSession
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserSession', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER UserBalance_I AFTER INSERT ON UserBalance
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserBalance', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserBalance_U AFTER UPDATE ON UserBalance
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserBalance', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserBalance_D AFTER DELETE ON UserBalance
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserBalance', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER SellChips_I AFTER INSERT ON SellChips
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellChips', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER SellChips_U AFTER UPDATE ON SellChips
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellChips', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER SellChips_D AFTER DELETE ON SellChips
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellChips', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER SellOrder_I AFTER INSERT ON SellOrder
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellOrder', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER SellOrder_U AFTER UPDATE ON SellOrder
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellOrder', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER SellOrder_D AFTER DELETE ON SellOrder
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('SellOrder', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER BuyOrder_I AFTER INSERT ON BuyOrder
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('BuyOrder', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER BuyOrder_U AFTER UPDATE ON BuyOrder
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('BuyOrder', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
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, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER VaultTransactions_I AFTER INSERT ON VaultTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('VaultTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER VaultTransactions_U AFTER UPDATE ON VaultTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('VaultTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER VaultTransactions_D AFTER DELETE ON VaultTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('VaultTransactions', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER CloseBondTransact_I AFTER INSERT ON CloseBondTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseBondTransact', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER CloseBondTransact_U AFTER UPDATE ON CloseBondTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseBondTransact', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER CloseBondTransact_D AFTER DELETE ON CloseBondTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseBondTransact', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER CloseFundTransact_I AFTER INSERT ON CloseFundTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseFundTransact', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER CloseFundTransact_U AFTER UPDATE ON CloseFundTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseFundTransact', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER CloseFundTransact_D AFTER DELETE ON CloseFundTransact
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('CloseFundTransact', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER ConvertFund_I AFTER INSERT ON ConvertFund
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('ConvertFund', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER ConvertFund_U AFTER UPDATE ON ConvertFund
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('ConvertFund', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER ConvertFund_D AFTER DELETE ON ConvertFund
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('ConvertFund', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=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, u_time=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, u_time=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, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER RefundConvert_I AFTER INSERT ON RefundConvert
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RefundConvert', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER RefundConvert_U AFTER UPDATE ON RefundConvert
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RefundConvert', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER RefundConvert_D AFTER DELETE ON RefundConvert
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('RefundConvert', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=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, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserTag_U AFTER UPDATE ON UserTag
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserTag', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER UserTag_D AFTER DELETE ON UserTag
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('UserTag', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER Distributors_I AFTER INSERT ON Distributors
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Distributors', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER Distributors_U AFTER UPDATE ON Distributors
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Distributors', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER Distributors_D AFTER DELETE ON Distributors
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('Distributors', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER PriceHistory_I AFTER INSERT ON PriceHistory
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('PriceHistory', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER PriceHistory_U AFTER UPDATE ON PriceHistory
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('PriceHistory', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER PriceHistory_D AFTER DELETE ON PriceHistory
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('PriceHistory', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER AuditTrade_I AFTER INSERT ON AuditTrade
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('AuditTrade', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER AuditTrade_U AFTER UPDATE ON AuditTrade
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('AuditTrade', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER AuditTrade_D AFTER DELETE ON AuditTrade
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('AuditTrade', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER TradeTransactions_I AFTER INSERT ON TradeTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TradeTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER TradeTransactions_U AFTER UPDATE ON TradeTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TradeTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER TradeTransactions_D AFTER DELETE ON TradeTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TradeTransactions', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
|
||||
CREATE TRIGGER TransferTransactions_I AFTER INSERT ON TransferTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TransferTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER TransferTransactions_U AFTER UPDATE ON TransferTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TransferTransactions', NEW.id) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=DEFAULT;
|
||||
CREATE TRIGGER TransferTransactions_D AFTER DELETE ON TransferTransactions
|
||||
FOR EACH ROW INSERT INTO _backup (t_name, id) VALUES ('TransferTransactions', OLD.id) ON DUPLICATE KEY UPDATE mode=NULL, u_time=DEFAULT;
|
||||
31
exchangemarket/args/truncateAll.sql
Normal file
31
exchangemarket/args/truncateAll.sql
Normal file
@ -0,0 +1,31 @@
|
||||
/* Node data */
|
||||
TRUNCATE _backup;
|
||||
TRUNCATE _backupCache;
|
||||
TRUNCATE AuditTrade;
|
||||
TRUNCATE BuyOrder;
|
||||
TRUNCATE Distributors;
|
||||
TRUNCATE VaultTransactions;
|
||||
TRUNCATE PriceHistory;
|
||||
TRUNCATE RequestLog;
|
||||
TRUNCATE SellOrder;
|
||||
TRUNCATE UserBalance;
|
||||
TRUNCATE UserSession;
|
||||
TRUNCATE UserTag;
|
||||
TRUNCATE TransferTransactions;
|
||||
TRUNCATE TradeTransactions;
|
||||
TRUNCATE SellChips;
|
||||
TRUNCATE CloseBondTransact;
|
||||
TRUNCATE CloseFundTransact;
|
||||
TRUNCATE ConvertFund;
|
||||
TRUNCATE DirectConvert;
|
||||
TRUNCATE RefundConvert;
|
||||
|
||||
/* Blockchain data */
|
||||
TRUNCATE LastTx;
|
||||
TRUNCATE NodeList;
|
||||
TRUNCATE TrustedList;
|
||||
DELETE FROM BlockchainBonds;
|
||||
TRUNCATE BobsFundInvestments;
|
||||
DELETE FROM BobsFund;
|
||||
DELETE FROM TagList;
|
||||
DELETE FROM AssetList;
|
||||
56
exchangemarket/debug/checksum-db.js
Normal file
56
exchangemarket/debug/checksum-db.js
Normal file
@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
let _I = "";
|
||||
for (let arg of process.argv)
|
||||
if (/^-I=/.test(arg)) {
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
break;
|
||||
}
|
||||
|
||||
const DB = require('../src/database');
|
||||
|
||||
const ignoreTables = ['_backupCache', 'sinkShares'];
|
||||
var ignoreTables_regex = new RegExp(ignoreTables.join("|"), "i");
|
||||
function listTables() {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SHOW TABLES").then(result => {
|
||||
let tables = [];
|
||||
for (let i in result)
|
||||
for (let j in result[i])
|
||||
if (!ignoreTables_regex.test(result[i][j]))
|
||||
tables.push(result[i][j]);
|
||||
resolve(tables);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checksumTable(table) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("CHECKSUM TABLE ??", [table]).then(result => {
|
||||
let checksum = result[0].Checksum;
|
||||
DB.query("SELECT COUNT(*) AS rec_count FROM ??", [table])
|
||||
.then(result => resolve({ table, rec_count: result[0].rec_count, checksum }))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function CheckDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = require(`../args/config${_I}.json`);
|
||||
DB.connect(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(pool => {
|
||||
listTables().then(tables => {
|
||||
Promise.allSettled(tables.map(t => checksumTable(t))).then(results => {
|
||||
let records = results.filter(r => r.status === "fulfilled").map(r => r.value);
|
||||
console.table(records);
|
||||
let errors = results.filter(r => r.status === "rejected");
|
||||
if (errors.length)
|
||||
console.error(errors.map(r => r.reason));
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
CheckDB().then(_ => process.exit(0)).catch(error => { console.error(error); process.exit(1); })
|
||||
1
exchangemarket/docs/css/back.svg
Normal file
1
exchangemarket/docs/css/back.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:#ffcdab;}.b{fill:#fff4c5;}</style></defs><title>back</title><path class="a" d="M1343.5-.5c33.54,48.75,50.34,166,277.18,193.18C1808.39,215.21,1899.21,313.38,1920,364c-.33-146.23-.17-218.31-.5-364.54Z"/><path class="b" d="M1343.5-.5a288.26,288.26,0,0,0,159,88.78c48.38,9.32,76.65,13.77,132.58,22.68,93.5,14.9,141.37,56.9,170.42,80.54a466.36,466.36,0,0,1,115,139q-.49-165.49-1-331Z"/></svg>
|
||||
|
After Width: | Height: | Size: 499 B |
33
exchangemarket/docs/css/bg-art2.svg
Normal file
33
exchangemarket/docs/css/bg-art2.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<g filter="url(#filter0_d)">
|
||||
<rect x="312" y="323" width="240" height="140" rx="18" transform="rotate(30 312 323)" fill="#1C783B"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d)">
|
||||
<rect x="431.283" y="302" width="240" height="140" rx="18" transform="rotate(50.0235 431.283 302)" fill="#2DBD5E"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="225" y="320" width="301.846" height="265.244" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-5" dy="9"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d" x="307" y="299" width="285.477" height="297.86" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-5" dy="9"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1736
exchangemarket/docs/css/main.css
Normal file
1736
exchangemarket/docs/css/main.css
Normal file
File diff suppressed because it is too large
Load Diff
1
exchangemarket/docs/css/main.min.css
vendored
Normal file
1
exchangemarket/docs/css/main.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1637
exchangemarket/docs/css/main.scss
Normal file
1637
exchangemarket/docs/css/main.scss
Normal file
File diff suppressed because it is too large
Load Diff
2985
exchangemarket/docs/index.html
Normal file
2985
exchangemarket/docs/index.html
Normal file
File diff suppressed because one or more lines are too long
996
exchangemarket/docs/scripts/btcOperator.js
Normal file
996
exchangemarket/docs/scripts/btcOperator.js
Normal file
@ -0,0 +1,996 @@
|
||||
(function (EXPORTS) { //btcOperator v1.1.3b
|
||||
/* BTC Crypto and API Operator */
|
||||
const btcOperator = EXPORTS;
|
||||
|
||||
//This library uses API provided by chain.so (https://chain.so/)
|
||||
const URL = "https://blockchain.info/";
|
||||
|
||||
const DUST_AMT = 546,
|
||||
MIN_FEE_UPDATE = 219;
|
||||
|
||||
const fetch_api = btcOperator.fetch = function (api, json_res = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.debug(URL + api);
|
||||
fetch(URL + api).then(response => {
|
||||
if (response.ok) {
|
||||
(json_res ? response.json() : response.text())
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
response.json()
|
||||
.then(result => reject(result))
|
||||
.catch(error => reject(error))
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
};
|
||||
|
||||
const SATOSHI_IN_BTC = 1e8;
|
||||
|
||||
const util = btcOperator.util = {};
|
||||
|
||||
util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
|
||||
util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
|
||||
|
||||
function get_fee_rate() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('https://api.blockchain.info/mempool/fees').then(response => {
|
||||
if (response.ok)
|
||||
response.json()
|
||||
.then(result => resolve(util.Sat_to_BTC(result.regular)))
|
||||
.catch(error => reject(error));
|
||||
else
|
||||
reject(response);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => {
|
||||
let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction';
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: "rawtx=" + rawTxHex
|
||||
}).then(response => {
|
||||
response.text().then(resultText => {
|
||||
let r = resultText.match(/<result>.*<\/result>/);
|
||||
if (!r)
|
||||
reject(resultText);
|
||||
else {
|
||||
r = r.pop().replace('<result>', '').replace('</result>', '');
|
||||
if (r == '1') {
|
||||
let txid = resultText.match(/<txid>.*<\/txid>/).pop().replace('<txid>', '').replace('</txid>', '');
|
||||
resolve(txid);
|
||||
} else if (r == '0') {
|
||||
let error = resultText.match(/<response>.*<\/response>/).pop().replace('<response>', '').replace('</response>', '');
|
||||
reject(decodeURIComponent(error.replace(/\+/g, " ")));
|
||||
} else reject(resultText);
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
Object.defineProperties(btcOperator, {
|
||||
newKeys: {
|
||||
get: () => {
|
||||
let r = coinjs.newKeys();
|
||||
r.segwitAddress = coinjs.segwitAddress(r.pubkey).address;
|
||||
r.bech32Address = coinjs.bech32Address(r.pubkey).address;
|
||||
return r;
|
||||
}
|
||||
},
|
||||
pubkey: {
|
||||
value: key => key.length >= 66 ? key : (key.length == 64 ? coinjs.newPubkey(key) : coinjs.wif2pubkey(key).pubkey)
|
||||
},
|
||||
address: {
|
||||
value: (key, prefix = undefined) => coinjs.pubkey2address(btcOperator.pubkey(key), prefix)
|
||||
},
|
||||
segwitAddress: {
|
||||
value: key => coinjs.segwitAddress(btcOperator.pubkey(key)).address
|
||||
},
|
||||
bech32Address: {
|
||||
value: key => coinjs.bech32Address(btcOperator.pubkey(key)).address
|
||||
}
|
||||
});
|
||||
|
||||
coinjs.compressed = true;
|
||||
|
||||
const verifyKey = btcOperator.verifyKey = function (addr, key) {
|
||||
if (!addr || !key)
|
||||
return undefined;
|
||||
switch (coinjs.addressDecode(addr).type) {
|
||||
case "standard":
|
||||
return btcOperator.address(key) === addr;
|
||||
case "multisig":
|
||||
return btcOperator.segwitAddress(key) === addr;
|
||||
case "bech32":
|
||||
return btcOperator.bech32Address(key) === addr;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const validateAddress = btcOperator.validateAddress = function (addr) {
|
||||
if (!addr)
|
||||
return undefined;
|
||||
let type = coinjs.addressDecode(addr).type;
|
||||
if (["standard", "multisig", "bech32", "multisigBech32"].includes(type))
|
||||
return type;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
btcOperator.multiSigAddress = function (pubKeys, minRequired, bech32 = true) {
|
||||
if (!Array.isArray(pubKeys))
|
||||
throw "pubKeys must be an array of public keys";
|
||||
else if (pubKeys.length < minRequired)
|
||||
throw "minimum required should be less than the number of pubKeys";
|
||||
if (bech32)
|
||||
return coinjs.pubkeys2MultisigAddressBech32(pubKeys, minRequired);
|
||||
else
|
||||
return coinjs.pubkeys2MultisigAddress(pubKeys, minRequired);
|
||||
}
|
||||
|
||||
btcOperator.decodeRedeemScript = function (redeemScript, bech32 = true) {
|
||||
let script = coinjs.script();
|
||||
let decoded = (bech32) ?
|
||||
script.decodeRedeemScriptBech32(redeemScript) :
|
||||
script.decodeRedeemScript(redeemScript);
|
||||
if (!decoded)
|
||||
return null;
|
||||
return {
|
||||
address: decoded.address,
|
||||
pubKeys: decoded.pubkeys,
|
||||
redeemScript: decoded.redeemscript,
|
||||
required: decoded.signaturesRequired
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//convert from one blockchain to another blockchain (target version)
|
||||
btcOperator.convert = {};
|
||||
|
||||
btcOperator.convert.wif = function (source_wif, target_version = coinjs.priv) {
|
||||
let keyHex = util.decodeLegacy(source_wif).hex;
|
||||
if (!keyHex || keyHex.length < 66 || !/01$/.test(keyHex))
|
||||
return null;
|
||||
else
|
||||
return util.encodeLegacy(keyHex, target_version);
|
||||
}
|
||||
|
||||
btcOperator.convert.legacy2legacy = function (source_addr, target_version = coinjs.pub) {
|
||||
let rawHex = util.decodeLegacy(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else
|
||||
return util.encodeLegacy(rawHex, target_version);
|
||||
}
|
||||
|
||||
btcOperator.convert.legacy2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
|
||||
let rawHex = util.decodeLegacy(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else
|
||||
return util.encodeBech32(rawHex, target_version, target_hrp);
|
||||
}
|
||||
|
||||
btcOperator.convert.bech2bech = function (source_addr, target_version = coinjs.bech32.version, target_hrp = coinjs.bech32.hrp) {
|
||||
let rawHex = util.decodeBech32(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else
|
||||
return util.encodeBech32(rawHex, target_version, target_hrp);
|
||||
}
|
||||
|
||||
btcOperator.convert.bech2legacy = function (source_addr, target_version = coinjs.pub) {
|
||||
let rawHex = util.decodeBech32(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else
|
||||
return util.encodeLegacy(rawHex, target_version);
|
||||
}
|
||||
|
||||
btcOperator.convert.multisig2multisig = function (source_addr, target_version = coinjs.multisig) {
|
||||
let rawHex = util.decodeLegacy(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else
|
||||
return util.encodeLegacy(rawHex, target_version);
|
||||
}
|
||||
|
||||
btcOperator.convert.bech2multisig = function (source_addr, target_version = coinjs.multisig) {
|
||||
let rawHex = util.decodeBech32(source_addr).hex;
|
||||
if (!rawHex)
|
||||
return null;
|
||||
else {
|
||||
rawHex = Crypto.util.bytesToHex(ripemd160(Crypto.util.hexToBytes(rawHex), { asBytes: true }));
|
||||
return util.encodeLegacy(rawHex, target_version);
|
||||
}
|
||||
}
|
||||
|
||||
util.decodeLegacy = function (source) {
|
||||
var decode = coinjs.base58decode(source);
|
||||
var raw = decode.slice(0, decode.length - 4),
|
||||
checksum = decode.slice(decode.length - 4);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
|
||||
return false;
|
||||
let version = raw.shift();
|
||||
return {
|
||||
version: version,
|
||||
hex: Crypto.util.bytesToHex(raw)
|
||||
}
|
||||
}
|
||||
|
||||
util.encodeLegacy = function (hex, version) {
|
||||
var bytes = Crypto.util.hexToBytes(hex);
|
||||
bytes.unshift(version);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
var checksum = hash.slice(0, 4);
|
||||
return coinjs.base58encode(bytes.concat(checksum));
|
||||
}
|
||||
|
||||
util.decodeBech32 = function (source) {
|
||||
let decode = coinjs.bech32_decode(source);
|
||||
if (!decode)
|
||||
return false;
|
||||
var raw = decode.data;
|
||||
let version = raw.shift();
|
||||
raw = coinjs.bech32_convert(raw, 5, 8, false);
|
||||
return {
|
||||
hrp: decode.hrp,
|
||||
version: version,
|
||||
hex: Crypto.util.bytesToHex(raw)
|
||||
}
|
||||
}
|
||||
|
||||
util.encodeBech32 = function (hex, version, hrp) {
|
||||
var bytes = Crypto.util.hexToBytes(hex);
|
||||
bytes = coinjs.bech32_convert(bytes, 8, 5, true);
|
||||
bytes.unshift(version)
|
||||
return coinjs.bech32_encode(hrp, bytes);
|
||||
}
|
||||
|
||||
//BTC blockchain APIs
|
||||
|
||||
btcOperator.getBalance = addr => new Promise((resolve, reject) => {
|
||||
fetch_api(`q/addressbalance/${addr}`)
|
||||
.then(result => resolve(util.Sat_to_BTC(result)))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
|
||||
const BASE_TX_SIZE = 12,
|
||||
BASE_INPUT_SIZE = 41,
|
||||
LEGACY_INPUT_SIZE = 107,
|
||||
BECH32_INPUT_SIZE = 27,
|
||||
BECH32_MULTISIG_INPUT_SIZE = 35,
|
||||
SEGWIT_INPUT_SIZE = 59,
|
||||
MULTISIG_INPUT_SIZE_ES = 351,
|
||||
BASE_OUTPUT_SIZE = 9,
|
||||
LEGACY_OUTPUT_SIZE = 25,
|
||||
BECH32_OUTPUT_SIZE = 23,
|
||||
BECH32_MULTISIG_OUTPUT_SIZE = 34,
|
||||
SEGWIT_OUTPUT_SIZE = 23;
|
||||
|
||||
function _redeemScript(addr, key) {
|
||||
let decode = coinjs.addressDecode(addr);
|
||||
switch (decode.type) {
|
||||
case "standard":
|
||||
return false;
|
||||
case "multisig":
|
||||
return key ? coinjs.segwitAddress(btcOperator.pubkey(key)).redeemscript : null;
|
||||
case "bech32":
|
||||
return decode.redeemscript;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _sizePerInput(addr, rs) {
|
||||
switch (coinjs.addressDecode(addr).type) {
|
||||
case "standard":
|
||||
return BASE_INPUT_SIZE + LEGACY_INPUT_SIZE;
|
||||
case "bech32":
|
||||
return BASE_INPUT_SIZE + BECH32_INPUT_SIZE;
|
||||
case "multisigBech32":
|
||||
return BASE_INPUT_SIZE + BECH32_MULTISIG_INPUT_SIZE;
|
||||
case "multisig":
|
||||
switch (coinjs.script().decodeRedeemScript(rs).type) {
|
||||
case "segwit__":
|
||||
return BASE_INPUT_SIZE + SEGWIT_INPUT_SIZE;
|
||||
case "multisig__":
|
||||
return BASE_INPUT_SIZE + MULTISIG_INPUT_SIZE_ES;
|
||||
default:
|
||||
return null;
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function _sizePerOutput(addr) {
|
||||
switch (coinjs.addressDecode(addr).type) {
|
||||
case "standard":
|
||||
return BASE_OUTPUT_SIZE + LEGACY_OUTPUT_SIZE;
|
||||
case "bech32":
|
||||
return BASE_OUTPUT_SIZE + BECH32_OUTPUT_SIZE;
|
||||
case "multisigBech32":
|
||||
return BASE_OUTPUT_SIZE + BECH32_MULTISIG_OUTPUT_SIZE;
|
||||
case "multisig":
|
||||
return BASE_OUTPUT_SIZE + SEGWIT_OUTPUT_SIZE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function validateTxParameters(parameters) {
|
||||
let invalids = [];
|
||||
//sender-ids
|
||||
if (parameters.senders) {
|
||||
if (!Array.isArray(parameters.senders))
|
||||
parameters.senders = [parameters.senders];
|
||||
parameters.senders.forEach(id => !validateAddress(id) ? invalids.push(id) : null);
|
||||
if (invalids.length)
|
||||
throw "Invalid senders:" + invalids;
|
||||
}
|
||||
if (parameters.privkeys) {
|
||||
if (!Array.isArray(parameters.privkeys))
|
||||
parameters.privkeys = [parameters.privkeys];
|
||||
if (parameters.senders.length != parameters.privkeys.length)
|
||||
throw "Array length for senders and privkeys should be equal";
|
||||
parameters.senders.forEach((id, i) => {
|
||||
let key = parameters.privkeys[i];
|
||||
if (!verifyKey(id, key)) //verify private-key
|
||||
invalids.push(id);
|
||||
if (key.length === 64) //convert Hex to WIF if needed
|
||||
parameters.privkeys[i] = coinjs.privkey2wif(key);
|
||||
});
|
||||
if (invalids.length)
|
||||
throw "Invalid private key for address:" + invalids;
|
||||
}
|
||||
//receiver-ids (and change-id)
|
||||
if (!Array.isArray(parameters.receivers))
|
||||
parameters.receivers = [parameters.receivers];
|
||||
parameters.receivers.forEach(id => !validateAddress(id) ? invalids.push(id) : null);
|
||||
if (invalids.length)
|
||||
throw "Invalid receivers:" + invalids;
|
||||
if (parameters.change_address && !validateAddress(parameters.change_address))
|
||||
throw "Invalid change_address:" + parameters.change_address;
|
||||
//fee and amounts
|
||||
if ((typeof parameters.fee !== "number" || parameters.fee <= 0) && parameters.fee !== null) //fee = null (auto calc)
|
||||
throw "Invalid fee:" + parameters.fee;
|
||||
if (!Array.isArray(parameters.amounts))
|
||||
parameters.amounts = [parameters.amounts];
|
||||
if (parameters.receivers.length != parameters.amounts.length)
|
||||
throw "Array length for receivers and amounts should be equal";
|
||||
parameters.amounts.forEach(a => typeof a !== "number" || a <= 0 ? invalids.push(a) : null);
|
||||
if (invalids.length)
|
||||
throw "Invalid amounts:" + invalids;
|
||||
//return
|
||||
return parameters;
|
||||
}
|
||||
|
||||
function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_address, fee_from_receiver) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
|
||||
const tx = coinjs.transaction();
|
||||
let output_size = addOutputs(tx, receivers, amounts, change_address);
|
||||
addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver).then(result => {
|
||||
if (result.change_amount > 0 && result.change_amount > result.fee) //add change amount if any (ignore dust change)
|
||||
tx.outs[tx.outs.length - 1].value = util.BTC_to_Sat(result.change_amount); //values are in satoshi
|
||||
if (fee_from_receiver) { //deduce fee from receivers if fee_from_receiver
|
||||
let fee_remaining = util.BTC_to_Sat(result.fee);
|
||||
for (let i = 0; i < tx.outs.length - 1 && fee_remaining > 0; i++) {
|
||||
if (fee_remaining < tx.outs[i].value) {
|
||||
tx.outs[i].value -= fee_remaining;
|
||||
fee_remaining = 0;
|
||||
} else {
|
||||
fee_remaining -= tx.outs[i].value;
|
||||
tx.outs[i].value = 0;
|
||||
}
|
||||
}
|
||||
if (fee_remaining > 0)
|
||||
return reject("Send amount is less than fee");
|
||||
|
||||
}
|
||||
//remove all output with value less than DUST amount
|
||||
let filtered_outputs = [], dust_value = 0;
|
||||
tx.outs.forEach(o => o.value >= DUST_AMT ? filtered_outputs.push(o) : dust_value += o.value);
|
||||
tx.outs = filtered_outputs;
|
||||
//update result values
|
||||
result.fee += util.Sat_to_BTC(dust_value);
|
||||
result.output_size = output_size;
|
||||
result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0);
|
||||
result.total_size = BASE_TX_SIZE + output_size + result.input_size;
|
||||
result.transaction = tx;
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (fee !== null) {
|
||||
addUTXOs(tx, senders, redeemScripts, fee_from_receiver ? total_amount : total_amount + fee, false).then(result => {
|
||||
result.fee = fee;
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
} else {
|
||||
get_fee_rate().then(fee_rate => {
|
||||
let net_fee = BASE_TX_SIZE * fee_rate;
|
||||
net_fee += (output_size * fee_rate);
|
||||
(fee_from_receiver ?
|
||||
addUTXOs(tx, senders, redeemScripts, total_amount, false) :
|
||||
addUTXOs(tx, senders, redeemScripts, total_amount + net_fee, fee_rate)
|
||||
).then(result => {
|
||||
result.fee = parseFloat((net_fee + (result.input_size * fee_rate)).toFixed(8));
|
||||
result.fee_rate = fee_rate;
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
required_amount = parseFloat(required_amount.toFixed(8));
|
||||
if (typeof rec_args.n === "undefined") {
|
||||
rec_args.n = 0;
|
||||
rec_args.input_size = 0;
|
||||
rec_args.input_amount = 0;
|
||||
}
|
||||
if (required_amount <= 0)
|
||||
return resolve({
|
||||
input_size: rec_args.input_size,
|
||||
input_amount: rec_args.input_amount,
|
||||
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
|
||||
});
|
||||
else if (rec_args.n >= senders.length)
|
||||
return reject("Insufficient Balance");
|
||||
let addr = senders[rec_args.n],
|
||||
rs = redeemScripts[rec_args.n];
|
||||
let addr_type = coinjs.addressDecode(addr).type;
|
||||
let size_per_input = _sizePerInput(addr, rs);
|
||||
fetch_api(`unspent?active=${addr}`).then(result => {
|
||||
let utxos = result.unspent_outputs;
|
||||
//console.debug("add-utxo", addr, rs, required_amount, utxos);
|
||||
for (let i = 0; i < utxos.length && required_amount > 0; i++) {
|
||||
if (!utxos[i].confirmations) //ignore unconfirmed utxo
|
||||
continue;
|
||||
var script;
|
||||
if (!rs || !rs.length) //legacy script
|
||||
script = utxos[i].script;
|
||||
else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_type === 'multisigBech32') {
|
||||
//redeemScript for segwit/bech32 and multisig (bech32)
|
||||
let s = coinjs.script();
|
||||
s.writeBytes(Crypto.util.hexToBytes(rs));
|
||||
s.writeOp(0);
|
||||
s.writeBytes(coinjs.numToBytes(utxos[i].value.toFixed(0), 8));
|
||||
script = Crypto.util.bytesToHex(s.buffer);
|
||||
} else //redeemScript for multisig (segwit)
|
||||
script = rs;
|
||||
tx.addinput(utxos[i].tx_hash_big_endian, utxos[i].tx_output_n, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee
|
||||
//update track values
|
||||
rec_args.input_size += size_per_input;
|
||||
rec_args.input_amount += util.Sat_to_BTC(utxos[i].value);
|
||||
required_amount -= util.Sat_to_BTC(utxos[i].value);
|
||||
if (fee_rate) //automatic fee calculation (dynamic)
|
||||
required_amount += size_per_input * fee_rate;
|
||||
}
|
||||
rec_args.n += 1;
|
||||
addUTXOs(tx, senders, redeemScripts, required_amount, fee_rate, rec_args)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function addOutputs(tx, receivers, amounts, change_address) {
|
||||
let size = 0;
|
||||
for (let i in receivers) {
|
||||
tx.addoutput(receivers[i], amounts[i]);
|
||||
size += _sizePerOutput(receivers[i]);
|
||||
}
|
||||
tx.addoutput(change_address, 0);
|
||||
size += _sizePerOutput(change_address);
|
||||
return size;
|
||||
}
|
||||
|
||||
/*
|
||||
function autoFeeCalc(tx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
get_fee_rate().then(fee_rate => {
|
||||
let tx_size = tx.size();
|
||||
for (var i = 0; i < this.ins.length; i++)
|
||||
switch (tx.extractScriptKey(i).type) {
|
||||
case 'scriptpubkey':
|
||||
tx_size += SIGN_SIZE;
|
||||
break;
|
||||
case 'segwit':
|
||||
case 'multisig':
|
||||
tx_size += SIGN_SIZE * 0.25;
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown script-type');
|
||||
tx_size += SIGN_SIZE;
|
||||
}
|
||||
resolve(tx_size * fee_rate);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function editFee(tx, current_fee, target_fee, index = -1) {
|
||||
//values are in satoshi
|
||||
index = parseInt(index >= 0 ? index : tx.outs.length - index);
|
||||
if (index < 0 || index >= tx.outs.length)
|
||||
throw "Invalid index";
|
||||
let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places
|
||||
current_value = tx.outs[index].value; //could be BigInterger
|
||||
if (edit_value < 0 && edit_value > current_value)
|
||||
throw "Insufficient value at vout";
|
||||
tx.outs[index].value = current_value instanceof BigInteger ?
|
||||
current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value);
|
||||
}
|
||||
*/
|
||||
|
||||
function tx_fetch_for_editing(tx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid
|
||||
getTx.hex(tx)
|
||||
.then(txhex => resolve(deserializeTx(txhex)))
|
||||
.catch(error => reject(error))
|
||||
} else resolve(deserializeTx(tx));
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
btcOperator.editFee = function (tx_hex, new_fee, private_keys, change_only = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(private_keys))
|
||||
private_keys = [private_keys];
|
||||
tx_fetch_for_editing(tx_hex).then(tx => {
|
||||
parseTransaction(tx).then(tx_parsed => {
|
||||
if (tx_parsed.fee >= new_fee)
|
||||
return reject("Fees can only be increased");
|
||||
|
||||
//editable addresses in output values (for fee increase)
|
||||
var edit_output_address = new Set();
|
||||
if (change_only === true) //allow only change values (ie, sender address) to be edited to inc fee
|
||||
tx_parsed.inputs.forEach(inp => edit_output_address.add(inp.address));
|
||||
else if (change_only === false) //allow all output values to be edited
|
||||
tx_parsed.outputs.forEach(out => edit_output_address.add(out.address));
|
||||
else if (typeof change_only == 'string') // allow only given receiver id output to be edited
|
||||
edit_output_address.add(change_only);
|
||||
else if (Array.isArray(change_only)) //allow only given set of receiver id outputs to be edited
|
||||
change_only.forEach(id => edit_output_address.add(id));
|
||||
|
||||
//edit output values to increase fee
|
||||
let inc_fee = util.BTC_to_Sat(new_fee - tx_parsed.fee);
|
||||
if (inc_fee < MIN_FEE_UPDATE)
|
||||
return reject(`Insufficient additional fee. Minimum increment: ${MIN_FEE_UPDATE}`);
|
||||
for (let i = tx.outs.length - 1; i >= 0 && inc_fee > 0; i--) //reduce in reverse order
|
||||
if (edit_output_address.has(tx_parsed.outputs[i].address)) {
|
||||
let current_value = tx.outs[i].value;
|
||||
if (current_value instanceof BigInteger) //convert BigInteger class to inv value
|
||||
current_value = current_value.intValue();
|
||||
//edit the value as required
|
||||
if (current_value > inc_fee) {
|
||||
tx.outs[i].value = current_value - inc_fee;
|
||||
inc_fee = 0;
|
||||
} else {
|
||||
inc_fee -= current_value;
|
||||
tx.outs[i].value = 0;
|
||||
}
|
||||
}
|
||||
if (inc_fee > 0) {
|
||||
let max_possible_fee = util.BTC_to_Sat(new_fee) - inc_fee; //in satoshi
|
||||
return reject(`Insufficient output values to increase fee. Maximum fee possible: ${util.Sat_to_BTC(max_possible_fee)}`);
|
||||
}
|
||||
tx.outs = tx.outs.filter(o => o.value >= DUST_AMT); //remove all output with value less than DUST amount
|
||||
|
||||
//remove existing signatures and reset the scripts
|
||||
let wif_keys = [];
|
||||
for (let i in tx.ins) {
|
||||
var addr = tx_parsed.inputs[i].address,
|
||||
value = util.BTC_to_Sat(tx_parsed.inputs[i].value);
|
||||
let addr_decode = coinjs.addressDecode(addr);
|
||||
//find the correct key for addr
|
||||
var privKey = private_keys.find(pk => verifyKey(addr, pk));
|
||||
if (!privKey)
|
||||
return reject(`Private key missing for ${addr}`);
|
||||
//find redeemScript (if any)
|
||||
const rs = _redeemScript(addr, privKey);
|
||||
rs === false ? wif_keys.unshift(privKey) : wif_keys.push(privKey); //sorting private-keys (wif)
|
||||
//reset the script for re-signing
|
||||
var script;
|
||||
if (!rs || !rs.length) {
|
||||
//legacy script (derive from address)
|
||||
let s = coinjs.script();
|
||||
s.writeOp(118); //OP_DUP
|
||||
s.writeOp(169); //OP_HASH160
|
||||
s.writeBytes(addr_decode.bytes);
|
||||
s.writeOp(136); //OP_EQUALVERIFY
|
||||
s.writeOp(172); //OP_CHECKSIG
|
||||
script = Crypto.util.bytesToHex(s.buffer);
|
||||
} else if (((rs.match(/^00/) && rs.length == 44)) || (rs.length == 40 && rs.match(/^[a-f0-9]+$/gi)) || addr_decode.type === 'multisigBech32') {
|
||||
//redeemScript for segwit/bech32 and multisig (bech32)
|
||||
let s = coinjs.script();
|
||||
s.writeBytes(Crypto.util.hexToBytes(rs));
|
||||
s.writeOp(0);
|
||||
s.writeBytes(coinjs.numToBytes(value.toFixed(0), 8));
|
||||
script = Crypto.util.bytesToHex(s.buffer);
|
||||
} else //redeemScript for multisig (segwit)
|
||||
script = rs;
|
||||
tx.ins[i].script = coinjs.script(script);
|
||||
}
|
||||
tx.witness = false; //remove all witness signatures
|
||||
console.debug("Unsigned:", tx.serialize());
|
||||
//re-sign the transaction
|
||||
new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
|
||||
resolve(tx.serialize());
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => {
|
||||
debugger;
|
||||
broadcastTx(result.transaction.serialize())
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const createSignedTx = btcOperator.createSignedTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
({
|
||||
senders,
|
||||
privkeys,
|
||||
receivers,
|
||||
amounts
|
||||
} = validateTxParameters({
|
||||
senders,
|
||||
privkeys,
|
||||
receivers,
|
||||
amounts,
|
||||
fee,
|
||||
change_address: options.change_address
|
||||
}));
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
let redeemScripts = [],
|
||||
wif_keys = [];
|
||||
for (let i in senders) {
|
||||
let rs = _redeemScript(senders[i], privkeys[i]); //get redeem-script (segwit/bech32)
|
||||
redeemScripts.push(rs);
|
||||
rs === false ? wif_keys.unshift(privkeys[i]) : wif_keys.push(privkeys[i]); //sorting private-keys (wif)
|
||||
}
|
||||
if (redeemScripts.includes(null)) //TODO: segwit
|
||||
return reject("Unable to get redeem-script");
|
||||
//create transaction
|
||||
createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => {
|
||||
let tx = result.transaction;
|
||||
console.debug("Unsigned:", tx.serialize());
|
||||
new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
|
||||
console.debug("Signed:", tx.serialize());
|
||||
resolve(result);
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
btcOperator.createTx = function (senders, receivers, amounts, fee = null, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
({
|
||||
senders,
|
||||
receivers,
|
||||
amounts
|
||||
} = validateTxParameters({
|
||||
senders,
|
||||
receivers,
|
||||
amounts,
|
||||
fee,
|
||||
change_address: options.change_address
|
||||
}));
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
let redeemScripts = senders.map(id => _redeemScript(id));
|
||||
if (redeemScripts.includes(null)) //TODO: segwit
|
||||
return reject("Unable to get redeem-script");
|
||||
//create transaction
|
||||
createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => {
|
||||
result.tx_hex = result.transaction.serialize();
|
||||
delete result.transaction;
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
btcOperator.createMultiSigTx = function (sender, redeemScript, receivers, amounts, fee = null, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
//validate tx parameters
|
||||
let addr_type = validateAddress(sender);
|
||||
if (!(["multisig", "multisigBech32"].includes(addr_type)))
|
||||
return reject("Invalid sender (multisig):" + sender);
|
||||
else {
|
||||
let script = coinjs.script();
|
||||
let decode = (addr_type == "multisig") ?
|
||||
script.decodeRedeemScript(redeemScript) :
|
||||
script.decodeRedeemScriptBech32(redeemScript);
|
||||
if (!decode || decode.address !== sender)
|
||||
return reject("Invalid redeem-script");
|
||||
}
|
||||
try {
|
||||
({
|
||||
receivers,
|
||||
amounts
|
||||
} = validateTxParameters({
|
||||
receivers,
|
||||
amounts,
|
||||
fee,
|
||||
change_address: options.change_address
|
||||
}));
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
//create transaction
|
||||
createTransaction([sender], [redeemScript], receivers, amounts, fee, options.change_address || sender, options.fee_from_receiver).then(result => {
|
||||
result.tx_hex = result.transaction.serialize();
|
||||
delete result.transaction;
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function deserializeTx(tx) {
|
||||
if (typeof tx === 'string' || Array.isArray(tx)) {
|
||||
try {
|
||||
tx = coinjs.transaction().deserialize(tx);
|
||||
} catch {
|
||||
throw "Invalid transaction hex";
|
||||
}
|
||||
} else if (typeof tx !== 'object' || typeof tx.sign !== 'function')
|
||||
throw "Invalid transaction object";
|
||||
return tx;
|
||||
}
|
||||
|
||||
btcOperator.signTx = function (tx, privkeys, sighashtype = 1) {
|
||||
tx = deserializeTx(tx);
|
||||
if (!Array.isArray(privkeys))
|
||||
privkeys = [privkeys];
|
||||
for (let i in privkeys)
|
||||
if (privkeys[i].length === 64)
|
||||
privkeys[i] = coinjs.privkey2wif(privkeys[i]);
|
||||
new Set(privkeys).forEach(key => tx.sign(key, sighashtype)); //Sign the tx using private key WIF
|
||||
return tx.serialize();
|
||||
}
|
||||
|
||||
const checkSigned = btcOperator.checkSigned = function (tx, bool = true) {
|
||||
tx = deserializeTx(tx);
|
||||
let n = [];
|
||||
for (let i in tx.ins) {
|
||||
var s = tx.extractScriptKey(i);
|
||||
if (s['type'] !== 'multisig' && s['type'] !== 'multisig_bech32')
|
||||
n.push(s.signed == 'true' || (tx.witness[i] && tx.witness[i].length == 2))
|
||||
else {
|
||||
var rs = coinjs.script().decodeRedeemScript(s.script); //will work for bech32 too, as only address is diff
|
||||
let x = {
|
||||
s: s['signatures'],
|
||||
r: rs['signaturesRequired'],
|
||||
t: rs['pubkeys'].length
|
||||
};
|
||||
if (x.r > x.t)
|
||||
throw "signaturesRequired is more than publicKeys";
|
||||
else if (x.s < x.r)
|
||||
n.push(x);
|
||||
else
|
||||
n.push(true);
|
||||
}
|
||||
}
|
||||
return bool ? !(n.filter(x => x !== true).length) : n;
|
||||
}
|
||||
|
||||
btcOperator.checkIfSameTx = function (tx1, tx2) {
|
||||
tx1 = deserializeTx(tx1);
|
||||
tx2 = deserializeTx(tx2);
|
||||
//compare input and output length
|
||||
if (tx1.ins.length !== tx2.ins.length || tx1.outs.length !== tx2.outs.length)
|
||||
return false;
|
||||
//compare inputs
|
||||
for (let i = 0; i < tx1.ins.length; i++)
|
||||
if (tx1.ins[i].outpoint.hash !== tx2.ins[i].outpoint.hash || tx1.ins[i].outpoint.index !== tx2.ins[i].outpoint.index)
|
||||
return false;
|
||||
//compare outputs
|
||||
for (let i = 0; i < tx1.outs.length; i++)
|
||||
if (tx1.outs[i].value !== tx2.outs[i].value || Crypto.util.bytesToHex(tx1.outs[i].script.buffer) !== Crypto.util.bytesToHex(tx2.outs[i].script.buffer))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
|
||||
fetch_api(`rawtx/${txid}`)
|
||||
.then(result => resolve(result.out[i]))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
|
||||
const parseTransaction = btcOperator.parseTransaction = function (tx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tx = deserializeTx(tx);
|
||||
let result = {};
|
||||
let promises = [];
|
||||
//Parse Inputs
|
||||
for (let i = 0; i < tx.ins.length; i++)
|
||||
promises.push(getTxOutput(tx.ins[i].outpoint.hash, tx.ins[i].outpoint.index));
|
||||
Promise.all(promises).then(inputs => {
|
||||
result.inputs = inputs.map(inp => Object({
|
||||
address: inp.addr,
|
||||
value: util.Sat_to_BTC(inp.value)
|
||||
}));
|
||||
let signed = checkSigned(tx, false);
|
||||
result.inputs.forEach((inp, i) => inp.signed = signed[i]);
|
||||
//Parse Outputs
|
||||
result.outputs = tx.outs.map(out => {
|
||||
var address;
|
||||
switch (out.script.chunks[0]) {
|
||||
case 0: //bech32, multisig-bech32
|
||||
address = util.encodeBech32(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.bech32.version, coinjs.bech32.hrp);
|
||||
break;
|
||||
case 169: //segwit, multisig-segwit
|
||||
address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[1]), coinjs.multisig);
|
||||
break;
|
||||
case 118: //legacy
|
||||
address = util.encodeLegacy(Crypto.util.bytesToHex(out.script.chunks[2]), coinjs.pub);
|
||||
}
|
||||
return {
|
||||
address,
|
||||
value: util.Sat_to_BTC(out.value)
|
||||
}
|
||||
});
|
||||
//Parse Totals
|
||||
result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8));
|
||||
result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8));
|
||||
result.fee = parseFloat((result.total_input - result.total_output).toFixed(8));
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
btcOperator.transactionID = function (tx) {
|
||||
tx = deserializeTx(tx);
|
||||
let clone = coinjs.clone(tx);
|
||||
clone.witness = null;
|
||||
let raw_bytes = Crypto.util.hexToBytes(clone.serialize());
|
||||
let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse();
|
||||
return Crypto.util.bytesToHex(txid);
|
||||
}
|
||||
|
||||
const getLatestBlock = btcOperator.getLatestBlock = () => new Promise((resolve, reject) => {
|
||||
fetch_api(`q/getblockcount`)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
|
||||
const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => {
|
||||
fetch_api(`rawtx/${txid}`).then(result => {
|
||||
getLatestBlock().then(latest_block => resolve({
|
||||
block: result.block_height,
|
||||
txid: result.hash,
|
||||
time: result.time * 1000,
|
||||
confirmations: result.block_height === null ? 0 : latest_block - result.block_height, //calculate confirmations using latest block number as api doesnt relay it
|
||||
size: result.size,
|
||||
fee: util.Sat_to_BTC(result.fee),
|
||||
inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })),
|
||||
total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)),
|
||||
outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })),
|
||||
total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)),
|
||||
}))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
getTx.hex = txid => new Promise((resolve, reject) => {
|
||||
fetch_api(`rawtx/${txid}?format=hex`, false)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
|
||||
btcOperator.getAddressData = address => new Promise((resolve, reject) => {
|
||||
fetch_api(`rawaddr/${address}`).then(data => {
|
||||
let details = {};
|
||||
details.balance = util.Sat_to_BTC(data.final_balance);
|
||||
details.address = data.address;
|
||||
details.txs = data.txs.map(tx => {
|
||||
let d = {
|
||||
txid: tx.hash,
|
||||
time: tx.time * 1000, //s to ms
|
||||
block: tx.block_height,
|
||||
}
|
||||
//sender list
|
||||
d.tx_senders = {};
|
||||
tx.inputs.forEach(i => {
|
||||
if (i.prev_out.addr in d.tx_senders)
|
||||
d.tx_senders[i.prev_out.addr] += i.prev_out.value;
|
||||
else d.tx_senders[i.prev_out.addr] = i.prev_out.value;
|
||||
});
|
||||
d.tx_input_value = 0;
|
||||
for (let s in d.tx_senders) {
|
||||
let val = d.tx_senders[s];
|
||||
d.tx_senders[s] = util.Sat_to_BTC(val);
|
||||
d.tx_input_value += val;
|
||||
}
|
||||
d.tx_input_value = util.Sat_to_BTC(d.tx_input_value);
|
||||
//receiver list
|
||||
d.tx_receivers = {};
|
||||
tx.out.forEach(o => {
|
||||
if (o.addr in d.tx_receivers)
|
||||
d.tx_receivers[o.addr] += o.value;
|
||||
else d.tx_receivers[o.addr] = o.value;
|
||||
});
|
||||
d.tx_output_value = 0;
|
||||
for (let r in d.tx_receivers) {
|
||||
let val = d.tx_receivers[r];
|
||||
d.tx_receivers[r] = util.Sat_to_BTC(val);
|
||||
d.tx_output_value += val;
|
||||
}
|
||||
d.tx_output_value = util.Sat_to_BTC(d.tx_output_value);
|
||||
d.tx_fee = util.Sat_to_BTC(tx.fee);
|
||||
//tx type
|
||||
if (tx.result > 0) { //net > 0, balance inc => type=in
|
||||
d.type = "in";
|
||||
d.amount = util.Sat_to_BTC(tx.result);
|
||||
d.sender = Object.keys(d.tx_senders).filter(s => s !== address);
|
||||
} else if (Object.keys(d.tx_receivers).some(r => r !== address)) { //net < 0, balance dec & receiver present => type=out
|
||||
d.type = "out";
|
||||
d.amount = util.Sat_to_BTC(tx.result * -1);
|
||||
d.receiver = Object.keys(d.tx_receivers).filter(r => r !== address);
|
||||
d.fee = d.tx_fee;
|
||||
} else { //net < 0 (fee) & no other id in receiver list => type=self
|
||||
d.type = "self";
|
||||
d.amount = d.tx_receivers[address];
|
||||
d.address = address
|
||||
}
|
||||
return d;
|
||||
})
|
||||
resolve(details);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
btcOperator.getBlock = block => new Promise((resolve, reject) => {
|
||||
fetch_api(`rawblock/${block}`).then(result => resolve({
|
||||
height: result.height,
|
||||
hash: result.hash,
|
||||
merkle_root: result.mrkl_root,
|
||||
prev_block: result.prev_block,
|
||||
next_block: result.next_block[0],
|
||||
size: result.size,
|
||||
time: result.time * 1000, //s to ms
|
||||
txs: result.tx.map(t => Object({
|
||||
fee: t.fee,
|
||||
size: t.size,
|
||||
inputs: t.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })),
|
||||
total_input_value: util.Sat_to_BTC(t.inputs.reduce((a, i) => a + i.prev_out.value, 0)),
|
||||
outputs: t.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })),
|
||||
total_output_value: util.Sat_to_BTC(t.out.reduce((a, o) => a += o.value, 0)),
|
||||
}))
|
||||
|
||||
})).catch(error => reject(error))
|
||||
});
|
||||
|
||||
})('object' === typeof module ? module.exports : window.btcOperator = {});
|
||||
226
exchangemarket/docs/scripts/components.js
Normal file
226
exchangemarket/docs/scripts/components.js
Normal file
File diff suppressed because one or more lines are too long
1044
exchangemarket/docs/scripts/floBlockchainAPI.js
Normal file
1044
exchangemarket/docs/scripts/floBlockchainAPI.js
Normal file
File diff suppressed because it is too large
Load Diff
530
exchangemarket/docs/scripts/floCrypto.js
Normal file
530
exchangemarket/docs/scripts/floCrypto.js
Normal file
@ -0,0 +1,530 @@
|
||||
(function (EXPORTS) { //floCrypto v2.3.6a
|
||||
/* FLO Crypto Operators */
|
||||
'use strict';
|
||||
const floCrypto = EXPORTS;
|
||||
|
||||
const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);
|
||||
const ecparams = EllipticCurve.getSECCurveByName("secp256k1");
|
||||
const ascii_alternatives = `‘ '\n’ '\n“ "\n” "\n– --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`;
|
||||
const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4"));
|
||||
coinjs.compressed = true; //defaulting coinjs compressed to true;
|
||||
|
||||
function calculateY(x) {
|
||||
let exp = exponent1();
|
||||
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
|
||||
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
|
||||
}
|
||||
|
||||
function getUncompressedPublicKey(compressedPublicKey) {
|
||||
// Fetch x from compressedPublicKey
|
||||
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
|
||||
const prefix = pubKeyBytes.shift() // remove prefix
|
||||
let prefix_modulus = prefix % 2;
|
||||
pubKeyBytes.unshift(0) // add prefix 0
|
||||
let x = new BigInteger(pubKeyBytes)
|
||||
let xDecimalValue = x.toString()
|
||||
// Fetch y
|
||||
let y = calculateY(x);
|
||||
let yDecimalValue = y.toString();
|
||||
// verify y value
|
||||
let resultBigInt = y.mod(BigInteger("2"));
|
||||
let check = resultBigInt.toString() % 2;
|
||||
if (prefix_modulus !== check)
|
||||
yDecimalValue = y.negate().mod(p).toString();
|
||||
return {
|
||||
x: xDecimalValue,
|
||||
y: yDecimalValue
|
||||
};
|
||||
}
|
||||
|
||||
function getSenderPublicKeyString() {
|
||||
let privateKey = ellipticCurveEncryption.senderRandom();
|
||||
var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
|
||||
return {
|
||||
privateKey: privateKey,
|
||||
senderPublicKeyString: senderPublicKeyString
|
||||
}
|
||||
}
|
||||
|
||||
function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) {
|
||||
let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex);
|
||||
var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
|
||||
receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey);
|
||||
return senderDerivedKey;
|
||||
}
|
||||
|
||||
function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) {
|
||||
return ellipticCurveEncryption.receiverSharedKeyDerivation(
|
||||
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey);
|
||||
}
|
||||
|
||||
function getReceiverPublicKeyString(privateKey) {
|
||||
return ellipticCurveEncryption.receiverPublicString(privateKey);
|
||||
}
|
||||
|
||||
function wifToDecimal(pk_wif, isPubKeyCompressed = false) {
|
||||
let pk = Bitcoin.Base58.decode(pk_wif)
|
||||
pk.shift()
|
||||
pk.splice(-4, 4)
|
||||
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
|
||||
if (isPubKeyCompressed == true) pk.pop()
|
||||
pk.unshift(0)
|
||||
let privateKeyDecimal = BigInteger(pk).toString()
|
||||
let privateKeyHex = Crypto.util.bytesToHex(pk)
|
||||
return {
|
||||
privateKeyDecimal: privateKeyDecimal,
|
||||
privateKeyHex: privateKeyHex
|
||||
}
|
||||
}
|
||||
|
||||
//generate a random Interger within range
|
||||
floCrypto.randInt = function (min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(securedMathRandom() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
//generate a random String within length (options : alphaNumeric chars only)
|
||||
floCrypto.randString = function (length, alphaNumeric = true) {
|
||||
var result = '';
|
||||
var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' :
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():';
|
||||
for (var i = 0; i < length; i++)
|
||||
result += characters.charAt(Math.floor(securedMathRandom() * characters.length));
|
||||
return result;
|
||||
}
|
||||
|
||||
//Encrypt Data using public-key
|
||||
floCrypto.encryptData = function (data, receiverPublicKeyHex) {
|
||||
var senderECKeyData = getSenderPublicKeyString();
|
||||
var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey);
|
||||
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
|
||||
let secret = Crypto.AES.encrypt(data, senderKey);
|
||||
return {
|
||||
secret: secret,
|
||||
senderPublicKeyString: senderECKeyData.senderPublicKeyString
|
||||
};
|
||||
}
|
||||
|
||||
//Decrypt Data using private-key
|
||||
floCrypto.decryptData = function (data, privateKeyHex) {
|
||||
var receiverECKeyData = {};
|
||||
if (typeof privateKeyHex !== "string") throw new Error("No private key found.");
|
||||
let privateKey = wifToDecimal(privateKeyHex, true);
|
||||
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key.");
|
||||
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
|
||||
var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey);
|
||||
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
|
||||
let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey);
|
||||
return decryptMsg;
|
||||
}
|
||||
|
||||
//Sign data using private-key
|
||||
floCrypto.signData = function (data, privateKeyHex) {
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
var messageHash = Crypto.SHA256(data);
|
||||
var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv);
|
||||
var sighex = Crypto.util.bytesToHex(messageSign);
|
||||
return sighex;
|
||||
}
|
||||
|
||||
//Verify signatue of the data using public-key
|
||||
floCrypto.verifySign = function (data, signatureHex, publicKeyHex) {
|
||||
var msgHash = Crypto.SHA256(data);
|
||||
var sigBytes = Crypto.util.hexToBytes(signatureHex);
|
||||
var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex);
|
||||
var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint);
|
||||
return verify;
|
||||
}
|
||||
|
||||
//Generates a new flo ID and returns private-key, public-key and floID
|
||||
const generateNewID = floCrypto.generateNewID = function () {
|
||||
var key = new Bitcoin.ECKey(false);
|
||||
key.setCompressed(true);
|
||||
return {
|
||||
floID: key.getBitcoinAddress(),
|
||||
pubKey: key.getPubKeyHex(),
|
||||
privKey: key.getBitcoinWalletImportFormat()
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(floCrypto, {
|
||||
newID: {
|
||||
get: () => generateNewID()
|
||||
},
|
||||
hashID: {
|
||||
value: (str) => {
|
||||
let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), { asBytes: true });
|
||||
bytes.unshift(bitjs.pub);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
var checksum = hash.slice(0, 4);
|
||||
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||
}
|
||||
},
|
||||
tmpID: {
|
||||
get: () => {
|
||||
let bytes = Crypto.util.randomBytes(20);
|
||||
bytes.unshift(bitjs.pub);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
var checksum = hash.slice(0, 4);
|
||||
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Returns public-key from private-key
|
||||
floCrypto.getPubKeyHex = function (privateKeyHex) {
|
||||
if (!privateKeyHex)
|
||||
return null;
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
if (key.priv == null)
|
||||
return null;
|
||||
key.setCompressed(true);
|
||||
return key.getPubKeyHex();
|
||||
}
|
||||
|
||||
//Returns flo-ID from public-key or private-key
|
||||
floCrypto.getFloID = function (keyHex) {
|
||||
if (!keyHex)
|
||||
return null;
|
||||
try {
|
||||
var key = new Bitcoin.ECKey(keyHex);
|
||||
if (key.priv == null)
|
||||
key.setPub(keyHex);
|
||||
return key.getBitcoinAddress();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
floCrypto.getAddress = function (privateKeyHex, strict = false) {
|
||||
if (!privateKeyHex)
|
||||
return;
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
if (key.priv == null)
|
||||
return null;
|
||||
key.setCompressed(true);
|
||||
let pubKey = key.getPubKeyHex(),
|
||||
version = bitjs.Base58.decode(privateKeyHex)[0];
|
||||
switch (version) {
|
||||
case coinjs.priv: //BTC
|
||||
return coinjs.bech32Address(pubKey).address;
|
||||
case bitjs.priv: //FLO
|
||||
return bitjs.pubkey2address(pubKey);
|
||||
default:
|
||||
return strict ? false : bitjs.pubkey2address(pubKey); //default to FLO address (if strict=false)
|
||||
}
|
||||
}
|
||||
|
||||
//Verify the private-key for the given public-key or flo-ID
|
||||
floCrypto.verifyPrivKey = function (privateKeyHex, pubKey_floID, isfloID = true) {
|
||||
if (!privateKeyHex || !pubKey_floID)
|
||||
return false;
|
||||
try {
|
||||
var key = new Bitcoin.ECKey(privateKeyHex);
|
||||
if (key.priv == null)
|
||||
return false;
|
||||
key.setCompressed(true);
|
||||
if (isfloID && pubKey_floID == key.getBitcoinAddress())
|
||||
return true;
|
||||
else if (!isfloID && pubKey_floID.toUpperCase() == key.getPubKeyHex().toUpperCase())
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
floCrypto.getMultisigAddress = function (publicKeyList, requiredSignatures) {
|
||||
if (!Array.isArray(publicKeyList) || !publicKeyList.length)
|
||||
return null;
|
||||
if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1 || requiredSignatures > publicKeyList.length)
|
||||
return null;
|
||||
try {
|
||||
var multisig = bitjs.pubkeys2multisig(publicKeyList, requiredSignatures);
|
||||
return multisig;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
floCrypto.decodeRedeemScript = function (redeemScript) {
|
||||
try {
|
||||
var decoded = bitjs.transaction().decodeRedeemScript(redeemScript);
|
||||
return decoded;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the given flo-id is valid or not
|
||||
floCrypto.validateFloID = function (floID, regularOnly = false) {
|
||||
if (!floID)
|
||||
return false;
|
||||
try {
|
||||
let addr = new Bitcoin.Address(floID);
|
||||
if (regularOnly && addr.version != Bitcoin.Address.standardVersion)
|
||||
return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the given address (any blockchain) is valid or not
|
||||
floCrypto.validateAddr = function (address, std = true, bech = true) {
|
||||
let raw = decodeAddress(address);
|
||||
if (!raw)
|
||||
return false;
|
||||
if (typeof raw.version !== 'undefined') { //legacy or segwit
|
||||
if (std == false)
|
||||
return false;
|
||||
else if (std === true || (!Array.isArray(std) && std === raw.version) || (Array.isArray(std) && std.includes(raw.version)))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} else if (typeof raw.bech_version !== 'undefined') { //bech32
|
||||
if (bech === false)
|
||||
return false;
|
||||
else if (bech === true || (!Array.isArray(bech) && bech === raw.bech_version) || (Array.isArray(bech) && bech.includes(raw.bech_version)))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
} else //unknown
|
||||
return false;
|
||||
}
|
||||
|
||||
//Check the public-key (or redeem-script) for the address (any blockchain)
|
||||
floCrypto.verifyPubKey = function (pubKeyHex, address) {
|
||||
let raw = decodeAddress(address);
|
||||
if (!raw)
|
||||
return;
|
||||
let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { asBytes: true })));
|
||||
if (typeof raw.bech_version !== 'undefined' && raw.bytes.length == 32) //bech32-multisig
|
||||
raw.hex = Crypto.util.bytesToHex(ripemd160(raw.bytes, { asBytes: true }));
|
||||
return pub_hash === raw.hex;
|
||||
}
|
||||
|
||||
//Convert the given address (any blockchain) to equivalent floID
|
||||
floCrypto.toFloID = function (address, options = null) {
|
||||
if (!address)
|
||||
return;
|
||||
let raw = decodeAddress(address);
|
||||
if (!raw)
|
||||
return;
|
||||
else if (options) { //if (optional) version check is passed
|
||||
if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version)))
|
||||
return;
|
||||
if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version)))
|
||||
return;
|
||||
}
|
||||
raw.bytes.unshift(bitjs.pub);
|
||||
let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4)));
|
||||
}
|
||||
|
||||
//Convert raw address bytes to floID
|
||||
floCrypto.rawToFloID = function (raw_bytes) {
|
||||
if (typeof raw_bytes === 'string')
|
||||
raw_bytes = Crypto.util.hexToBytes(raw_bytes);
|
||||
if (raw_bytes.length != 20)
|
||||
return null;
|
||||
raw_bytes.unshift(bitjs.pub);
|
||||
let hash = Crypto.SHA256(Crypto.SHA256(raw_bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
return bitjs.Base58.encode(raw_bytes.concat(hash.slice(0, 4)));
|
||||
}
|
||||
|
||||
//Convert the given multisig address (any blockchain) to equivalent multisig floID
|
||||
floCrypto.toMultisigFloID = function (address, options = null) {
|
||||
if (!address)
|
||||
return;
|
||||
let raw = decodeAddress(address);
|
||||
if (!raw)
|
||||
return;
|
||||
else if (options) { //if (optional) version check is passed
|
||||
if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version)))
|
||||
return;
|
||||
if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version)))
|
||||
return;
|
||||
}
|
||||
if (typeof raw.bech_version !== 'undefined') {
|
||||
if (raw.bytes.length != 32) return; //multisig bech address have 32 bytes
|
||||
//multisig-bech:hash=SHA256 whereas multisig:hash=r160(SHA265), thus ripemd160 the bytes from multisig-bech
|
||||
raw.bytes = ripemd160(raw.bytes, {
|
||||
asBytes: true
|
||||
});
|
||||
}
|
||||
raw.bytes.unshift(bitjs.multisig);
|
||||
let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4)));
|
||||
}
|
||||
|
||||
//Checks if the given addresses (any blockchain) are same (w.r.t keys)
|
||||
floCrypto.isSameAddr = function (addr1, addr2) {
|
||||
if (!addr1 || !addr2)
|
||||
return;
|
||||
let raw1 = decodeAddress(addr1),
|
||||
raw2 = decodeAddress(addr2);
|
||||
if (!raw1 || !raw2)
|
||||
return false;
|
||||
else {
|
||||
if (typeof raw1.bech_version !== 'undefined' && raw1.bytes.length == 32) //bech32-multisig
|
||||
raw1.hex = Crypto.util.bytesToHex(ripemd160(raw1.bytes, { asBytes: true }));
|
||||
if (typeof raw2.bech_version !== 'undefined' && raw2.bytes.length == 32) //bech32-multisig
|
||||
raw2.hex = Crypto.util.bytesToHex(ripemd160(raw2.bytes, { asBytes: true }));
|
||||
return raw1.hex === raw2.hex;
|
||||
}
|
||||
}
|
||||
|
||||
const decodeAddress = floCrypto.decodeAddr = function (address) {
|
||||
if (!address)
|
||||
return;
|
||||
else if (address.length == 33 || address.length == 34) { //legacy encoding
|
||||
let decode = bitjs.Base58.decode(address);
|
||||
let bytes = decode.slice(0, decode.length - 4);
|
||||
let checksum = decode.slice(decode.length - 4),
|
||||
hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
||||
asBytes: true
|
||||
}), {
|
||||
asBytes: true
|
||||
});
|
||||
return (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3]) ? null : {
|
||||
version: bytes.shift(),
|
||||
hex: Crypto.util.bytesToHex(bytes),
|
||||
bytes
|
||||
}
|
||||
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
||||
let decode = coinjs.bech32_decode(address);
|
||||
if (decode) {
|
||||
let bytes = decode.data;
|
||||
let bech_version = bytes.shift();
|
||||
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
||||
return {
|
||||
bech_version,
|
||||
hrp: decode.hrp,
|
||||
hex: Crypto.util.bytesToHex(bytes),
|
||||
bytes
|
||||
}
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Split the str using shamir's Secret and Returns the shares
|
||||
floCrypto.createShamirsSecretShares = function (str, total_shares, threshold_limit) {
|
||||
try {
|
||||
if (str.length > 0) {
|
||||
var strHex = shamirSecretShare.str2hex(str);
|
||||
var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit);
|
||||
return shares;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Returns the retrived secret by combining the shamirs shares
|
||||
const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function (sharesArray) {
|
||||
try {
|
||||
if (sharesArray.length > 0) {
|
||||
var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length));
|
||||
comb = shamirSecretShare.hex2str(comb);
|
||||
return comb;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Verifies the shares and str
|
||||
floCrypto.verifyShamirsSecret = function (sharesArray, str) {
|
||||
if (!str)
|
||||
return null;
|
||||
else if (retrieveShamirSecret(sharesArray) === str)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
const validateASCII = floCrypto.validateASCII = function (string, bool = true) {
|
||||
if (typeof string !== "string")
|
||||
return null;
|
||||
if (bool) {
|
||||
let x;
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
x = string.charCodeAt(i);
|
||||
if (x < 32 || x > 127)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
let x, invalids = {};
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
x = string.charCodeAt(i);
|
||||
if (x < 32 || x > 127)
|
||||
if (x in invalids)
|
||||
invalids[string[i]].push(i)
|
||||
else
|
||||
invalids[string[i]] = [i];
|
||||
}
|
||||
if (Object.keys(invalids).length)
|
||||
return invalids;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
floCrypto.convertToASCII = function (string, mode = 'soft-remove') {
|
||||
let chars = validateASCII(string, false);
|
||||
if (chars === true)
|
||||
return string;
|
||||
else if (chars === null)
|
||||
return null;
|
||||
let convertor, result = string,
|
||||
refAlt = {};
|
||||
ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2));
|
||||
mode = mode.toLowerCase();
|
||||
if (mode === "hard-unicode")
|
||||
convertor = (c) => `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`;
|
||||
else if (mode === "soft-unicode")
|
||||
convertor = (c) => refAlt[c] || `\\u${('000' + c.charCodeAt().toString(16)).slice(-4)}`;
|
||||
else if (mode === "hard-remove")
|
||||
convertor = c => "";
|
||||
else if (mode === "soft-remove")
|
||||
convertor = c => refAlt[c] || "";
|
||||
else
|
||||
return null;
|
||||
for (let c in chars)
|
||||
result = result.replaceAll(c, convertor(c));
|
||||
return result;
|
||||
}
|
||||
|
||||
floCrypto.revertUnicode = function (string) {
|
||||
return string.replace(/\\u[\dA-F]{4}/gi,
|
||||
m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16)));
|
||||
}
|
||||
|
||||
})('object' === typeof module ? module.exports : window.floCrypto = {});
|
||||
1925
exchangemarket/docs/scripts/floExchangeAPI.js
Normal file
1925
exchangemarket/docs/scripts/floExchangeAPI.js
Normal file
File diff suppressed because it is too large
Load Diff
7
exchangemarket/docs/scripts/floGlobals.js
Normal file
7
exchangemarket/docs/scripts/floGlobals.js
Normal file
@ -0,0 +1,7 @@
|
||||
const floGlobals = {
|
||||
blockchain: "FLO",
|
||||
marketID: "FMxYC7gYZhouzqtHZukGnPiQ8nvG4CMzXM",
|
||||
currency: "rupee"
|
||||
};
|
||||
|
||||
('object' === typeof module) ? module.exports = floGlobals : null;
|
||||
166
exchangemarket/docs/scripts/floTokenAPI.js
Normal file
166
exchangemarket/docs/scripts/floTokenAPI.js
Normal file
@ -0,0 +1,166 @@
|
||||
(function (EXPORTS) { //floTokenAPI v1.0.4a
|
||||
/* Token Operator to send/receive tokens via blockchain using API calls*/
|
||||
'use strict';
|
||||
const tokenAPI = EXPORTS;
|
||||
|
||||
const DEFAULT = {
|
||||
apiURL: floGlobals.tokenURL || "https://ranchimallflo.duckdns.org/",
|
||||
currency: floGlobals.currency || "rupee"
|
||||
}
|
||||
|
||||
Object.defineProperties(tokenAPI, {
|
||||
URL: {
|
||||
get: () => DEFAULT.apiURL
|
||||
},
|
||||
currency: {
|
||||
get: () => DEFAULT.currency,
|
||||
set: currency => DEFAULT.currency = currency
|
||||
}
|
||||
});
|
||||
|
||||
if (floGlobals.currency) tokenAPI.currency = floGlobals.currency;
|
||||
|
||||
Object.defineProperties(floGlobals, {
|
||||
currency: {
|
||||
get: () => DEFAULT.currency,
|
||||
set: currency => DEFAULT.currency = currency
|
||||
}
|
||||
});
|
||||
|
||||
const fetch_api = tokenAPI.fetch = function (apicall) {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.debug(DEFAULT.apiURL + apicall);
|
||||
fetch(DEFAULT.apiURL + apicall).then(response => {
|
||||
if (response.ok)
|
||||
response.json().then(data => resolve(data));
|
||||
else
|
||||
reject(response)
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const getBalance = tokenAPI.getBalance = function (floID, token = DEFAULT.currency) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch_api(`api/v1.0/getFloAddressBalance?token=${token}&floAddress=${floID}`)
|
||||
.then(result => resolve(result.balance || 0))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
tokenAPI.getTx = function (txID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
tokenAPI.sendToken = function (privKey, amount, receiverID, message = "", token = DEFAULT.currency, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let senderID = floCrypto.getFloID(privKey);
|
||||
if (typeof amount !== "number" || isNaN(amount) || amount <= 0)
|
||||
return reject("Invalid amount");
|
||||
getBalance(senderID, token).then(bal => {
|
||||
if (amount > bal)
|
||||
return reject(`Insufficient ${token}# balance`);
|
||||
floBlockchainAPI.writeData(senderID, `send ${amount} ${token}# ${message}`, privKey, receiverID, options)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
function sendTokens_raw(privKey, receiverID, token, amount, utxo, vout, scriptPubKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var trx = bitjs.transaction();
|
||||
trx.addinput(utxo, vout, scriptPubKey)
|
||||
trx.addoutput(receiverID, floBlockchainAPI.sendAmt);
|
||||
trx.addflodata(`send ${amount} ${token}#`);
|
||||
var signedTxHash = trx.sign(privKey, 1);
|
||||
floBlockchainAPI.broadcastTx(signedTxHash)
|
||||
.then(txid => resolve([receiverID, txid]))
|
||||
.catch(error => reject([receiverID, error]))
|
||||
})
|
||||
}
|
||||
|
||||
//bulk transfer tokens
|
||||
tokenAPI.bulkTransferTokens = function (sender, privKey, token, receivers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof receivers !== 'object')
|
||||
return reject("receivers must be object in format {receiver1: amount1, receiver2:amount2...}")
|
||||
|
||||
let receiver_list = Object.keys(receivers), amount_list = Object.values(receivers);
|
||||
let invalidReceivers = receiver_list.filter(id => !floCrypto.validateFloID(id));
|
||||
let invalidAmount = amount_list.filter(val => typeof val !== 'number' || val <= 0);
|
||||
if (invalidReceivers.length)
|
||||
return reject(`Invalid receivers: ${invalidReceivers}`);
|
||||
else if (invalidAmount.length)
|
||||
return reject(`Invalid amounts: ${invalidAmount}`);
|
||||
|
||||
if (receiver_list.length == 0)
|
||||
return reject("Receivers cannot be empty");
|
||||
|
||||
if (receiver_list.length == 1) {
|
||||
let receiver = receiver_list[0], amount = amount_list[0];
|
||||
floTokenAPI.sendToken(privKey, amount, receiver, "", token)
|
||||
.then(txid => resolve({ success: { [receiver]: txid } }))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
//check for token balance
|
||||
floTokenAPI.getBalance(sender, token).then(token_balance => {
|
||||
let total_token_amout = amount_list.reduce((a, e) => a + e, 0);
|
||||
if (total_token_amout > token_balance)
|
||||
return reject(`Insufficient ${token}# balance`);
|
||||
|
||||
//split utxos
|
||||
floBlockchainAPI.splitUTXOs(sender, privKey, receiver_list.length).then(split_txid => {
|
||||
//wait for the split utxo to get confirmation
|
||||
floBlockchainAPI.waitForConfirmation(split_txid).then(split_tx => {
|
||||
//send tokens using the split-utxo
|
||||
var scriptPubKey = split_tx.vout[0].scriptPubKey.hex;
|
||||
let promises = [];
|
||||
for (let i in receiver_list)
|
||||
promises.push(sendTokens_raw(privKey, receiver_list[i], token, amount_list[i], split_txid, i, scriptPubKey));
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let success = Object.fromEntries(results.filter(r => r.status == 'fulfilled').map(r => r.value));
|
||||
let failed = Object.fromEntries(results.filter(r => r.status == 'rejected').map(r => r.reason));
|
||||
resolve({ success, failed });
|
||||
})
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
tokenAPI.getAllTxs = function (floID, token = DEFAULT.currency) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch_api(`api/v1.0/getFloAddressTransactions?token=${token}&floAddress=${floID}`)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const util = tokenAPI.util = {};
|
||||
|
||||
util.parseTxData = function (txData) {
|
||||
let parsedData = {};
|
||||
for (let p in txData.parsedFloData)
|
||||
parsedData[p] = txData.parsedFloData[p];
|
||||
parsedData.sender = txData.transactionDetails.vin[0].addr;
|
||||
for (let vout of txData.transactionDetails.vout)
|
||||
if (vout.scriptPubKey.addresses[0] !== parsedData.sender)
|
||||
parsedData.receiver = vout.scriptPubKey.addresses[0];
|
||||
parsedData.time = txData.transactionDetails.time;
|
||||
return parsedData;
|
||||
}
|
||||
|
||||
})('object' === typeof module ? module.exports : window.floTokenAPI = {});
|
||||
9975
exchangemarket/docs/scripts/lib.js
Normal file
9975
exchangemarket/docs/scripts/lib.js
Normal file
File diff suppressed because it is too large
Load Diff
41
exchangemarket/package.json
Normal file
41
exchangemarket/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "flo-exchange-market",
|
||||
"version": "1.0.0",
|
||||
"description": "Central Exchange market for FLO",
|
||||
"main": "src/main.js",
|
||||
"dependencies": {
|
||||
"cookie-parser": "^1.4.5",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.2",
|
||||
"mysql": "^2.18.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"ws": "^8.2.3"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"postinstall": "node setup/post-install.js",
|
||||
"help": "node setup/help.js",
|
||||
"setup": "node setup/post-install.js",
|
||||
"configure": "node setup/configure-settings.js",
|
||||
"reset-password": "node setup/reset-password.js",
|
||||
"checksum-db": "node debug/checksum-db.js",
|
||||
"start": "node start.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ranchimall/exchange-market.git"
|
||||
},
|
||||
"keywords": [
|
||||
"exchange-market",
|
||||
"FLO",
|
||||
"blockchain",
|
||||
"Ranchimall"
|
||||
],
|
||||
"author": "Sai Raj",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ranchimall/exchange-market/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ranchimall/exchange-market#readme"
|
||||
}
|
||||
129
exchangemarket/setup/configure-settings.js
Normal file
129
exchangemarket/setup/configure-settings.js
Normal file
@ -0,0 +1,129 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const getInput = require('./getInput');
|
||||
|
||||
let _I = "";
|
||||
for (let arg of process.argv)
|
||||
if (/^-I=/.test(arg)) {
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
break;
|
||||
}
|
||||
|
||||
var config, flag_new;
|
||||
try {
|
||||
config = require(`../args/config${_I}.json`);
|
||||
flag_new = false;
|
||||
} catch (error) {
|
||||
config = {
|
||||
"secret": null,
|
||||
"port": "8080",
|
||||
|
||||
"sql_user": null,
|
||||
"sql_pwd": null,
|
||||
"sql_db": "exchange",
|
||||
"sql_host": "localhost"
|
||||
};
|
||||
flag_new = true;
|
||||
}
|
||||
|
||||
function flaggedYesOrNo(text) {
|
||||
return new Promise((resolve) => {
|
||||
if (flag_new)
|
||||
resolve(true);
|
||||
else
|
||||
getInput.YesOrNo(text)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function configurePort() {
|
||||
return new Promise(resolve => {
|
||||
getInput.Text('Enter port', config["port"]).then(port => {
|
||||
config["port"] = port;
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function configureSQL() {
|
||||
return new Promise(resolve => {
|
||||
flaggedYesOrNo('Do you want to re-configure mySQL connection').then(value => {
|
||||
if (value) {
|
||||
console.log('Enter mySQL connection values: ')
|
||||
getInput.Text('MySQL Host', config['sql_host']).then(host => {
|
||||
config['sql_host'] = host;
|
||||
getInput.Text('Database name', config['sql_db']).then(dbname => {
|
||||
config['sql_db'] = dbname;
|
||||
getInput.Text('MySQL username', config['sql_user']).then(sql_user => {
|
||||
config['sql_user'] = sql_user;
|
||||
getInput.Text('MySQL password', config['sql_pwd']).then(sql_pwd => {
|
||||
config['sql_pwd'] = sql_pwd;
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
} else
|
||||
resolve(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function randomizeSessionSecret() {
|
||||
return new Promise((resolve) => {
|
||||
flaggedYesOrNo('Do you want to randomize the session secret').then(value => {
|
||||
if (value) {
|
||||
let N = Math.floor(Math.random() * (64 - 32 + 1)) + 32;
|
||||
var secret = '';
|
||||
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (var i = 0; i < N; i++)
|
||||
secret += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
config['secret'] = secret;
|
||||
resolve(true);
|
||||
} else
|
||||
resolve(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function configure() {
|
||||
return new Promise((resolve, reject) => {
|
||||
configurePort().then(port_result => {
|
||||
randomizeSessionSecret().then(secret_result => {
|
||||
configureSQL().then(sql_result => {
|
||||
fs.writeFile(path.resolve(__dirname, '..', 'args', `config${_I}.json`), JSON.stringify(config), 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(false);
|
||||
}
|
||||
console.log('Configuration successful!');
|
||||
/*
|
||||
if (sql_result) {
|
||||
getInput.YesOrNo('Do you want to create schema in the database').then(value => {
|
||||
if (value) {
|
||||
const createSchema = require('./create-schema');
|
||||
createSchema().then(result => resolve(result))
|
||||
.catch(error => {
|
||||
console.log('Retry using: \n' + 'npm run create-schema');
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
console.log('To create schema, use: \n' + 'npm run create-schema');
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else
|
||||
*/
|
||||
resolve(true);
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent)
|
||||
configure().then(_ => null).catch(error => console.error(error)).finally(_ => process.exit(0));
|
||||
else
|
||||
module.exports = configure;
|
||||
45
exchangemarket/setup/create-schema.js
Normal file
45
exchangemarket/setup/create-schema.js
Normal file
@ -0,0 +1,45 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let DB = require('../src/database');
|
||||
|
||||
let _I = "";
|
||||
for (let arg of process.argv)
|
||||
if (/^-I=/.test(arg)) {
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
break;
|
||||
}
|
||||
|
||||
function createSchema() {
|
||||
const config = require(`../args/config${_I}.json`);
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(path.resolve(__dirname, '..', 'args', `schema.sql`), 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(null);
|
||||
}
|
||||
DB.connect(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(_ => {
|
||||
let txQueries = data.split(';');
|
||||
txQueries.pop();
|
||||
txQueries = txQueries.map(q => q.trim().replace(/\n/g, ' '));
|
||||
//console.log(txQueries);
|
||||
DB.transaction(txQueries).then(_ => {
|
||||
console.log('SQL Schema created successfully!');
|
||||
resolve(true);
|
||||
}).catch(error => {
|
||||
console.error(error.message);
|
||||
console.log('SQL Schema creation failed! Check user permission');
|
||||
reject(true);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
console.log('Unable to connect to MySQL database! Check user permission');
|
||||
reject(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!module.parent)
|
||||
createSchema().then(_ => null).catch(_ => null).finally(_ => process.exit(0));
|
||||
else
|
||||
module.exports = createSchema;
|
||||
42
exchangemarket/setup/getInput.js
Normal file
42
exchangemarket/setup/getInput.js
Normal file
@ -0,0 +1,42 @@
|
||||
const readline = require('readline');
|
||||
|
||||
const getInput = {
|
||||
Text: function(text, current = null) {
|
||||
return new Promise((resolve) => {
|
||||
let r = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
r.question(`${text} :` + (current ? `(${current})` : ''), value => {
|
||||
r.close();
|
||||
value = value || current;
|
||||
if (value === null) {
|
||||
console.log("Please enter a value!");
|
||||
this.Text(text, current).then(result => resolve(result));
|
||||
} else
|
||||
resolve(value);
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
YesOrNo: function(text, def_value = "YES") {
|
||||
return new Promise((resolve) => {
|
||||
let r = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
r.question(`${text}? [YES/NO] : (${def_value})`, value => {
|
||||
r.close();
|
||||
value = (value || def_value).toLowerCase();
|
||||
value = ['yes', 'y'].includes(value) ? true : ['no', 'n'].includes(value) ? false : null;
|
||||
if (value === null) {
|
||||
console.log("Please enter a valid value!");
|
||||
this.YesOrNo(text, def_value).then(result => resolve(result));
|
||||
} else
|
||||
resolve(value);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = getInput;
|
||||
23
exchangemarket/setup/help.js
Normal file
23
exchangemarket/setup/help.js
Normal file
@ -0,0 +1,23 @@
|
||||
let message = `
|
||||
Exchange market
|
||||
---------------
|
||||
|
||||
npm install - Install the app and node modules.
|
||||
npm run help - List all commands.
|
||||
npm run setup - Finish the setup (configure and reset password).
|
||||
npm run configure - Configure the node.
|
||||
npm run reset-password - Reset the password (for private-key).
|
||||
|
||||
npm start - Start the application (main).
|
||||
|
||||
NOTE: argument 'PASSWORD' required for 'npm start'
|
||||
npm start -- -PASSWORD=<password>
|
||||
|
||||
(Optional) Multiple instance can be run/setup on the same dir with different config files by using argument 'I'.
|
||||
<command> -- -I=<instance_ID>
|
||||
|
||||
(Optional) 'console.debug' is now turned off by default. pass argument '--debug' to turn it on
|
||||
npm start -- -PASSWORD=<password> --debug
|
||||
`;
|
||||
|
||||
console.log(message);
|
||||
38
exchangemarket/setup/post-install.js
Normal file
38
exchangemarket/setup/post-install.js
Normal file
@ -0,0 +1,38 @@
|
||||
const getInput = require("./getInput");
|
||||
|
||||
let message = `
|
||||
Exchange Market is installed
|
||||
To list all commands, use:
|
||||
npm run help
|
||||
`;
|
||||
console.log(message);
|
||||
|
||||
getInput.YesOrNo('Do you want to finish the setup now').then(value => {
|
||||
if (value) {
|
||||
let configureSettings = require('./configure-settings');
|
||||
configureSettings()
|
||||
.then(_ => console.log('To Re-configure, use:'))
|
||||
.catch(_ => console.log('Finish the configuration later using: '))
|
||||
.finally(_ => {
|
||||
console.log('npm run configure');
|
||||
getInput.YesOrNo('Do you want to Reset password for private key now').then(value => {
|
||||
if (value) {
|
||||
let resetPassword = require('./reset-password');
|
||||
resetPassword()
|
||||
.then(_ => console.log('To reset the password again, use: '))
|
||||
.catch(_ => console.log('Reset the password later using: '))
|
||||
.finally(_ => {
|
||||
console.log('npm run reset-password');
|
||||
process.exit(0);
|
||||
})
|
||||
} else {
|
||||
console.log('Reset the password later using:\n' + 'npm run reset-password');
|
||||
process.exit(0);
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.log('Finish the setup later using:\n' + 'npm run setup');
|
||||
process.exit(0);
|
||||
}
|
||||
})
|
||||
94
exchangemarket/setup/reset-password.js
Normal file
94
exchangemarket/setup/reset-password.js
Normal file
@ -0,0 +1,94 @@
|
||||
const fs = require('fs');
|
||||
const getInput = require('./getInput');
|
||||
|
||||
global.floGlobals = require('../docs/scripts/floGlobals');
|
||||
require('../src/set_globals');
|
||||
require('../docs/scripts/lib');
|
||||
const floCrypto = require('../docs/scripts/floCrypto');
|
||||
const path = require('path');
|
||||
|
||||
let _I = "";
|
||||
for (let arg of process.argv)
|
||||
if (/^-I=/.test(arg)) {
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
break;
|
||||
}
|
||||
|
||||
function validateKey(privKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!privKey || privKey === "")
|
||||
throw 'Private Key cannot be empty!';
|
||||
let floID = floCrypto.getFloID(privKey);
|
||||
if (!floID || !floCrypto.verifyPrivKey(privKey, floID))
|
||||
throw 'Invalid Private Key!';
|
||||
return resolve(privKey);
|
||||
} catch (error) {
|
||||
getInput.Text(error + ' Re-Enter: (Cancel)', 'Cancel').then(value => {
|
||||
if (value === 'Cancel')
|
||||
return reject(true);
|
||||
validateKey(value)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getPassword() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInput.Text(`Enter a password [Minimum 8 characters]`, 'Cancel').then(value1 => {
|
||||
if (value1 === 'Cancel')
|
||||
return reject(true);
|
||||
else if (value1.length < 8) {
|
||||
console.log('Password length must be minimum of 8 characters');
|
||||
getPassword()
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else {
|
||||
getInput.Text(`Re-enter password`).then(value2 => {
|
||||
if (value1 !== value2) {
|
||||
console.log('Passwords doesnot match! Try again.');
|
||||
getPassword()
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
} else
|
||||
resolve(value1);
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getInput.Text(`Enter private key`).then(value => {
|
||||
validateKey(value).then(privKey => {
|
||||
getPassword().then(password => {
|
||||
let encrypted = Crypto.AES.encrypt(privKey, password);
|
||||
let randNum = floCrypto.randInt(10, 15);
|
||||
let splitShares = floCrypto.createShamirsSecretShares(encrypted, randNum, randNum);
|
||||
fs.writeFile(path.resolve(__dirname, '..', 'args', `keys${_I}.json`), JSON.stringify(splitShares), 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return reject(false);
|
||||
}
|
||||
console.log('Password reset successful!');
|
||||
resolve(true);
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Password reset cancelled!');
|
||||
reject(true);
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Password reset cancelled!');
|
||||
reject(true);
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!module.parent)
|
||||
resetPassword().then(_ => null).catch(_ => null).finally(_ => process.exit(0));
|
||||
else
|
||||
module.exports = resetPassword;
|
||||
51
exchangemarket/src/_constants.js
Normal file
51
exchangemarket/src/_constants.js
Normal file
@ -0,0 +1,51 @@
|
||||
module.exports = {
|
||||
app: {
|
||||
BLOCKCHAIN_REFRESH_INTERVAL: 1 * 60 * 60 * 1000, //1 hr
|
||||
},
|
||||
request: {
|
||||
SIGN_EXPIRE_TIME: 5 * 60 * 1000, //5 mins
|
||||
MAX_SESSION_TIMEOUT: 30 * 24 * 60 * 60 * 1000, //30 days
|
||||
},
|
||||
background: {
|
||||
PERIOD_INTERVAL: 5 * 60 * 1000, //5 min,
|
||||
WAIT_TIME: 2 * 60 * 1000, //2 mins,
|
||||
REQUEST_TIMEOUT: 24 * 60 * 60 * 1000, //1 day
|
||||
},
|
||||
keys: {
|
||||
SHARES_PER_NODE: 8,
|
||||
SHARE_THRESHOLD: 50 / 100, //50%
|
||||
DISCARD_COOLDOWN: 24 * 60 * 60 * 1000, //1 day
|
||||
SHUFFLE_INTERVAL: 12 * 60 * 60 * 1000, //12 hrs
|
||||
},
|
||||
market: {
|
||||
LAUNCH_SELLER_TAG: "launch-seller",
|
||||
MAXIMUM_LAUNCH_SELL_CHIPS: 100000,
|
||||
TRADE_HASH_PREFIX: "z1",
|
||||
TRANSFER_HASH_PREFIX: "z0"
|
||||
},
|
||||
price: {
|
||||
MIN_TIME: 1 * 60 * 60 * 1000, // 1 hr
|
||||
DOWN_RATE: 0.05 / 100, //0.05% dec
|
||||
UP_RATE: 0.2 / 100, //0.2% inc
|
||||
MAX_DOWN_PER_DAY: 1 / 100, //max 1% dec
|
||||
MAX_UP_PER_DAY: 4 / 100, //max 4% inc
|
||||
CHECK_RATED_SELLER: false,
|
||||
TOP_RANGE: 10 / 100, //top 10%
|
||||
REC_HISTORY_INTERVAL: 1 * 60 * 60 * 1000, //1 hr
|
||||
},
|
||||
convert: {
|
||||
MIN_FUND: 0.3, // 30%
|
||||
TO_FIXED_VALUES: [250, 500],
|
||||
TO_MIN_VALUE: 1000,
|
||||
TO_MAX_VALUE: 10000,
|
||||
FROM_FIXED_VALUES: [0.01],
|
||||
FROM_MIN_VALUE: 0.0001,
|
||||
FROM_MAX_VALUE: 10000,
|
||||
},
|
||||
backup: {
|
||||
HASH_N_ROW: 100,
|
||||
BACKUP_INTERVAL: 5 * 60 * 1000, //5 min
|
||||
BACKUP_SYNC_TIMEOUT: 10 * 60 * 1000, //10 mins
|
||||
CHECKSUM_INTERVAL: 10, //times of BACKUP_INTERVAL
|
||||
}
|
||||
}
|
||||
162
exchangemarket/src/app.js
Normal file
162
exchangemarket/src/app.js
Normal file
@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
//const cookieParser = require("cookie-parser");
|
||||
//const sessions = require('express-session');
|
||||
const Request = require('./request');
|
||||
const path = require('path');
|
||||
const PUBLIC_DIR = path.resolve(__dirname, '..', 'docs');
|
||||
|
||||
module.exports = function App(secret) {
|
||||
|
||||
if (!(this instanceof App))
|
||||
return new App(secret);
|
||||
|
||||
var server = null;
|
||||
const app = express();
|
||||
//session middleware
|
||||
/*app.use(sessions({
|
||||
secret: secret,
|
||||
saveUninitialized: true,
|
||||
resave: false,
|
||||
name: "session"
|
||||
}));*/
|
||||
// parsing the incoming data
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
//serving public file
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
// cookie parser middleware
|
||||
//app.use(cookieParser());
|
||||
|
||||
/* Decentralising - Users will load from user-end files and request via APIs only
|
||||
//Initital page loading
|
||||
app.get('/', (req, res) => res.sendFile('home.html', {
|
||||
root: PUBLIC_DIR
|
||||
}));
|
||||
*/
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
res.setHeader('Access-Control-Allow-Origin', "*");
|
||||
// Request methods you wish to allow
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
||||
// Request headers you wish to allow
|
||||
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
|
||||
// Pass to next layer of middleware
|
||||
next();
|
||||
})
|
||||
|
||||
//get code for login
|
||||
app.get('/get-login-code', Request.GetLoginCode);
|
||||
|
||||
//login request
|
||||
app.post('/login', Request.Login);
|
||||
|
||||
//logout request
|
||||
app.post('/logout', Request.Logout);
|
||||
|
||||
//place sell or buy order
|
||||
app.post('/buy', Request.PlaceBuyOrder);
|
||||
app.post('/sell', Request.PlaceSellOrder);
|
||||
|
||||
//cancel sell or buy order
|
||||
app.post('/cancel', Request.CancelOrder);
|
||||
|
||||
//transfer amount to another user
|
||||
app.post('/transfer-token', Request.TransferToken);
|
||||
|
||||
//list all orders and trades
|
||||
app.get('/list-sellorders', Request.ListSellOrders);
|
||||
app.get('/list-buyorders', Request.ListBuyOrders);
|
||||
app.get('/list-trades', Request.ListTradeTransactions);
|
||||
|
||||
//get rates, balance and tx
|
||||
app.get('/get-rates', Request.GetRates);
|
||||
app.get('/rate-history', Request.GetRateHistory);
|
||||
app.get('/get-balance', Request.GetBalance);
|
||||
app.get('/get-transaction', Request.GetTransaction);
|
||||
app.get('/get-sink', Request.GetSink);
|
||||
|
||||
//get account details
|
||||
app.post('/account', Request.Account);
|
||||
|
||||
//withdraw and deposit request
|
||||
app.post('/deposit-flo', Request.DepositFLO);
|
||||
app.post('/withdraw-flo', Request.WithdrawFLO);
|
||||
app.post('/deposit-token', Request.DepositToken);
|
||||
app.post('/withdraw-token', Request.WithdrawToken);
|
||||
app.post('/get-transact', Request.GetUserTransacts);
|
||||
|
||||
//generate or discard sinks (admin only)
|
||||
app.post('/generate-sink', Request.GenerateSink);
|
||||
app.post('/reshare-sink', Request.ReshareSink);
|
||||
app.post('/discard-sink', Request.DiscardSink);
|
||||
|
||||
//convert from or to coin
|
||||
app.get('/get-convert-values', Request.GetConvertValues);
|
||||
app.post('/convert-to', Request.ConvertTo);
|
||||
app.post('/convert-from', Request.ConvertFrom);
|
||||
app.post('/deposit-convert-coin-fund', Request.DepositConvertCoinFund);
|
||||
app.post('/deposit-convert-currency-fund', Request.DepositConvertCurrencyFund);
|
||||
app.post('/withdraw-convert-coin-fund', Request.WithdrawConvertCoinFund);
|
||||
app.post('/withdraw-convert-currency-fund', Request.WithdrawConvertCurrencyFund);
|
||||
|
||||
//close blockchain-bond and bobs-fund-investment
|
||||
app.post('/close-blockchain-bonds', Request.CloseBlockchainBond);
|
||||
app.post('/close-bobs-fund-investment', Request.CloseBobsFund);
|
||||
|
||||
//check balance for blockchain-bond and bobs-fund (trusted IDs only)
|
||||
app.post('/check-blockchain-bond', Request.CheckBlockchainBondBalance);
|
||||
app.post('/check-bobs-fund', Request.CheckBobsFundBalance);
|
||||
|
||||
//Manage user tags (trusted IDs only)
|
||||
app.post('/add-tag', Request.AddUserTag);
|
||||
app.post('/remove-tag', Request.RemoveUserTag);
|
||||
app.post('/add-distributor', Request.AddDistributor);
|
||||
app.post('/remove-distributor', Request.RemoveDistributor);
|
||||
|
||||
Request.secret = secret;
|
||||
|
||||
//Properties
|
||||
let self = this;
|
||||
|
||||
//return server, express-app
|
||||
Object.defineProperty(self, "server", {
|
||||
get: () => server
|
||||
});
|
||||
Object.defineProperty(self, "express", {
|
||||
get: () => app
|
||||
});
|
||||
|
||||
//set trustedID for subAdmin requests
|
||||
Object.defineProperty(self, "trustedIDs", {
|
||||
set: (ids) => Request.trustedIDs = ids
|
||||
});
|
||||
|
||||
Object.defineProperty(self, "assetList", {
|
||||
set: (assets) => Request.assetList = assets
|
||||
});
|
||||
|
||||
//Refresh data (from blockchain)
|
||||
self.refreshData = (nodeList) => Request.refreshData(nodeList);
|
||||
|
||||
//Start (or) Stop servers
|
||||
self.start = (port) => new Promise(resolve => {
|
||||
server = app.listen(port, () => {
|
||||
resolve(`Server Running at port ${port}`);
|
||||
});
|
||||
});
|
||||
self.stop = () => new Promise(resolve => {
|
||||
server.close(() => {
|
||||
server = null;
|
||||
resolve('Server stopped');
|
||||
});
|
||||
});
|
||||
|
||||
//(Node is not master) Pause serving the clients
|
||||
self.pause = () => Request.pause();
|
||||
//(Node is master) Resume serving the clients
|
||||
self.resume = () => Request.resume();
|
||||
|
||||
}
|
||||
542
exchangemarket/src/background.js
Normal file
542
exchangemarket/src/background.js
Normal file
@ -0,0 +1,542 @@
|
||||
'use strict';
|
||||
|
||||
const keys = require('./keys');
|
||||
const blockchain = require('./blockchain');
|
||||
const conversion_rates = require('./services/conversion').getRate;
|
||||
const bond_util = require('./services/bonds').util;
|
||||
const fund_util = require('./services/bobs-fund').util;
|
||||
const pCode = require('../docs/scripts/floExchangeAPI').processCode;
|
||||
const DB = require("./database");
|
||||
const coupling = require('./coupling');
|
||||
const price = require('./price');
|
||||
|
||||
const {
|
||||
PERIOD_INTERVAL,
|
||||
REQUEST_TIMEOUT
|
||||
} = require('./_constants')['background'];
|
||||
const {
|
||||
LAUNCH_SELLER_TAG,
|
||||
MAXIMUM_LAUNCH_SELL_CHIPS
|
||||
} = require('./_constants')["market"];
|
||||
|
||||
var assetList; //container and allowed assets
|
||||
var updateBalance; // container for updateBalance function
|
||||
|
||||
const verifyTx = {};
|
||||
|
||||
function confirmDepositFLO() {
|
||||
DB.query("SELECT id, floID, txid FROM VaultTransactions WHERE mode=? AND asset=? AND asset_type=? AND r_status=?", [pCode.VAULT_MODE_DEPOSIT, "FLO", pCode.ASSET_TYPE_COIN, pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => {
|
||||
verifyTx.FLO(r.floID, r.txid, keys.sink_groups.EXCHANGE).then(amount => {
|
||||
addSellChipsIfLaunchSeller(r.floID, amount).then(txQueries => {
|
||||
txQueries.push(updateBalance.add(r.floID, "FLO", amount));
|
||||
txQueries.push(["UPDATE VaultTransactions SET r_status=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, amount, r.id]]);
|
||||
DB.transaction(txQueries)
|
||||
.then(result => console.info("FLO deposited:", r.floID, amount))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
verifyTx.FLO = function (sender, txid, group) {
|
||||
return new Promise((resolve, reject) => {
|
||||
floBlockchainAPI.getTx(txid).then(tx => {
|
||||
let vin_sender = tx.vin.filter(v => v.addresses[0] === 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.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((a, v) => keys.sink_chest.includes(group, v.scriptPubKey.addresses[0]) ? a + 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 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 => {
|
||||
if (result) //floID is launch-seller
|
||||
Promise.all([
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS sold FROM TradeTransactions WHERE seller=? AND asset=?", [floID, 'FLO']),
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS brought FROM TradeTransactions WHERE buyer=? AND asset=?", [floID, 'FLO']),
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS chips FROM SellChips WHERE floID=? AND asset=?", [floID, 'FLO']),
|
||||
]).then(result => {
|
||||
let sold = result[0][0].sold,
|
||||
brought = result[1][0].brought,
|
||||
chips = result[2][0].chips;
|
||||
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]]]]);
|
||||
else
|
||||
resolve([]);
|
||||
}).catch(error => reject(error))
|
||||
else //floID is not launch-seller
|
||||
resolve([]);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function confirmDepositToken() {
|
||||
DB.query("SELECT id, floID, txid FROM VaultTransactions WHERE mode=? AND asset_type=? AND r_status=?", [pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_TOKEN, pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => {
|
||||
verifyTx.token(r.floID, r.txid, keys.sink_groups.EXCHANGE).then(({ token, amount, flo_amount }) => {
|
||||
DB.query("SELECT id FROM VaultTransactions where floID=? AND mode=? AND asset=? AND asset_type=? AND txid=?", [r.floID, pCode.VAULT_MODE_DEPOSIT, "FLO", pCode.ASSET_TYPE_COIN, r.txid]).then(result => {
|
||||
let txQueries = [];
|
||||
//Add the FLO balance if necessary
|
||||
if (!result.length) {
|
||||
txQueries.push(updateBalance.add(r.floID, "FLO", flo_amount));
|
||||
txQueries.push(["INSERT INTO VaultTransactions(txid, floID, mode, asset_type, asset, amount, r_status) VALUES (?)", [[r.txid, r.floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_COIN, "FLO", flo_amount, pCode.STATUS_SUCCESS]]]);
|
||||
}
|
||||
txQueries.push(["UPDATE VaultTransactions SET r_status=?, asset=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, token, amount, r.id]]);
|
||||
txQueries.push(updateBalance.add(r.floID, token, amount));
|
||||
DB.transaction(txQueries)
|
||||
.then(result => console.info("Token deposited:", r.floID, token, amount))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
verifyTx.token = function (sender, txid, group, 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 = 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 flo_amount = tx.transactionDetails.vout.reduce((a, v) => keys.sink_chest.includes(group, 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, amount, flo_amount });
|
||||
}).catch(error => reject([false, error]))
|
||||
})
|
||||
}
|
||||
|
||||
function retryVaultWithdrawal() {
|
||||
DB.query("SELECT id, floID, asset, asset_type, amount FROM VaultTransactions WHERE mode=? AND r_status=?", [pCode.VAULT_MODE_WITHDRAW, pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.asset_type == pCode.ASSET_TYPE_COIN) {
|
||||
if (r.asset == "FLO")
|
||||
blockchain.withdrawAsset.retry(r.floID, r.asset, r.amount, r.id);
|
||||
} else if (r.asset_type == pCode.ASSET_TYPE_TOKEN)
|
||||
blockchain.withdrawAsset.retry(r.floID, r.asset, r.amount, r.id)
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmVaultWithdraw() {
|
||||
DB.query("SELECT id, floID, asset, asset_type, amount, txid FROM VaultTransactions WHERE mode=? AND r_status=?", [pCode.VAULT_MODE_WITHDRAW, pCode.STATUS_CONFIRMATION]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.asset_type == pCode.ASSET_TYPE_COIN) {
|
||||
if (r.asset == "FLO")
|
||||
floBlockchainAPI.getTx(r.txid).then(tx => {
|
||||
if (!tx.blockheight || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info("FLO withdrawed:", r.floID, r.amount))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
else if (r.asset == "BTC")
|
||||
btcOperator.getTx(r.txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info("BTC withdrawed:", r.floID, r.amount))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
} else if (r.asset_type == pCode.ASSET_TYPE_TOKEN)
|
||||
floTokenAPI.getTx(r.txid).then(tx => {
|
||||
if (!tx.transactionDetails.blockheight || !tx.transactionDetails.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE VaultTransactions SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info("Token withdrawed:", r.floID, r.asset, r.amount))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
})
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
verifyTx.BTC = function (sender, txid, group) {
|
||||
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.inputs.length)
|
||||
return reject([true, "Transaction input containes other floIDs"]);
|
||||
if (!tx.block)
|
||||
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) =>
|
||||
keys.sink_chest.includes(group, 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() {
|
||||
//Set all timeout convert request to refund mode (thus, asset will be refund if tx gets confirmed later)
|
||||
let req_timeout = new Date(Date.now() - REQUEST_TIMEOUT),
|
||||
to_refund_sql = "INSERT INTO RefundConvert (floID, in_txid, asset_type, asset, r_status)" +
|
||||
" SELECT floID, in_txid, ? AS asset_type, ? AS asset, r_status FROM DirectConvert" +
|
||||
" WHERE r_status=? AND locktime<? AND mode=?";
|
||||
let txQueries = [];
|
||||
txQueries.push([to_refund_sql, [pCode.ASSET_TYPE_TOKEN, floGlobals.currency, pCode.STATUS_PENDING, req_timeout, pCode.CONVERT_MODE_GET]]);
|
||||
txQueries.push([to_refund_sql, [pCode.ASSET_TYPE_COIN, "BTC", pCode.STATUS_PENDING, req_timeout, pCode.CONVERT_MODE_PUT]]);
|
||||
txQueries.push(["UPDATE DirectConvert SET r_status=? WHERE r_status=? AND locktime<?", [pCode.STATUS_REJECTED, pCode.STATUS_PENDING, req_timeout]]);
|
||||
DB.transaction(txQueries).then(result => {
|
||||
DB.query("SELECT id, floID, mode, in_txid, amount, quantity FROM DirectConvert WHERE r_status=? AND coin=?", [pCode.STATUS_PENDING, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET) {
|
||||
verifyTx.token(r.floID, r.in_txid, keys.sink_groups.CONVERT, true).then(({ amount }) => {
|
||||
if (r.amount !== amount)
|
||||
throw ([true, "Transaction amount mismatched in blockchain"]);
|
||||
conversion_rates.BTC_INR().then(rate => {
|
||||
blockchain.convertToCoin.init(r.floID, "BTC", amount, rate, r.id)
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE DirectConvert SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
} else if (r.mode == pCode.CONVERT_MODE_PUT) {
|
||||
verifyTx.BTC(r.floID, r.in_txid, keys.sink_groups.CONVERT).then(quantity => {
|
||||
if (r.quantity !== quantity)
|
||||
throw ([true, "Transaction quantity mismatched in blockchain"]);
|
||||
conversion_rates.BTC_INR().then(rate => {
|
||||
blockchain.convertFromCoin.init(r.floID, quantity, rate, r.id)
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE DirectConvert SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
}
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function retryConvert() {
|
||||
DB.query("SELECT id, floID, mode, amount, quantity FROM DirectConvert WHERE r_status=? AND coin=?", [pCode.STATUS_PROCESSING, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET)
|
||||
blockchain.convertToCoin.retry(r.floID, "BTC", r.quantity, r.id);
|
||||
else if (r.mode == pCode.CONVERT_MODE_PUT)
|
||||
blockchain.convertFromCoin.retry(r.floID, r.amount, r.id)
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmConvert() {
|
||||
DB.query("SELECT id, floID, mode, amount, quantity, out_txid FROM DirectConvert WHERE r_status=? AND coin=?", [pCode.STATUS_CONFIRMATION, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET)
|
||||
btcOperator.getTx(r.out_txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE DirectConvert SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`${r.floID} converted ${r.amount} to ${r.quantity} BTC`))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
else if (r.mode == pCode.CONVERT_MODE_PUT)
|
||||
floTokenAPI.getTx(r.out_txid).then(tx => {
|
||||
if (!tx.transactionDetails.blockheight || !tx.transactionDetails.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE DirectConvert SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`${r.floID} converted ${r.quantity} BTC to ${r.amount}`))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
})
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function verifyConvertFundDeposit() {
|
||||
DB.query("SELECT id, mode, txid, coin FROM ConvertFund WHERE r_status=? AND coin=?", [pCode.STATUS_PROCESSING, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET) { //deposit currency
|
||||
verifyTx.token(floGlobals.adminID, r.txid, keys.sink_groups.CONVERT, true).then(({ amount }) => {
|
||||
DB.query("UPDATE ConvertFund SET r_status=?, amount=? WHERE id=?", [pCode.STATUS_SUCCESS, amount, r.id])
|
||||
.then(result => console.info(`Deposit-fund ${amount} ${floGlobals.currency} successful`))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE ConvertFund SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
} else if (r.mode == pCode.CONVERT_MODE_PUT) {//deposit coin
|
||||
verifyTx.BTC(floGlobals.adminID, r.txid, keys.sink_groups.CONVERT).then(quantity => {
|
||||
DB.query("UPDATE ConvertFund SET r_status=?, quantity=? WHERE id=?", [pCode.STATUS_SUCCESS, quantity, r.id])
|
||||
.then(result => console.info(`Deposit-fund ${quantity} ${r.coin} successful`))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE ConvertFund SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
}
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function retryConvertFundWithdraw() {
|
||||
DB.query("SELECT id, mode, coin, quantity, amount FROM ConvertFund WHERE r_status=? AND coin=?", [pCode.STATUS_PENDING, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET) //withdraw coin
|
||||
blockchain.convertFundWithdraw.retry(r.coin, r.quantity, r.id);
|
||||
else if (r.mode == pCode.CONVERT_MODE_PUT) //withdraw currency
|
||||
blockchain.convertFundWithdraw.retry(floGlobals.currency, r.amount, r.id);
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmConvertFundWithdraw() {
|
||||
DB.query("SELECT * FROM ConvertFund WHERE r_status=? AND coin=?", [pCode.STATUS_CONFIRMATION, "BTC"]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.mode == pCode.CONVERT_MODE_GET) { //withdraw coin
|
||||
btcOperator.getTx(r.txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE ConvertFund SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`Withdraw-fund ${r.quantity} ${r.coin} successful`))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
} else if (r.mode == pCode.CONVERT_MODE_PUT) {//withdraw currency
|
||||
floTokenAPI.getTx(r.txid).then(tx => {
|
||||
if (!tx.transactionDetails.blockheight || !tx.transactionDetails.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE ConvertFund SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`Withdraw-fund ${r.amount} ${floGlobals.currency} successful`))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function verifyConvertRefund() {
|
||||
DB.query("SELECT id, floID, asset_type, asset, in_txid FROM RefundConvert WHERE r_status=?", [pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.ASSET_TYPE_COIN) {
|
||||
if (r.asset == "BTC") //Convert is only for BTC right now
|
||||
verifyTx.BTC(r.floID, r.in_txid, keys.sink_groups.CONVERT)
|
||||
.then(amount => blockchain.refundConvert.init(r.floID, r.asset, amount, r.id))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE RefundConvert SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
} else if (r.ASSET_TYPE_TOKEN)
|
||||
verifyTx.token(r.floID, r.in_txid, keys.sink_groups.CONVERT, true).then(({ amount }) => {
|
||||
blockchain.refundConvert.init(r.floID, floGlobals.currency, amount, r.id);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
if (error[0])
|
||||
DB.query("UPDATE RefundConvert SET r_status=? WHERE id=?", [pCode.STATUS_REJECTED, r.id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
});
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function retryConvertRefund() {
|
||||
DB.query("SELECT id, floID, asset, amount FROM RefundConvert WHERE r_status=?", [pCode.STATUS_PROCESSING]).then(results => {
|
||||
results.forEach(r => blockchain.refundConvert.retry(r.floID, r.asset, r.amount, r.id))
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmConvertRefund() {
|
||||
DB.query("SELECT * FROM RefundConvert WHERE r_status=?", [pCode.STATUS_CONFIRMATION]).then(results => {
|
||||
results.forEach(r => {
|
||||
if (r.ASSET_TYPE_COIN) {
|
||||
if (r.asset == "BTC") //Convert is only for BTC right now
|
||||
btcOperator.getTx(r.out_txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE RefundConvert SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`Refunded ${r.amount} ${r.asset} to ${r.floID}`))
|
||||
.catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
} else if (r.ASSET_TYPE_TOKEN)
|
||||
floTokenAPI.getTx(r.out_txid).then(tx => {
|
||||
if (!tx.transactionDetails.blockheight || !tx.transactionDetails.confirmations) //Still not confirmed
|
||||
return;
|
||||
DB.query("UPDATE RefundConvert SET r_status=? WHERE id=?", [pCode.STATUS_SUCCESS, r.id])
|
||||
.then(result => console.info(`Refunded ${r.amount} ${r.asset} to ${r.floID}`))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error));
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function retryBondClosing() {
|
||||
DB.query("SELECT id, floID, amount, btc_net, usd_net FROM CloseBondTransact WHERE r_status=?", [pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => blockchain.bondTransact.retry(r.floID, r.amount, r.btc_net, r.usd_net, r.id))
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmBondClosing() {
|
||||
DB.query("SELECT * FROM CloseBondTransact WHERE r_status=?", [pCode.STATUS_CONFIRMATION]).then(results => {
|
||||
results.forEach(r => {
|
||||
btcOperator.getTx(r.txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
let closeBondString = bond_util.stringify.end(r.bond_id, r.end_date, r.btc_net, r.usd_net, r.amount, r.ref_sign, r.txid);
|
||||
floBlockchainAPI.writeData(keys.node_id, closeBondString, keys.node_priv, bond_util.config.adminID).then(txid => {
|
||||
DB.query("UPDATE CloseBondTransact SET r_status=?, close_id=? WHERE id=?", [pCode.STATUS_SUCCESS, txid, r.id])
|
||||
.then(result => console.info("Bond closed:", r.bond_id))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function retryFundClosing() {
|
||||
DB.query("SELECT id, floID, amount, btc_net, usd_net FROM CloseFundTransact WHERE r_status=?", [pCode.STATUS_PENDING]).then(results => {
|
||||
results.forEach(r => blockchain.fundTransact.retry(r.floID, r.amount, r.btc_net, r.usd_net, r.id))
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function confirmFundClosing() {
|
||||
DB.query("SELECT * FROM CloseFundTransact WHERE r_status=?", [pCode.STATUS_CONFIRMATION]).then(results => {
|
||||
results.forEach(r => {
|
||||
btcOperator.getTx(r.txid).then(tx => {
|
||||
if (!tx.block || !tx.confirmations) //Still not confirmed
|
||||
return;
|
||||
let closeFundString = fund_util.stringify.end(r.fund_id, r.floID, r.end_date, r.btc_net, r.usd_net, r.amount, r.ref_sign, r.txid);
|
||||
floBlockchainAPI.writeData(keys.node_id, closeFundString, keys.node_priv, fund_util.config.adminID).then(txid => {
|
||||
DB.query("UPDATE CloseFundTransact SET r_status=?, close_id=? WHERE id=?", [pCode.STATUS_SUCCESS, txid, r.id])
|
||||
.then(result => console.info("Fund investment closed:", r.fund_id, r.floID))
|
||||
.catch(error => console.error(error));
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
//Periodic Process
|
||||
|
||||
function processAll() {
|
||||
//deposit-withdraw asset balance
|
||||
if (keys.sink_chest.list(keys.sink_groups.EXCHANGE).length) {
|
||||
confirmDepositFLO();
|
||||
confirmDepositToken();
|
||||
retryVaultWithdrawal();
|
||||
confirmVaultWithdraw();
|
||||
}
|
||||
//convert service
|
||||
if (keys.sink_chest.list(keys.sink_groups.CONVERT).length) {
|
||||
verifyConvert();
|
||||
retryConvert();
|
||||
confirmConvert();
|
||||
verifyConvertFundDeposit();
|
||||
retryConvertFundWithdraw();
|
||||
confirmConvertFundWithdraw();
|
||||
verifyConvertRefund();
|
||||
retryConvertRefund();
|
||||
confirmConvertRefund();
|
||||
}
|
||||
//blockchain-bond service
|
||||
if (keys.sink_chest.list(keys.sink_groups.BLOCKCHAIN_BONDS).length) {
|
||||
retryBondClosing();
|
||||
confirmBondClosing();
|
||||
}
|
||||
//bob's fund service
|
||||
if (keys.sink_chest.list(keys.sink_groups.EXCHANGE).length) {
|
||||
retryFundClosing();
|
||||
confirmFundClosing();
|
||||
}
|
||||
}
|
||||
|
||||
var lastSyncBlockHeight = 0;
|
||||
|
||||
function periodicProcess() {
|
||||
floBlockchainAPI.promisedAPI('api/status').then(result => {
|
||||
let blockbook_height = result.blockbook.bestHeight;
|
||||
if (lastSyncBlockHeight < blockbook_height) {
|
||||
lastSyncBlockHeight = blockbook_height;
|
||||
processAll();
|
||||
console.log("Last Block :", lastSyncBlockHeight);
|
||||
}
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function periodicProcess_start() {
|
||||
periodicProcess_stop();
|
||||
periodicProcess();
|
||||
assetList.forEach(asset => coupling.initiate(asset, true));
|
||||
price.storeHistory.start();
|
||||
periodicProcess.instance = setInterval(periodicProcess, PERIOD_INTERVAL);
|
||||
}
|
||||
|
||||
function periodicProcess_stop() {
|
||||
if (periodicProcess.instance !== undefined) {
|
||||
clearInterval(periodicProcess.instance);
|
||||
delete periodicProcess.instance;
|
||||
}
|
||||
coupling.stopAll();
|
||||
price.storeHistory.stop();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
blockchain,
|
||||
periodicProcess: {
|
||||
start: periodicProcess_start,
|
||||
stop: periodicProcess_stop
|
||||
},
|
||||
set assetList(assets) {
|
||||
assetList = assets;
|
||||
},
|
||||
set updateBalance(f) {
|
||||
updateBalance = f;
|
||||
}
|
||||
}
|
||||
495
exchangemarket/src/backup/head.js
Normal file
495
exchangemarket/src/backup/head.js
Normal file
@ -0,0 +1,495 @@
|
||||
'use strict';
|
||||
|
||||
const keys = require('../keys');
|
||||
const K_Bucket = require('../../docs/scripts/floExchangeAPI').K_Bucket;
|
||||
const slave = require('./slave');
|
||||
const sync = require('./sync');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const { BACKUP_INTERVAL } = require("../_constants")["backup"];
|
||||
const { DISCARD_COOLDOWN } = require("../_constants")["keys"];
|
||||
|
||||
var _app, _wss, tokenList; //Container for app and wss
|
||||
var nodeList, nodeURL, nodeKBucket; //Container for (backup) node list
|
||||
const connectedSlaves = {},
|
||||
shares_collected = {},
|
||||
shares_pending = {},
|
||||
discarded_sinks = [];
|
||||
|
||||
var _mode = null;
|
||||
const SLAVE_MODE = 0,
|
||||
MASTER_MODE = 1;
|
||||
|
||||
//Shares
|
||||
function generateShares(sinkKey) {
|
||||
let nextNodes = nodeKBucket.nextNode(keys.node_id, null),
|
||||
aliveNodes = Object.keys(connectedSlaves);
|
||||
nextNodes.unshift(keys.node_id);
|
||||
aliveNodes.unshift(keys.node_id);
|
||||
let shares, mappedShares = {};
|
||||
shares = keys.generateShares(sinkKey, nextNodes.length, aliveNodes.length);
|
||||
for (let i in nextNodes)
|
||||
mappedShares[nextNodes[i]] = shares[i];
|
||||
return mappedShares;
|
||||
}
|
||||
|
||||
function sendShares(ws, sinkID) {
|
||||
if (!(sinkID in shares_pending) || !(ws.floID in shares_pending[sinkID].shares))
|
||||
return;
|
||||
let { ref, group } = shares_pending[sinkID],
|
||||
shares = shares_pending[sinkID].shares[ws.floID];
|
||||
delete shares_pending[sinkID].shares[ws.floID]; //delete the share after sending it to respective slave
|
||||
shares.forEach(s => ws.send(JSON.stringify({
|
||||
command: "SINK_SHARE",
|
||||
sinkID, ref, group,
|
||||
share: floCrypto.encryptData(s, ws.pubKey)
|
||||
})));
|
||||
}
|
||||
|
||||
function sendSharesToNodes(sinkID, group, shares) {
|
||||
if (discarded_sinks.includes(sinkID)) { //sinkID is discarded, abort the new shares
|
||||
let i = discarded_sinks.findIndex(sinkID);
|
||||
discarded_sinks.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
let ref = Date.now();
|
||||
shares_pending[sinkID] = { shares, group, ref };
|
||||
if (keys.node_id in shares) {
|
||||
shares_pending[sinkID].shares[keys.node_id].forEach(s =>
|
||||
keys.addShare(group, sinkID, ref, s).then(_ => null).catch(error => console.error(error)));
|
||||
delete shares_pending[sinkID].shares[keys.node_id];
|
||||
}
|
||||
for (let node in shares)
|
||||
if (node in connectedSlaves)
|
||||
sendShares(connectedSlaves[node], sinkID);
|
||||
keys.sink_chest.set_id(group, sinkID, ref);
|
||||
}
|
||||
|
||||
function requestShare(ws, group, sinkID) {
|
||||
ws.send(JSON.stringify({
|
||||
command: "SEND_SHARE",
|
||||
group, sinkID,
|
||||
pubKey: keys.node_pub
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
function transferMoneyToNewSink(oldSinkID, oldSinkKey, newSink) {
|
||||
const transferToken = token => new Promise((resolve, reject) => {
|
||||
floTokenAPI.getBalance(oldSinkID, token).then(tokenBalance => {
|
||||
floBlockchainAPI.writeData(oldSinkID, `send ${tokenBalance} ${token}# |Exchange-market New sink`, oldSinkKey, newSink.floID, false)
|
||||
.then(txid => resolve(txid))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
console.debug("Transferring tokens to new Sink:", newSink.floID)
|
||||
Promise.allSettled(tokenList.map(token => transferToken(token))).then(result => {
|
||||
let failedFlag = false;
|
||||
tokenList.forEach((token, i) => {
|
||||
if (result[i].status === "fulfilled")
|
||||
console.log(token, result[i].value);
|
||||
else {
|
||||
failedFlag = true;
|
||||
console.error(token, result[i].reason);
|
||||
}
|
||||
});
|
||||
if (failedFlag)
|
||||
return reject("Some token transfer has failed");
|
||||
floBlockchainAPI.getBalance(oldSinkID).then(floBalance => {
|
||||
floTokenAPI.getBalance(oldSinkID).then(cashBalance => {
|
||||
floBlockchainAPI.sendTx(oldSinkID, newSink.floID, floBalance - floGlobals.fee, oldSinkKey, `send ${cashBalance} ${floGlobals.currency}# |Exchange-market New sink`)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
function collectAndCall(group, sinkID, callback, timeout = null) {
|
||||
if (!(callback instanceof Function))
|
||||
throw Error("callback should be a function");
|
||||
if (!(sinkID in shares_collected)) { //if not already collecting shares for sinkID, then initiate collection
|
||||
shares_collected[sinkID] = { group, ref: 0, callbacks: [], shares: {} };
|
||||
for (let floID in connectedSlaves)
|
||||
requestShare(connectedSlaves[floID], group, sinkID);
|
||||
keys.getShares(group, sinkID)
|
||||
.then(({ ref, shares }) => shares.forEach(s => collectShares(sinkID, ref, s)))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
shares_collected[sinkID].callbacks.push(callback);
|
||||
if (timeout)
|
||||
setTimeout(() => {
|
||||
if (sinkID in shares_collected) {
|
||||
let i = shares_collected[sinkID].callbacks.indexOf(callback);
|
||||
delete shares_collected[sinkID].callbacks[i]; //deleting will empty the index, but space will be there so that order of other indexes are not affected
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
collectAndCall.isAlive = (sinkID, callbackRef) => (sinkID in shares_collected && shares_collected[sinkID].callbacks.indexOf(callbackRef) != -1);
|
||||
|
||||
function collectShares(sinkID, ref, share) {
|
||||
if (_mode !== MASTER_MODE)
|
||||
return console.warn("Not serving as master");
|
||||
if (!(sinkID in shares_collected))
|
||||
return console.debug("Received shares for sink thats not been collected right now");
|
||||
if (shares_collected[sinkID].ref > ref)
|
||||
return console.debug("Received expired share");
|
||||
else if (shares_collected[sinkID].ref < ref) {
|
||||
shares_collected[sinkID].ref = ref;
|
||||
shares_collected[sinkID].shares = [];
|
||||
}
|
||||
if (shares_collected[sinkID].shares.includes(share))
|
||||
return console.debug("Received duplicate share");
|
||||
shares_collected[sinkID].shares.push(share);
|
||||
try {
|
||||
let sinkKey = floCrypto.retrieveShamirSecret(shares_collected[sinkID].shares);
|
||||
if (floCrypto.verifyPrivKey(sinkKey, sinkID)) {
|
||||
console.debug("Shares collected successfully for", sinkID);
|
||||
shares_collected[sinkID].callbacks.forEach(fn => fn instanceof Function ? fn(sinkKey) : null);
|
||||
delete shares_collected[sinkID];
|
||||
}
|
||||
} catch {
|
||||
//Unable to retrive sink private key. Waiting for more shares! Do nothing for now
|
||||
};
|
||||
}
|
||||
|
||||
function connectWS(floID) {
|
||||
let url = nodeURL[floID];
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket('wss://' + url);
|
||||
ws.on('open', _ => resolve(ws));
|
||||
ws.on('error', error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function connectToMaster(i = 0, init = false) {
|
||||
if (i >= nodeList.length) {
|
||||
console.error("No master is found and Node not in list. This should not happen!");
|
||||
process.exit(1);
|
||||
}
|
||||
let floID = nodeList[i];
|
||||
if (floID === keys.node_id)
|
||||
serveAsMaster(init);
|
||||
else
|
||||
connectWS(floID).then(ws => {
|
||||
ws.floID = floID;
|
||||
ws.onclose = () => connectToMaster(i);
|
||||
serveAsSlave(ws, init);
|
||||
}).catch(error => {
|
||||
console.log(`Node(${floID}) is offline`);
|
||||
connectToMaster(i + 1, init)
|
||||
});
|
||||
}
|
||||
|
||||
function informLiveNodes(init) {
|
||||
let message = {
|
||||
floID: keys.node_id,
|
||||
type: "UPDATE_MASTER",
|
||||
pubKey: keys.node_pub,
|
||||
req_time: Date.now()
|
||||
};
|
||||
message.sign = floCrypto.signData(message.type + "|" + message.req_time, keys.node_priv);
|
||||
message = JSON.stringify(message);
|
||||
let nodes = nodeList.filter(n => n !== keys.node_id);
|
||||
Promise.allSettled(nodes.map(n => connectWS(n))).then(result => {
|
||||
let flag = false;
|
||||
for (let i in result)
|
||||
if (result[i].status === "fulfilled") {
|
||||
let ws = result[i].value;
|
||||
ws.send(message);
|
||||
ws.close();
|
||||
flag = true;
|
||||
} else
|
||||
console.warn(`Node(${nodes[i]}) is offline`);
|
||||
if (init && flag)
|
||||
syncRequest();
|
||||
keys.getStoredList().then(stored_list => {
|
||||
if (Object.keys(stored_list).length) {
|
||||
keys.getDiscardedList().then(discarded_list => {
|
||||
let cur_time = Date.now();
|
||||
for (let group in stored_list)
|
||||
stored_list[group].forEach(id => {
|
||||
if (!(id in discarded_list))
|
||||
reconstructShares(group, id)
|
||||
else if (cur_time - discarded_list[id] < DISCARD_COOLDOWN) //sinkID still in cooldown period
|
||||
keys.sink_chest.set_id(group, id, null);
|
||||
});
|
||||
}).catch(error => console.error(error))
|
||||
} else if (init && !flag) {
|
||||
console.log("Starting the exchange...");
|
||||
//generate a sinkID for each group in starting list
|
||||
keys.sink_groups.initial_list.forEach(group =>
|
||||
generateSink(group).then(_ => null).catch(e => console.error(e)));
|
||||
}
|
||||
}).catch(error => console.error(error));
|
||||
});
|
||||
}
|
||||
|
||||
function syncRequest(cur = keys.node_id) {
|
||||
//Sync data from next available node
|
||||
let nextNode = nodeKBucket.nextNode(cur);
|
||||
if (!nextNode)
|
||||
return console.warn("No nodes available to Sync");
|
||||
connectWS(nextNode)
|
||||
.then(ws => slave.syncRequest(ws))
|
||||
.catch(_ => syncRequest(nextNode));
|
||||
}
|
||||
|
||||
function updateMaster(floID) {
|
||||
let currentMaster = _mode === MASTER_MODE ? keys.node_id : slave.masterWS.floID;
|
||||
if (nodeList.indexOf(floID) < nodeList.indexOf(currentMaster))
|
||||
connectToMaster();
|
||||
}
|
||||
|
||||
function reconstructAllActiveShares() {
|
||||
if (_mode !== MASTER_MODE)
|
||||
return console.debug("Not serving as master");
|
||||
console.debug("Reconstructing shares for all active IDs")
|
||||
let group_list = keys.sink_groups.list;
|
||||
group_list.forEach(g => {
|
||||
//active ids also ignore ids that are in queue for reconstructing shares
|
||||
let active_ids = keys.sink_chest.active_list(g);
|
||||
active_ids.forEach(id => reconstructShares(g, id));
|
||||
});
|
||||
}
|
||||
|
||||
function reconstructShares(group, sinkID) {
|
||||
if (_mode !== MASTER_MODE)
|
||||
return console.warn(`Not serving as master, but reconstruct-shares is called for ${sinkID}(${group})`);
|
||||
keys.sink_chest.set_id(group, sinkID, null);
|
||||
collectAndCall(group, sinkID, sinkKey => sendSharesToNodes(sinkID, group, generateShares(sinkKey)));
|
||||
}
|
||||
|
||||
function slaveConnect(floID, pubKey, ws, slave_sinks) {
|
||||
if (_mode !== MASTER_MODE)
|
||||
return console.warn("Not serving as master");
|
||||
ws.floID = floID;
|
||||
ws.pubKey = pubKey;
|
||||
connectedSlaves[floID] = ws;
|
||||
|
||||
//Send shares if need to be delivered
|
||||
for (let sinkID in shares_pending)
|
||||
if (floID in shares_pending[sinkID].shares)
|
||||
sendShares(ws, sinkID);
|
||||
//Request shares if any
|
||||
for (let sinkID in shares_collected)
|
||||
requestShare(ws, shares_collected[sinkID].group, sinkID);
|
||||
//check if sinks in slaves are present
|
||||
if (slave_sinks instanceof Object) {
|
||||
for (let group in slave_sinks)
|
||||
for (let sinkID of slave_sinks[group]) {
|
||||
if (!keys.sink_chest.includes(group, sinkID))
|
||||
keys.checkIfDiscarded(sinkID)
|
||||
.then(result => result === false ? reconstructShares(group, sinkID) : null)
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eCode = require('../../docs/scripts/floExchangeAPI').errorCode;
|
||||
|
||||
function generateSink(group) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!keys.sink_groups.generate_list.includes(group))
|
||||
return reject(INVALID(eCode.INVALID_VALUE, `Invalid Group ${group}`));
|
||||
try {
|
||||
let newSink = floCrypto.generateNewID();
|
||||
console.debug("Generated sink:", group, newSink.floID);
|
||||
sendSharesToNodes(newSink.floID, group, generateShares(newSink.privKey));
|
||||
resolve(`Generated ${newSink.floID} (${group})`);
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function reshareSink(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(id))
|
||||
return reject(INVALID(eCode.INVALID_VALUE, `Invalid ID ${id}`));
|
||||
else {
|
||||
let group = keys.sink_chest.find_group(id);
|
||||
if (!group)
|
||||
return reject(INVALID(eCode.NOT_FOUND, `ID ${id} not found`));
|
||||
else keys.checkIfDiscarded(id).then(result => {
|
||||
if (result)
|
||||
return reject(INVALID(eCode.NOT_FOUND, `ID is discarded`));
|
||||
try {
|
||||
reconstructShares(group, id);
|
||||
resolve(`Resharing ${id} (${group})`);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function discardSink(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(id))
|
||||
return reject(INVALID(eCode.INVALID_VALUE, `Invalid ID ${id}`));
|
||||
else if (!keys.sink_chest.find_group(id))
|
||||
return reject(INVALID(eCode.NOT_FOUND, `ID ${id} not found`));
|
||||
else keys.checkIfDiscarded(id).then(result => {
|
||||
if (result)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, `ID already discarded`));
|
||||
keys.discardSink(id).then(result => {
|
||||
console.debug("Discarded sink:", id);
|
||||
resolve(result);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkForDiscardedSinks() {
|
||||
let cur_time = Date.now(),
|
||||
all_sinks = keys.sink_chest.get_all();
|
||||
for (let group in all_sinks)
|
||||
all_sinks[group].forEach(id => keys.checkIfDiscarded(id).then(result => {
|
||||
console.debug(group, id); //Check if group is correctly mapped, or if its changed by loop
|
||||
if (result != false) {
|
||||
if (cur_time - result > DISCARD_COOLDOWN)
|
||||
keys.sink_chest.rm_id(group, id);
|
||||
else
|
||||
keys.sink_chest.set_id(group, id, null);
|
||||
if (id in shares_collected && !discarded_sinks.includes(id))
|
||||
discarded_sinks.push(id);
|
||||
}
|
||||
}).catch(error => console.debug(error)))
|
||||
}
|
||||
|
||||
//Master interval process
|
||||
function intervalProcess() {
|
||||
checkForDiscardedSinks();
|
||||
}
|
||||
|
||||
intervalProcess.start = () => {
|
||||
intervalProcess.stop();
|
||||
intervalProcess.instance = setInterval(intervalProcess, BACKUP_INTERVAL);
|
||||
}
|
||||
|
||||
intervalProcess.stop = () => {
|
||||
if (intervalProcess.instance !== undefined) {
|
||||
clearInterval(intervalProcess.instance);
|
||||
delete intervalProcess.instance;
|
||||
}
|
||||
}
|
||||
|
||||
//Node becomes master
|
||||
function serveAsMaster(init) {
|
||||
console.info('Starting master process');
|
||||
slave.stop();
|
||||
_mode = MASTER_MODE;
|
||||
keys.sink_chest.reset();
|
||||
intervalProcess.start();
|
||||
informLiveNodes(init);
|
||||
_app.resume();
|
||||
}
|
||||
|
||||
//Node becomes slave
|
||||
function serveAsSlave(ws, init) {
|
||||
console.info('Starting slave process');
|
||||
intervalProcess.stop();
|
||||
_app.pause();
|
||||
slave.start(ws, init);
|
||||
_mode = SLAVE_MODE;
|
||||
}
|
||||
|
||||
//Transmistter
|
||||
function startBackupTransmitter(server) {
|
||||
_wss = new WebSocket.Server({
|
||||
server
|
||||
});
|
||||
_wss.on('connection', ws => {
|
||||
ws.on('message', message => {
|
||||
//verify if from a backup node
|
||||
try {
|
||||
let invalid = null,
|
||||
request = JSON.parse(message);
|
||||
//console.debug(request);
|
||||
if (!nodeList.includes(request.floID))
|
||||
invalid = `floID ${request.floID} not in nodeList`;
|
||||
else if (request.floID !== floCrypto.getFloID(request.pubKey))
|
||||
invalid = "Invalid pubKey";
|
||||
else if (!floCrypto.verifySign(request.type + "|" + request.req_time, request.sign, request.pubKey))
|
||||
invalid = "Invalid signature";
|
||||
//TODO: check if request time is valid;
|
||||
else switch (request.type) {
|
||||
case "BACKUP_SYNC":
|
||||
sync.sendBackupData(request.last_time, request.checksum, ws);
|
||||
break;
|
||||
case "HASH_SYNC":
|
||||
sync.sendTableHash(request.tables, ws);
|
||||
break;
|
||||
case "RE_SYNC":
|
||||
sync.sendTableData(request.tables, ws);
|
||||
break;
|
||||
case "UPDATE_MASTER":
|
||||
updateMaster(request.floID);
|
||||
break;
|
||||
case "SLAVE_CONNECT":
|
||||
slaveConnect(request.floID, request.pubKey, ws, request.sinks);
|
||||
break;
|
||||
case "SINK_SHARE":
|
||||
collectShares(request.sinkID, request.ref, floCrypto.decryptData(request.share, keys.node_priv))
|
||||
default:
|
||||
invalid = "Invalid Request Type";
|
||||
}
|
||||
if (invalid)
|
||||
ws.send(JSON.stringify({
|
||||
type: request.type,
|
||||
command: "REQUEST_ERROR",
|
||||
error: invalid
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ws.send(JSON.stringify({
|
||||
command: "REQUEST_ERROR",
|
||||
error: 'Unable to process the request!'
|
||||
}));
|
||||
}
|
||||
});
|
||||
ws.on('close', () => {
|
||||
// remove from connected slaves (if needed)
|
||||
if (ws.floID in connectedSlaves)
|
||||
delete connectedSlaves[ws.floID];
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function initProcess(app) {
|
||||
_app = app;
|
||||
startBackupTransmitter(_app.server);
|
||||
connectToMaster(0, true);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: initProcess,
|
||||
collectAndCall,
|
||||
reconstructAllActiveShares,
|
||||
sink: {
|
||||
generate: generateSink,
|
||||
reshare: reshareSink,
|
||||
discard: discardSink
|
||||
},
|
||||
set nodeList(list) {
|
||||
nodeURL = list;
|
||||
nodeKBucket = new K_Bucket(floGlobals.adminID, Object.keys(nodeURL));
|
||||
nodeList = nodeKBucket.order;
|
||||
},
|
||||
get nodeList() {
|
||||
return nodeList;
|
||||
},
|
||||
set assetList(assets) {
|
||||
tokenList = assets.filter(a => a.toUpperCase() !== "FLO");
|
||||
},
|
||||
get wss() {
|
||||
return _wss;
|
||||
}
|
||||
};
|
||||
429
exchangemarket/src/backup/slave.js
Normal file
429
exchangemarket/src/backup/slave.js
Normal file
@ -0,0 +1,429 @@
|
||||
'use strict';
|
||||
|
||||
const keys = require("../keys");
|
||||
const DB = require("../database");
|
||||
const { getTableHashes } = require("./sync");
|
||||
|
||||
const {
|
||||
BACKUP_INTERVAL,
|
||||
BACKUP_SYNC_TIMEOUT,
|
||||
CHECKSUM_INTERVAL,
|
||||
HASH_N_ROW
|
||||
} = require("../_constants")["backup"];
|
||||
|
||||
var masterWS = null; //Container for Master websocket connection
|
||||
|
||||
var intervalID = null;
|
||||
|
||||
function startSlaveProcess(ws, init) {
|
||||
if (!ws) throw Error("Master WS connection required");
|
||||
//stop existing process
|
||||
stopSlaveProcess();
|
||||
//set masterWS
|
||||
ws.on('message', processDataFromMaster);
|
||||
masterWS = ws;
|
||||
let sinks_stored = {};
|
||||
Promise.all([keys.getStoredList(), keys.getDiscardedList()]).then(result => {
|
||||
let stored_list = result[0],
|
||||
discarded_list = result[1];
|
||||
for (let group in stored_list) {
|
||||
sinks_stored[group] = [];
|
||||
for (let id of stored_list[group])
|
||||
if (!(id in discarded_list))
|
||||
sinks_stored[group].push(id);
|
||||
}
|
||||
}).catch(error => console.error(error)).finally(_ => {
|
||||
//inform master
|
||||
let message = {
|
||||
floID: keys.node_id,
|
||||
pubKey: keys.node_pub,
|
||||
sinks: sinks_stored,
|
||||
req_time: Date.now(),
|
||||
type: "SLAVE_CONNECT"
|
||||
}
|
||||
message.sign = floCrypto.signData(message.type + "|" + message.req_time, keys.node_priv);
|
||||
ws.send(JSON.stringify(message));
|
||||
//start sync
|
||||
if (init)
|
||||
requestInstance.open();
|
||||
intervalID = setInterval(() => requestInstance.open(), BACKUP_INTERVAL);
|
||||
})
|
||||
}
|
||||
|
||||
function stopSlaveProcess() {
|
||||
if (masterWS !== null) {
|
||||
masterWS.onclose = () => null;
|
||||
masterWS.close();
|
||||
requestInstance.close();
|
||||
masterWS = null;
|
||||
}
|
||||
if (intervalID !== null) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
}
|
||||
|
||||
function requestBackupSync(checksum_trigger, ws) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query('SELECT MAX(u_time) as last_time FROM _backup').then(result => {
|
||||
let request = {
|
||||
floID: keys.node_id,
|
||||
pubKey: keys.node_pub,
|
||||
type: "BACKUP_SYNC",
|
||||
last_time: result[0].last_time,
|
||||
checksum: checksum_trigger,
|
||||
req_time: Date.now()
|
||||
};
|
||||
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
|
||||
ws.send(JSON.stringify(request));
|
||||
resolve(request);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const requestInstance = {
|
||||
ws: null,
|
||||
cache: null,
|
||||
checksum: null,
|
||||
delete_data: null,
|
||||
add_data: null,
|
||||
request: null,
|
||||
onetime: null,
|
||||
last_response_time: null,
|
||||
checksum_count_down: 0
|
||||
};
|
||||
|
||||
requestInstance.open = function (ws = null) {
|
||||
const self = this;
|
||||
//Check if there is an active request
|
||||
if (self.request) {
|
||||
console.log("A request is already active");
|
||||
if (self.last_response_time < Date.now() - BACKUP_SYNC_TIMEOUT)
|
||||
self.close();
|
||||
else
|
||||
return;
|
||||
}
|
||||
//Use websocket connection if passed, else use masterWS if available
|
||||
if (ws) {
|
||||
ws.on('message', processDataFromMaster);
|
||||
self.onetime = true;
|
||||
} else if (masterWS)
|
||||
ws = masterWS;
|
||||
else return console.warn("Not connected to master");
|
||||
|
||||
requestBackupSync(!self.checksum_count_down || self.onetime, ws).then(request => {
|
||||
self.request = request;
|
||||
self.cache = [];
|
||||
self.last_response_time = Date.now();
|
||||
self.ws = ws;
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
requestInstance.close = function () {
|
||||
const self = this;
|
||||
if (self.onetime)
|
||||
self.ws.close();
|
||||
else
|
||||
self.checksum_count_down = self.checksum_count_down ? self.checksum_count_down - 1 : CHECKSUM_INTERVAL;
|
||||
self.onetime = null;
|
||||
self.ws = null;
|
||||
self.cache = null;
|
||||
self.checksum = null;
|
||||
self.delete_data = null;
|
||||
self.add_data = null;
|
||||
self.request = null;
|
||||
self.last_response_time = null;
|
||||
}
|
||||
|
||||
function processDataFromMaster(message) {
|
||||
try {
|
||||
message = JSON.parse(message);
|
||||
//console.debug("Master:", message);
|
||||
if (message.command.startsWith("SYNC"))
|
||||
processBackupData(message);
|
||||
else switch (message.command) {
|
||||
case "SINK_SHARE":
|
||||
storeSinkShare(message.group, message.sinkID, message.share, message.ref);
|
||||
break;
|
||||
case "SEND_SHARE":
|
||||
sendSinkShare(message.group, message.sinkID, message.pubKey);
|
||||
break;
|
||||
case "REQUEST_ERROR":
|
||||
console.log(message.error);
|
||||
if (message.type === "BACKUP_SYNC")
|
||||
requestInstance.close();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function storeSinkShare(group, sinkID, share, ref) {
|
||||
share = floCrypto.decryptData(share, keys.node_priv);
|
||||
keys.addShare(group, sinkID, ref, share)
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function sendSinkShare(group, sinkID, pubKey) {
|
||||
keys.getShares(group, sinkID).then(({ ref, shares }) => {
|
||||
shares.forEach(s => {
|
||||
let response = {
|
||||
type: "SINK_SHARE",
|
||||
sinkID, ref,
|
||||
share: floCrypto.encryptData(s, pubKey),
|
||||
floID: keys.node_id,
|
||||
pubKey: keys.node_pub,
|
||||
req_time: Date.now()
|
||||
}
|
||||
response.sign = floCrypto.signData(response.type + "|" + response.req_time, keys.node_priv); //TODO: strengthen signature
|
||||
masterWS.send(JSON.stringify(response));
|
||||
})
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function processBackupData(response) {
|
||||
//TODO: Sync improvements needed. (2 types)
|
||||
//1. Either sync has to be completed or rollback all
|
||||
//2. Each table/data should be treated as independent chunks
|
||||
const self = requestInstance;
|
||||
self.last_response_time = Date.now();
|
||||
switch (response.command) {
|
||||
case "SYNC_END":
|
||||
if (response.status) {
|
||||
storeBackupData(self.cache, self.checksum).then(result => {
|
||||
updateBackupTable(self.add_data, self.delete_data);
|
||||
if (result) {
|
||||
console.log("Backup Sync completed successfully");
|
||||
self.close();
|
||||
} else
|
||||
console.log("Waiting for re-sync data");
|
||||
}).catch(_ => {
|
||||
console.warn("Backup Sync was not successful");
|
||||
self.close();
|
||||
});
|
||||
} else {
|
||||
console.info("Backup Sync was not successful! Failed info: ", response.info);
|
||||
self.close();
|
||||
}
|
||||
break;
|
||||
case "SYNC_DELETE":
|
||||
self.delete_data = response.delete_data;
|
||||
self.cache.push(cacheBackupData(null, response.delete_data));
|
||||
break;
|
||||
case "SYNC_HEADER":
|
||||
self.add_data = response.add_data;
|
||||
break;
|
||||
case "SYNC_UPDATE":
|
||||
self.cache.push(cacheBackupData(response.table, response.data));
|
||||
break;
|
||||
case "SYNC_CHECKSUM":
|
||||
self.checksum = response.checksum;
|
||||
break;
|
||||
case "SYNC_HASH":
|
||||
verifyHash(response.hashes)
|
||||
.then(mismatch => requestTableChunks(mismatch, self.ws))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
self.close();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const cacheBackupData = (tableName, dataCache) => new Promise((resolve, reject) => {
|
||||
DB.query("INSERT INTO _backupCache (t_name, data_cache) VALUE (?, ?)", [tableName, JSON.stringify(dataCache)])
|
||||
.then(_ => resolve(true)).catch(error => {
|
||||
console.error(error);
|
||||
reject(false);
|
||||
})
|
||||
});
|
||||
|
||||
function storeBackupData(cache_promises, checksum_ref) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.allSettled(cache_promises).then(_ => {
|
||||
console.log("START: BackupCache -> Tables");
|
||||
//Process 'Users' table 1st as it provides foreign key attribute to other tables
|
||||
DB.query("SELECT * FROM _backupCache WHERE t_name=?", ["Users"]).then(data => {
|
||||
Promise.allSettled(data.map(d => updateTableData("Users", JSON.parse(d.data_cache)))).then(result => {
|
||||
storeBackupData.commit(data, result).then(_ => {
|
||||
DB.query("SELECT * FROM _backupCache WHERE t_name IS NOT NULL").then(data => {
|
||||
Promise.allSettled(data.map(d => updateTableData(d.t_name, JSON.parse(d.data_cache)))).then(result => {
|
||||
storeBackupData.commit(data, result).then(_ => {
|
||||
DB.query("SELECT * FROM _backupCache WHERE t_name IS NULL").then(data => {
|
||||
Promise.allSettled(data.map(d => deleteTableData(JSON.parse(d.data_cache)))).then(result => {
|
||||
storeBackupData.commit(data, result).then(_ => {
|
||||
console.log("END: BackupCache -> Tables");
|
||||
if (!checksum_ref) //No checksum verification
|
||||
resolve(true);
|
||||
else
|
||||
verifyChecksum(checksum_ref)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
console.warn("ABORT: BackupCache -> Tables");
|
||||
reject(false);
|
||||
});
|
||||
})
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
console.warn("ABORT: BackupCache -> Tables");
|
||||
reject(false);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
storeBackupData.commit = function (data, result) {
|
||||
let promises = [];
|
||||
for (let i = 0; i < data.length; i++)
|
||||
switch (result[i].status) {
|
||||
case "fulfilled":
|
||||
promises.push(DB.query("DELETE FROM _backupCache WHERE id=?", data[i].id));
|
||||
break;
|
||||
case "rejected":
|
||||
console.error(result[i].reason);
|
||||
promises.push(DB.query("UPDATE _backupCache SET fail=TRUE WHERE id=?", data[i].id));
|
||||
break;
|
||||
}
|
||||
return Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
function updateBackupTable(add_data, delete_data) {
|
||||
//update _backup table for added data
|
||||
DB.transaction(add_data.map(r => [
|
||||
"INSERT INTO _backup (t_name, id, mode, u_time) VALUE (?, ?, TRUE, ?) ON DUPLICATE KEY UPDATE mode=TRUE, u_time=?",
|
||||
[r.t_name, r.id, validateValue(r.u_time), validateValue(r.u_time)]
|
||||
])).then(_ => null).catch(error => console.error(error));
|
||||
//update _backup table for deleted data
|
||||
DB.transaction(delete_data.map(r => [
|
||||
"INSERT INTO _backup (t_name, id, mode, u_time) VALUE (?, ?, NULL, ?) ON DUPLICATE KEY UPDATE mode=NULL, u_time=?",
|
||||
[r.t_name, r.id, validateValue(r.u_time), validateValue(r.u_time)]
|
||||
])).then(_ => null).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function deleteTableData(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let delete_needed = {};
|
||||
data.forEach(r => r.t_name in delete_needed ? delete_needed[r.t_name].push(r.id) : delete_needed[r.t_name] = [r.id]);
|
||||
let queries = [];
|
||||
for (let table in delete_needed)
|
||||
queries.push(["DELETE FROM ?? WHERE id IN (?)", [table, delete_needed[table]]]);
|
||||
DB.transaction(queries).then(_ => resolve(true)).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function updateTableData(table, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!data.length)
|
||||
return resolve(null);
|
||||
let cols = Object.keys(data[0]);
|
||||
let values = data.map(r => cols.map(c => validateValue(r[c])));
|
||||
let statement = "INSERT INTO ?? (??) VALUES ? ON DUPLICATE KEY UPDATE " + Array(cols.length).fill("??=VALUES(??)").join();
|
||||
let query_values = [table, cols, values];
|
||||
cols.forEach(c => query_values.push(c, c));
|
||||
DB.query(statement, query_values).then(_ => resolve(true)).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
const validateValue = val => (typeof val === "string" && /\.\d{3}Z$/.test(val)) ? new Date(val) : val;
|
||||
|
||||
function verifyChecksum(checksum_ref) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("CHECKSUM TABLE ??", [Object.keys(checksum_ref)]).then(result => {
|
||||
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
|
||||
let mismatch = [];
|
||||
for (let table in checksum)
|
||||
if (checksum[table] != checksum_ref[table])
|
||||
mismatch.push(table);
|
||||
//console.debug("Checksum-mismatch:", mismatch);
|
||||
if (!mismatch.length) //Checksum of every table is verified.
|
||||
resolve(true);
|
||||
else { //If one or more tables checksum is not correct, re-request the table data
|
||||
requestHash(mismatch);
|
||||
resolve(false);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject(false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function requestHash(tables) {
|
||||
//TODO: resync only necessary data (instead of entire table)
|
||||
let self = requestInstance;
|
||||
let request = {
|
||||
floID: keys.node_id,
|
||||
pubKey: keys.node_pub,
|
||||
type: "HASH_SYNC",
|
||||
tables: tables,
|
||||
req_time: Date.now()
|
||||
};
|
||||
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
|
||||
self.ws.send(JSON.stringify(request));
|
||||
self.request = request;
|
||||
self.checksum = null;
|
||||
self.cache = [];
|
||||
}
|
||||
|
||||
function verifyHash(hashes) {
|
||||
const convertIntArray = obj => Object.keys(obj).map(i => parseInt(i));
|
||||
const checkHash = (table, hash_ref) => new Promise((res, rej) => {
|
||||
getTableHashes(table).then(hash_cur => {
|
||||
for (let i in hash_ref)
|
||||
if (hash_ref[i] === hash_cur[i]) {
|
||||
delete hash_ref[i];
|
||||
delete hash_cur[i];
|
||||
}
|
||||
res([convertIntArray(hash_ref), convertIntArray(hash_cur)]);
|
||||
}).catch(error => rej(error))
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
let tables = Object.keys(hashes);
|
||||
Promise.allSettled(tables.map(t => checkHash(t, hashes[t]))).then(result => {
|
||||
let mismatch = {};
|
||||
for (let t in tables)
|
||||
if (result[t].status === "fulfilled") {
|
||||
mismatch[tables[t]] = result[t].value; //Data that are incorrect/missing/deleted
|
||||
//Data to be deleted (incorrect data will be added by resync)
|
||||
let id_end = result[t].value[1].map(i => i * HASH_N_ROW); //eg if i=2 AND H_R_C = 5 then id_end = 2 * 5 = 10 (ie, range 6-10)
|
||||
Promise.allSettled(id_end.map(i =>
|
||||
DB.query("DELETE FROM ?? WHERE id BETWEEN ? AND ?", [tables[t], i - HASH_N_ROW + 1, i]) //eg, i - HASH_N_ROW + 1 = 10 - 5 + 1 = 6
|
||||
)).then(_ => null);
|
||||
} else
|
||||
console.error(result[t].reason);
|
||||
//console.debug("Hash-mismatch", mismatch);
|
||||
resolve(mismatch);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function requestTableChunks(tables, ws) {
|
||||
let request = {
|
||||
floID: keys.node_id,
|
||||
pubKey: keys.node_pub,
|
||||
type: "RE_SYNC",
|
||||
tables: tables,
|
||||
req_time: Date.now()
|
||||
};
|
||||
request.sign = floCrypto.signData(request.type + "|" + request.req_time, keys.node_priv);
|
||||
ws.send(JSON.stringify(request));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get masterWS() {
|
||||
return masterWS;
|
||||
},
|
||||
start: startSlaveProcess,
|
||||
stop: stopSlaveProcess,
|
||||
syncRequest: ws => requestInstance.open(ws)
|
||||
}
|
||||
282
exchangemarket/src/backup/sync.js
Normal file
282
exchangemarket/src/backup/sync.js
Normal file
@ -0,0 +1,282 @@
|
||||
'use strict';
|
||||
|
||||
const DB = require("../database");
|
||||
|
||||
const {
|
||||
HASH_N_ROW
|
||||
} = require("../_constants")["backup"];
|
||||
|
||||
//Backup Transfer
|
||||
function sendBackupData(last_time, checksum, ws) {
|
||||
if (!last_time) last_time = 0;
|
||||
else if (typeof last_time === "string" && /\.\d{3}Z$/.test(last_time))
|
||||
last_time = last_time.substring(0, last_time.length - 1);
|
||||
let promises = [
|
||||
backupSync_data(last_time, ws),
|
||||
backupSync_delete(last_time, ws)
|
||||
];
|
||||
if (checksum)
|
||||
promises.push(backupSync_checksum(ws));
|
||||
Promise.allSettled(promises).then(result => {
|
||||
let failedSync = [];
|
||||
result.forEach(r => r.status === "rejected" ? failedSync.push(r.reason) : null);
|
||||
if (failedSync.length) {
|
||||
console.info("Backup Sync Failed:", failedSync);
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_END",
|
||||
status: false,
|
||||
info: failedSync
|
||||
}));
|
||||
} else {
|
||||
console.info("Backup Sync completed");
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_END",
|
||||
status: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function backupSync_delete(last_time, ws) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT * FROM _backup WHERE mode is NULL AND u_time > ?", [last_time]).then(result => {
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_DELETE",
|
||||
delete_data: result
|
||||
}));
|
||||
resolve("deleteSync");
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject("deleteSync");
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function backupSync_data(last_time, ws) {
|
||||
const sendTable = (table, id_list) => new Promise((res, rej) => {
|
||||
DB.query("SELECT * FROM ?? WHERE id IN (?)", [table, id_list])
|
||||
.then(data => {
|
||||
ws.send(JSON.stringify({
|
||||
table,
|
||||
command: "SYNC_UPDATE",
|
||||
data
|
||||
}));
|
||||
res(table);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
rej(table);
|
||||
});
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT * FROM _backup WHERE mode=TRUE AND u_time > ?", [last_time]).then(result => {
|
||||
let sync_needed = {};
|
||||
result.forEach(r => r.t_name in sync_needed ? sync_needed[r.t_name].push(r.id) : sync_needed[r.t_name] = [r.id]);
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_HEADER",
|
||||
add_data: result
|
||||
}));
|
||||
let promises = [];
|
||||
for (let table in sync_needed)
|
||||
promises.push(sendTable(table, sync_needed[table]));
|
||||
Promise.allSettled(promises).then(result => {
|
||||
let failedTables = [];
|
||||
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
|
||||
if (failedTables.length)
|
||||
reject(["dataSync", failedTables]);
|
||||
else
|
||||
resolve("dataSync");
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject("dataSync");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupSync_checksum(ws) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT DISTINCT t_name FROM _backup").then(result => {
|
||||
let tableList = result.map(r => r['t_name']);
|
||||
if (!tableList.length)
|
||||
return resolve("checksum");
|
||||
DB.query("CHECKSUM TABLE ??", [tableList]).then(result => {
|
||||
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_CHECKSUM",
|
||||
checksum: checksum
|
||||
}));
|
||||
resolve("checksum");
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject("checksum");
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject("checksum");
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function sendTableHash(tables, ws) {
|
||||
Promise.allSettled(tables.map(t => getTableHashes(t))).then(result => {
|
||||
let hashes = {};
|
||||
for (let i in tables)
|
||||
if (result[i].status === "fulfilled")
|
||||
hashes[tables[i]] = result[i].value;
|
||||
else
|
||||
console.error(result[i].reason);
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_HASH",
|
||||
hashes: hashes
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
function getTableHashes(table) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SHOW COLUMNS FROM ??", [table]).then(result => {
|
||||
//columns
|
||||
let columns = result.map(r => r["Field"]).sort();
|
||||
//select statement
|
||||
let statement = "SELECT CEIL(id/?) as group_id,";
|
||||
let query_values = [HASH_N_ROW];
|
||||
//aggregate column values
|
||||
let col_aggregate = columns.map(c => "IFNULL(CRC32(??), 0)").join('+');
|
||||
columns.forEach(c => query_values.push(c));
|
||||
//aggregate rows via group by
|
||||
statement += " SUM(CRC32(MD5(" + col_aggregate + "))) as hash FROM ?? GROUP BY group_id ORDER BY group_id";
|
||||
query_values.push(table);
|
||||
//query
|
||||
DB.query(statement, query_values)
|
||||
.then(result => resolve(Object.fromEntries(result.map(r => [r.group_id, r.hash]))))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function sendTableData(tables, ws) {
|
||||
let promises = [
|
||||
tableSync_data(tables, ws),
|
||||
tableSync_delete(tables, ws),
|
||||
tableSync_checksum(Object.keys(tables), ws)
|
||||
];
|
||||
Promise.allSettled(promises).then(result => {
|
||||
let failedSync = [];
|
||||
result.forEach(r => r.status === "rejected" ? failedSync.push(r.reason) : null);
|
||||
if (failedSync.length) {
|
||||
console.info("Backup Sync Failed:", failedSync);
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_END",
|
||||
status: false,
|
||||
info: failedSync
|
||||
}));
|
||||
} else {
|
||||
console.info("Backup Sync completed");
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_END",
|
||||
status: true
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tableSync_delete(tables, ws) {
|
||||
let getDelete = (table, group_id) => new Promise((res, rej) => {
|
||||
let id_end = group_id * HASH_N_ROW,
|
||||
id_start = id_end - HASH_N_ROW + 1;
|
||||
DB.query("SELECT * FROM _backup WHERE t_name=? AND mode is NULL AND (id BETWEEN ? AND ?)", [table, id_start, id_end])
|
||||
.then(result => res(result))
|
||||
.catch(error => rej(error))
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
let promises = [];
|
||||
for (let t in tables)
|
||||
for (let g_id in tables[t][1]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||
promises.push(getDelete(t, g_id));
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let delete_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
|
||||
delete_sync = [].concat(...delete_sync); //Convert 2d array into 1d
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_DELETE",
|
||||
delete_data: delete_sync
|
||||
}));
|
||||
resolve("deleteSync");
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function tableSync_data(tables, ws) {
|
||||
const sendTable = (table, group_id) => new Promise((res, rej) => {
|
||||
let id_end = group_id * HASH_N_ROW,
|
||||
id_start = id_end - HASH_N_ROW + 1;
|
||||
DB.query("SELECT * FROM ?? WHERE id BETWEEN ? AND ?", [table, id_start, id_end]).then(data => {
|
||||
ws.send(JSON.stringify({
|
||||
table,
|
||||
command: "SYNC_UPDATE",
|
||||
data
|
||||
}));
|
||||
res(table);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
rej(table);
|
||||
});
|
||||
});
|
||||
const getUpdate = (table, group_id) => new Promise((res, rej) => {
|
||||
let id_end = group_id * HASH_N_ROW,
|
||||
id_start = id_end - HASH_N_ROW + 1;
|
||||
DB.query("SELECT * FROM _backup WHERE t_name=? AND mode=TRUE AND (id BETWEEN ? AND ?)", [table, id_start, id_end])
|
||||
.then(result => res(result))
|
||||
.catch(error => rej(error))
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
let promises = [];
|
||||
for (let t in tables)
|
||||
for (let g_id of tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||
promises.push(getUpdate(t, g_id));
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let update_sync = results.filter(r => r.status === "fulfilled").map(r => r.value); //Filtered results
|
||||
update_sync = [].concat(...update_sync); //Convert 2d array into 1d
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_HEADER",
|
||||
add_data: update_sync
|
||||
}));
|
||||
let promises = [];
|
||||
for (let t in tables)
|
||||
for (let g_id of tables[t][0]) //tables[t] is [convertIntArray(hash_ref), convertIntArray(hash_cur)]
|
||||
promises.push(sendTable(t, g_id));
|
||||
Promise.allSettled(promises).then(result => {
|
||||
let failedTables = [];
|
||||
result.forEach(r => r.status === "rejected" ? failedTables.push(r.reason) : null);
|
||||
if (failedTables.length)
|
||||
reject(["dataSync", [...new Set(failedTables)]]);
|
||||
else
|
||||
resolve("dataSync");
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function tableSync_checksum(tables, ws) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("CHECKSUM TABLE ??", [tables]).then(result => {
|
||||
let checksum = Object.fromEntries(result.map(r => [r.Table.split(".").pop(), r.Checksum]));
|
||||
ws.send(JSON.stringify({
|
||||
command: "SYNC_CHECKSUM",
|
||||
checksum: checksum
|
||||
}));
|
||||
resolve("checksum");
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
reject("checksum");
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTableHashes,
|
||||
sendBackupData,
|
||||
sendTableHash,
|
||||
sendTableData
|
||||
}
|
||||
225
exchangemarket/src/blockchain.js
Normal file
225
exchangemarket/src/blockchain.js
Normal file
@ -0,0 +1,225 @@
|
||||
'use strict';
|
||||
|
||||
const pCode = require('../docs/scripts/floExchangeAPI').processCode;
|
||||
const { collectAndCall } = require('./backup/head');
|
||||
const keys = require('./keys');
|
||||
const DB = require("./database");
|
||||
|
||||
const TYPE_VAULT = "VAULT",
|
||||
TYPE_CONVERT = "CONVERT",
|
||||
TYPE_CONVERT_POOL = "CONVERT_POOL",
|
||||
TYPE_CONVERT_REFUND = "REFUND",
|
||||
TYPE_BLOCKCHAIN_BOND = "BOND",
|
||||
TYPE_BOBS_FUND = "BOB-FUND";
|
||||
|
||||
const SINK_GROUP = {
|
||||
[TYPE_VAULT]: keys.sink_groups.EXCHANGE,
|
||||
[TYPE_CONVERT]: keys.sink_groups.CONVERT,
|
||||
[TYPE_CONVERT_POOL]: keys.sink_groups.CONVERT,
|
||||
[TYPE_CONVERT_REFUND]: keys.sink_groups.CONVERT,
|
||||
[TYPE_BLOCKCHAIN_BOND]: keys.sink_groups.BLOCKCHAIN_BONDS,
|
||||
[TYPE_BOBS_FUND]: keys.sink_groups.BOBS_FUND
|
||||
}
|
||||
|
||||
const balance_locked = {},
|
||||
balance_cache = {},
|
||||
callbackCollection = {
|
||||
[TYPE_VAULT]: {},
|
||||
[TYPE_CONVERT]: {},
|
||||
[TYPE_CONVERT_POOL]: {},
|
||||
[TYPE_CONVERT_REFUND]: {},
|
||||
[TYPE_BLOCKCHAIN_BOND]: {},
|
||||
[TYPE_BOBS_FUND]: {}
|
||||
};
|
||||
|
||||
function getBalance(sinkID, asset) {
|
||||
switch (asset) {
|
||||
case "FLO":
|
||||
return floBlockchainAPI.getBalance(sinkID);
|
||||
case "BTC":
|
||||
let btc_id = btcOperator.convert.legacy2bech(sinkID);
|
||||
return btcOperator.getBalance(btc_id);
|
||||
default:
|
||||
return floTokenAPI.getBalance(sinkID, asset);
|
||||
}
|
||||
}
|
||||
|
||||
function getSinkID(type, quantity, asset, sinkList = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!sinkList)
|
||||
sinkList = keys.sink_chest.list(SINK_GROUP[type]).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]);
|
||||
if (!sinkList.length)
|
||||
return reject(`Insufficient balance for asset(${asset}) in chest(${SINK_GROUP[type]})`);
|
||||
let sinkID = sinkList.shift();
|
||||
getBalance(sinkID, asset).then(balance => {
|
||||
if (!(sinkID in balance_cache))
|
||||
balance_cache[sinkID] = {};
|
||||
balance_cache[sinkID][asset] = balance;
|
||||
if (balance > (quantity + (sinkID in balance_locked ? balance_locked[sinkID][asset] || 0 : 0)))
|
||||
return resolve(sinkID);
|
||||
else
|
||||
getSinkID(type, quantity, asset, sinkList)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
getSinkID(type, quantity, asset, sinkList)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const WITHDRAWAL_MESSAGE = {
|
||||
[TYPE_VAULT]: "(withdrawal from market)",
|
||||
[TYPE_CONVERT]: "(convert coin)",
|
||||
[TYPE_CONVERT_POOL]: "(convert fund)",
|
||||
[TYPE_CONVERT_REFUND]: "(refund from market)",
|
||||
[TYPE_BLOCKCHAIN_BOND]: "(bond closing)",
|
||||
[TYPE_BOBS_FUND]: "(fund investment closing)"
|
||||
}
|
||||
|
||||
function sendTx(floID, asset, quantity, sinkID, sinkKey, message) {
|
||||
switch (asset) {
|
||||
case "FLO":
|
||||
return floBlockchainAPI.sendTx(sinkID, floID, quantity, sinkKey, 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, { fee_from_receiver: true });
|
||||
default:
|
||||
return floTokenAPI.sendToken(sinkKey, quantity, floID, message, asset);
|
||||
}
|
||||
}
|
||||
|
||||
const updateSyntax = {
|
||||
[TYPE_VAULT]: "UPDATE VaultTransactions SET r_status=?, txid=? WHERE id=?",
|
||||
[TYPE_CONVERT]: "UPDATE DirectConvert SET r_status=?, out_txid=? WHERE id=?",
|
||||
[TYPE_CONVERT_POOL]: "UPDATE ConvertFund SET r_status=?, txid=? WHERE id=?",
|
||||
[TYPE_CONVERT_REFUND]: "UPDATE RefundConvert SET r_status=?, out_txid=? WHERE id=?",
|
||||
[TYPE_BLOCKCHAIN_BOND]: "UPDATE CloseBondTransact SET r_status=?, txid=? WHERE id=?",
|
||||
[TYPE_BOBS_FUND]: "UPDATE CloseFundTransact SET r_status=?, txid=? WHERE id=?"
|
||||
};
|
||||
|
||||
function sendAsset(floID, asset, quantity, type, id) {
|
||||
quantity = global.toStandardDecimal(quantity);
|
||||
getSinkID(type, quantity, asset).then(sinkID => {
|
||||
let callback = (sinkKey) => {
|
||||
//Send asset to user via API
|
||||
sendTx(floID, asset, quantity, sinkID, sinkKey, WITHDRAWAL_MESSAGE[type]).then(txid => {
|
||||
if (!txid)
|
||||
console.error("Transaction not successful");
|
||||
else //Transaction was successful, Add in database
|
||||
DB.query(updateSyntax[type], [pCode.STATUS_CONFIRMATION, txid, id])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
}).catch(error => console.error(error)).finally(_ => {
|
||||
delete callbackCollection[type][id];
|
||||
balance_locked[sinkID][asset] -= quantity;
|
||||
});
|
||||
}
|
||||
collectAndCall(SINK_GROUP[type], sinkID, callback); //TODO: add timeout to prevent infinite wait
|
||||
callbackCollection[type][id] = callback;
|
||||
if (!(sinkID in balance_locked))
|
||||
balance_locked[sinkID] = {};
|
||||
balance_locked[sinkID][asset] = (balance_locked[sinkID][asset] || 0) + quantity;
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function withdrawAsset_init(floID, asset, amount) {
|
||||
amount = global.toStandardDecimal(amount);
|
||||
let asset_type = ["FLO", "BTC"].includes(asset) ? pCode.ASSET_TYPE_COIN : pCode.ASSET_TYPE_TOKEN;
|
||||
DB.query("INSERT INTO VaultTransactions (floID, mode, asset_type, asset, amount, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_WITHDRAW, asset_type, asset, amount, pCode.STATUS_PENDING]])
|
||||
.then(result => sendAsset(floID, asset, amount, TYPE_VAULT, result.insertId))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function withdrawAsset_retry(floID, asset, amount, id) {
|
||||
if (id in callbackCollection[TYPE_VAULT])
|
||||
console.debug("A callback is already pending for this Coin transfer");
|
||||
else sendAsset(floID, asset, amount, TYPE_VAULT, id);
|
||||
}
|
||||
|
||||
function convertToCoin_init(floID, coin, currency_amount, rate, id) {
|
||||
let coin_quantity = global.toStandardDecimal(currency_amount / rate);
|
||||
DB.query("UPDATE DirectConvert SET quantity=?, r_status=?, rate=?, locktime=DEFAULT WHERE id=?", [coin_quantity, pCode.STATUS_PROCESSING, rate, 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, coin_quantity, rate, id) {
|
||||
let currency_amount = global.toStandardDecimal(coin_quantity * rate);
|
||||
DB.query("UPDATE DirectConvert SET amount=?, r_status=?, rate=?, locktime=DEFAULT WHERE id=?", [currency_amount, pCode.STATUS_PROCESSING, rate, id])
|
||||
.then(result => sendAsset(floID, floGlobals.currency, currency_amount, TYPE_CONVERT, id))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function convertFromCoin_retry(floID, currency_amount, id) {
|
||||
if (id in callbackCollection[TYPE_CONVERT])
|
||||
console.debug("A callback is already pending for this Coin Convert");
|
||||
else sendAsset(floID, floGlobals.currency, currency_amount, TYPE_CONVERT, id);
|
||||
}
|
||||
|
||||
function convertFundWithdraw_retry(asset, amount, id) {
|
||||
if (id in callbackCollection[TYPE_CONVERT_POOL])
|
||||
console.debug("A callback is already pending for this Convert fund withdrawal");
|
||||
else sendAsset(floGlobals.adminID, asset, amount, TYPE_CONVERT_POOL, id);
|
||||
}
|
||||
|
||||
function bondTransact_retry(floID, amount, btc_rate, usd_rate, id) {
|
||||
if (id in callbackCollection[TYPE_BLOCKCHAIN_BOND])
|
||||
console.debug("A callback is already pending for this Bond closing");
|
||||
else sendAsset(floID, "BTC", amount / (btc_rate * usd_rate), TYPE_BLOCKCHAIN_BOND, id);
|
||||
}
|
||||
function fundTransact_retry(floID, amount, btc_rate, usd_rate, id) {
|
||||
if (id in callbackCollection[TYPE_BOBS_FUND])
|
||||
console.debug("A callback is already pending for this Fund investment closing");
|
||||
else sendAsset(floID, "BTC", amount / (btc_rate * usd_rate), TYPE_BOBS_FUND, id);
|
||||
}
|
||||
|
||||
function refundConvert_init(floID, asset, amount, id) {
|
||||
amount = global.toStandardDecimal(amount);
|
||||
DB.query("UPDATE RefundConvert SET amount=?, r_status=?, locktime=DEFAULT WHERE id=?", [amount, pCode.STATUS_PROCESSING, id])
|
||||
.then(result => sendAsset(floID, asset, amount, TYPE_CONVERT_REFUND, id))
|
||||
.catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function refundConvert_retry(floID, asset, amount, id) {
|
||||
if (id in callbackCollection[TYPE_CONVERT_REFUND])
|
||||
console.debug("A callback is already pending for this Refund");
|
||||
else sendAsset(floID, asset, amount, TYPE_CONVERT_REFUND, id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withdrawAsset: {
|
||||
init: withdrawAsset_init,
|
||||
retry: withdrawAsset_retry
|
||||
},
|
||||
convertToCoin: {
|
||||
init: convertToCoin_init,
|
||||
retry: convertToCoin_retry
|
||||
},
|
||||
convertFromCoin: {
|
||||
init: convertFromCoin_init,
|
||||
retry: convertFromCoin_retry
|
||||
},
|
||||
convertFundWithdraw: {
|
||||
retry: convertFundWithdraw_retry
|
||||
},
|
||||
bondTransact: {
|
||||
retry: bondTransact_retry
|
||||
},
|
||||
fundTransact: {
|
||||
retry: fundTransact_retry
|
||||
},
|
||||
refundConvert: {
|
||||
init: refundConvert_init,
|
||||
retry: refundConvert_retry
|
||||
}
|
||||
}
|
||||
252
exchangemarket/src/coupling.js
Normal file
252
exchangemarket/src/coupling.js
Normal file
@ -0,0 +1,252 @@
|
||||
'use strict';
|
||||
|
||||
const price = require("./price");
|
||||
const DB = require("./database");
|
||||
|
||||
const {
|
||||
WAIT_TIME,
|
||||
TRADE_HASH_PREFIX
|
||||
} = require("./_constants")["market"];
|
||||
|
||||
const updateBalance = {};
|
||||
updateBalance.consume = (floID, token, amount) => ["UPDATE UserBalance SET quantity=quantity-? WHERE floID=? AND token=?", [amount, floID, token]];
|
||||
updateBalance.add = (floID, token, amount) => ["INSERT INTO UserBalance (floID, token, quantity) VALUE (?) ON DUPLICATE KEY UPDATE quantity=quantity+?", [[floID, token, amount], amount]];
|
||||
|
||||
const couplingInstance = {},
|
||||
couplingTimeout = {};
|
||||
|
||||
function stopAllInstance() {
|
||||
for (let asset in couplingTimeout) {
|
||||
if (couplingTimeout[asset])
|
||||
clearTimeout(couplingTimeout[asset]);
|
||||
delete couplingInstance[asset];
|
||||
delete couplingTimeout[asset];
|
||||
}
|
||||
}
|
||||
|
||||
function startCouplingForAsset(asset, updatePrice = false) {
|
||||
if (couplingInstance[asset] === true) { //if coupling is already running for asset
|
||||
if (updatePrice) { //wait until current instance is over
|
||||
if (couplingTimeout[asset]) clearTimeout(couplingTimeout[asset]);
|
||||
couplingTimeout[asset] = setTimeout(() => startCouplingForAsset(asset, true), WAIT_TIME);
|
||||
}
|
||||
return;
|
||||
}
|
||||
price.getRates(asset, updatePrice).then(cur_rate => {
|
||||
cur_rate = global.toStandardDecimal(cur_rate);
|
||||
couplingInstance[asset] = true; //set instance as running
|
||||
recursiveCoupling(asset, cur_rate, updatePrice);
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function getBestPair(asset, cur_rate) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Promise.allSettled([getBestBuyer(asset, cur_rate), getBestSeller(asset, cur_rate)]).then(results => {
|
||||
if (results[0].status === "fulfilled" && results[1].status === "fulfilled")
|
||||
resolve({
|
||||
buy: results[0].value,
|
||||
sell: results[1].value,
|
||||
})
|
||||
else
|
||||
reject({
|
||||
buy: results[0].reason,
|
||||
sell: results[1].reason
|
||||
})
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const getBestSeller = (asset, cur_rate) => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT SellOrder.id, SellOrder.floID, SellOrder.quantity, SellChips.id AS chip_id, SellChips.quantity AS chip_quantity FROM SellOrder" +
|
||||
" INNER JOIN UserBalance ON UserBalance.floID = SellOrder.floID AND UserBalance.token = SellOrder.asset" +
|
||||
" INNER JOIN SellChips ON SellChips.floID = SellOrder.floID AND SellChips.asset = SellOrder.asset AND SellChips.base <= ?" +
|
||||
" LEFT JOIN UserTag ON UserTag.floID = SellOrder.floID" +
|
||||
" LEFT JOIN TagList ON TagList.tag = UserTag.tag" +
|
||||
" WHERE UserBalance.quantity >= SellOrder.quantity AND SellOrder.asset = ? AND SellOrder.minPrice <= ?" +
|
||||
" ORDER BY TagList.sellPriority DESC, SellChips.locktime ASC, SellOrder.time_placed ASC" +
|
||||
" LIMIT 1", [cur_rate, asset, cur_rate]
|
||||
).then(result => {
|
||||
if (result.length)
|
||||
resolve(result[0]);
|
||||
else
|
||||
reject(null);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
const getBestBuyer = (asset, cur_rate) => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT BuyOrder.id, BuyOrder.floID, BuyOrder.quantity FROM BuyOrder" +
|
||||
" INNER JOIN UserBalance ON UserBalance.floID = BuyOrder.floID AND UserBalance.token = ?" +
|
||||
" LEFT JOIN UserTag ON UserTag.floID = BuyOrder.floID" +
|
||||
" LEFT JOIN TagList ON TagList.tag = UserTag.tag" +
|
||||
" WHERE UserBalance.quantity >= BuyOrder.maxPrice * BuyOrder.quantity AND BuyOrder.asset = ? AND BuyOrder.maxPrice >= ?" +
|
||||
" ORDER BY TagList.buyPriority DESC, BuyOrder.time_placed ASC" +
|
||||
" LIMIT 1", [floGlobals.currency, asset, cur_rate]
|
||||
).then(result => {
|
||||
if (result.length)
|
||||
resolve(result[0]);
|
||||
else
|
||||
reject(null);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
function recursiveCoupling(asset, cur_rate, flag = false) {
|
||||
processCoupling(asset, cur_rate).then(result => {
|
||||
console.log(result);
|
||||
if (couplingInstance[asset] === true)
|
||||
recursiveCoupling(asset, cur_rate, true);
|
||||
}).catch(error => {
|
||||
//noBuy = error[0], noSell = error[1], reason = error[2]
|
||||
price.noOrder(asset, error[0], error[1]);
|
||||
error[3] ? console.debug(error[2]) : console.error(error[2]);
|
||||
//set timeout for next coupling (if not order placement occurs)
|
||||
if (flag) {
|
||||
price.updateLastTime(asset);
|
||||
if (couplingInstance[asset] === true && flag) {
|
||||
//if price was updated and/or trade happened, reset timer
|
||||
if (couplingTimeout[asset]) clearTimeout(couplingTimeout[asset]);
|
||||
couplingTimeout[asset] = setTimeout(() => startCouplingForAsset(asset, true), price.MIN_TIME);
|
||||
}
|
||||
}
|
||||
delete couplingInstance[asset];
|
||||
})
|
||||
}
|
||||
|
||||
function processCoupling(asset, cur_rate) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getBestPair(asset, cur_rate).then(best => {
|
||||
//console.debug("Sell:", best.sell);
|
||||
//console.debug("Buy:", best.buy);
|
||||
let quantity = Math.min(best.buy.quantity, best.sell.quantity, best.sell.chip_quantity);
|
||||
let txQueries = processOrders(best.sell, best.buy, asset, cur_rate, quantity);
|
||||
//begin audit
|
||||
beginAudit(best.sell.floID, best.buy.floID, asset, cur_rate, quantity).then(audit => {
|
||||
//process txn query in SQL
|
||||
DB.transaction(txQueries).then(_ => {
|
||||
audit.end();
|
||||
resolve(`Transaction was successful! BuyOrder:${best.buy.id}| SellOrder:${best.sell.id}`)
|
||||
}).catch(error => reject([null, null, error]));
|
||||
}).catch(error => reject([null, null, error]));
|
||||
}).catch(error => {
|
||||
let noBuy, noSell;
|
||||
if (error.buy === undefined)
|
||||
noBuy = false;
|
||||
else if (error.buy === null)
|
||||
noBuy = true;
|
||||
else {
|
||||
console.error(error.buy);
|
||||
noBuy = null;
|
||||
}
|
||||
if (error.sell === undefined)
|
||||
noSell = false;
|
||||
else if (error.sell === null)
|
||||
noSell = true;
|
||||
else {
|
||||
console.error(error.sell);
|
||||
noSell = null;
|
||||
}
|
||||
reject([noBuy, noSell, `No valid ${noSell ? 'sellOrders' : ''} | ${noBuy ? 'buyOrders' : ''} for Asset: ${asset}`, true]);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function processOrders(seller_best, buyer_best, asset, cur_rate, quantity) {
|
||||
let txQueries = [];
|
||||
if (quantity > buyer_best.quantity || quantity > seller_best.quantity)
|
||||
throw Error("Tx quantity cannot be more than order quantity");
|
||||
|
||||
//Process Buy Order
|
||||
if (quantity == buyer_best.quantity)
|
||||
txQueries.push(["DELETE FROM BuyOrder WHERE id=?", [buyer_best.id]]);
|
||||
else
|
||||
txQueries.push(["UPDATE BuyOrder SET quantity=quantity-? WHERE id=?", [quantity, buyer_best.id]]);
|
||||
|
||||
//Process Sell Order
|
||||
if (quantity == seller_best.quantity)
|
||||
txQueries.push(["DELETE FROM SellOrder WHERE id=?", [seller_best.id]]);
|
||||
else
|
||||
txQueries.push(["UPDATE SellOrder SET quantity=quantity-? WHERE id=?", [quantity, seller_best.id]]);
|
||||
|
||||
//Process Sell Chip
|
||||
if (quantity == seller_best.chip_quantity)
|
||||
txQueries.push(["DELETE FROM SellChips WHERE id=?", [seller_best.chip_id]]);
|
||||
else
|
||||
txQueries.push(["UPDATE SellChips SET quantity=quantity-? WHERE id=?", [quantity, seller_best.chip_id]]);
|
||||
|
||||
//Update cash/asset balance for seller and buyer
|
||||
let totalAmount = cur_rate * quantity;
|
||||
txQueries.push(updateBalance.add(seller_best.floID, floGlobals.currency, totalAmount));
|
||||
txQueries.push(updateBalance.consume(buyer_best.floID, floGlobals.currency, totalAmount));
|
||||
txQueries.push(updateBalance.consume(seller_best.floID, asset, quantity));
|
||||
txQueries.push(updateBalance.add(buyer_best.floID, asset, quantity));
|
||||
|
||||
//Add SellChips to Buyer
|
||||
txQueries.push(["INSERT INTO SellChips(floID, asset, base, quantity) VALUES (?)", [[buyer_best.floID, asset, cur_rate, quantity]]])
|
||||
|
||||
//Record transaction
|
||||
let time = Date.now();
|
||||
let hash = TRADE_HASH_PREFIX + Crypto.SHA256(JSON.stringify({
|
||||
seller: seller_best.floID,
|
||||
buyer: buyer_best.floID,
|
||||
asset: asset,
|
||||
quantity: quantity,
|
||||
unitValue: cur_rate,
|
||||
tx_time: time,
|
||||
}));
|
||||
txQueries.push([
|
||||
"INSERT INTO TradeTransactions (seller, buyer, asset, quantity, unitValue, tx_time, txid) VALUES (?)",
|
||||
[[seller_best.floID, buyer_best.floID, asset, quantity, cur_rate, new Date(time), hash]]
|
||||
]);
|
||||
|
||||
return txQueries;
|
||||
}
|
||||
|
||||
function beginAudit(sellerID, buyerID, asset, unit_price, quantity) {
|
||||
return new Promise((resolve, reject) => {
|
||||
auditBalance(sellerID, buyerID, asset).then(old_bal => resolve({
|
||||
end: () => endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity)
|
||||
})).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function endAudit(sellerID, buyerID, asset, old_bal, unit_price, quantity) {
|
||||
auditBalance(sellerID, buyerID, asset).then(new_bal => {
|
||||
DB.query("INSERT INTO AuditTrade (asset, quantity, unit_price, total_cost," +
|
||||
" sellerID, seller_old_cash, seller_old_asset, seller_new_cash, seller_new_asset," +
|
||||
" buyerID, buyer_old_cash, buyer_old_asset, buyer_new_cash, buyer_new_asset)" +
|
||||
" Value (?)", [[
|
||||
asset, quantity, unit_price, quantity * unit_price,
|
||||
sellerID, old_bal[sellerID].cash, old_bal[sellerID].asset, new_bal[sellerID].cash, new_bal[sellerID].asset,
|
||||
buyerID, old_bal[buyerID].cash, old_bal[buyerID].asset, new_bal[buyerID].cash, new_bal[buyerID].asset,
|
||||
]]).then(_ => null).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function auditBalance(sellerID, buyerID, asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let balance = {
|
||||
[sellerID]: {
|
||||
cash: 0,
|
||||
asset: 0
|
||||
},
|
||||
[buyerID]: {
|
||||
cash: 0,
|
||||
asset: 0
|
||||
}
|
||||
};
|
||||
DB.query("SELECT floID, quantity, token FROM UserBalance WHERE floID IN (?) AND token IN (?)", [[sellerID, buyerID], [floGlobals.currency, asset]]).then(result => {
|
||||
for (let i in result) {
|
||||
if (result[i].token === floGlobals.currency)
|
||||
balance[result[i].floID].cash = result[i].quantity;
|
||||
else if (result[i].token === asset)
|
||||
balance[result[i].floID].asset = result[i].quantity;
|
||||
}
|
||||
resolve(balance);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initiate: startCouplingForAsset,
|
||||
stopAll: stopAllInstance,
|
||||
updateBalance
|
||||
}
|
||||
104
exchangemarket/src/database.js
Normal file
104
exchangemarket/src/database.js
Normal file
@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
var mysql = require('mysql');
|
||||
|
||||
var pool;//container for connected pool;
|
||||
|
||||
function connectToDatabase(user, password, dbname, host = 'localhost') {
|
||||
return new Promise((resolve, reject) => {
|
||||
pool = mysql.createPool({
|
||||
host: host,
|
||||
user: user,
|
||||
password: password,
|
||||
database: dbname,
|
||||
//dateStrings : true,
|
||||
//timezone: 'UTC'
|
||||
});
|
||||
getConnection().then(conn => {
|
||||
conn.release();
|
||||
resolve(pool);
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
function getConnection() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!pool)
|
||||
return reject("Database not connected");
|
||||
pool.getConnection((error, conn) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
else
|
||||
resolve(conn);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function SQL_query(sql, values) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getConnection().then(conn => {
|
||||
const fn = (err, res) => {
|
||||
conn.release();
|
||||
(err ? reject(err) : resolve(res));
|
||||
};
|
||||
if (values)
|
||||
conn.query(sql, values, fn);
|
||||
else
|
||||
conn.query(sql, fn);
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function SQL_transaction(queries) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getConnection().then(conn => {
|
||||
conn.beginTransaction(err => {
|
||||
if (err)
|
||||
conn.rollback(() => {
|
||||
conn.release();
|
||||
reject(err);
|
||||
});
|
||||
else {
|
||||
(function queryFn(result) {
|
||||
if (!queries.length) {
|
||||
conn.commit(err => {
|
||||
if (err)
|
||||
conn.rollback(() => {
|
||||
conn.release();
|
||||
reject(err);
|
||||
});
|
||||
else {
|
||||
conn.release();
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let q_i = queries.shift();
|
||||
const callback = function (err, res) {
|
||||
if (err)
|
||||
conn.rollback(() => {
|
||||
conn.release();
|
||||
reject(err);
|
||||
});
|
||||
else {
|
||||
result.push(res);
|
||||
queryFn(result);
|
||||
}
|
||||
};
|
||||
if (!Array.isArray(q_i))
|
||||
q_i = [q_i];
|
||||
if (q_i[1])
|
||||
conn.query(q_i[0], q_i[1], callback);
|
||||
else
|
||||
conn.query(q_i[0], callback);
|
||||
}
|
||||
})([]);
|
||||
}
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
module.exports = {
|
||||
connect: connectToDatabase,
|
||||
query: SQL_query,
|
||||
transaction: SQL_transaction
|
||||
};
|
||||
445
exchangemarket/src/keys.js
Normal file
445
exchangemarket/src/keys.js
Normal file
@ -0,0 +1,445 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const lockfile = require('proper-lockfile');
|
||||
const DB = require("./database");
|
||||
|
||||
var _I = ""; //Instance support
|
||||
for (let arg of process.argv)
|
||||
if (/^-I=/.test(arg)) {
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
break;
|
||||
}
|
||||
|
||||
const {
|
||||
SHARES_PER_NODE,
|
||||
SHARE_THRESHOLD,
|
||||
SHUFFLE_INTERVAL
|
||||
} = require("./_constants")["keys"];
|
||||
|
||||
const PRIV_EKEY_MIN = 32,
|
||||
PRIV_EKEY_MAX = 48,
|
||||
PRIME_FILE_TYPE = 'binary',
|
||||
INDEX_FILE_TYPE = 'utf-8',
|
||||
UNSIGNED_INT_MIN = 0,
|
||||
UNSIGNED_INT_MAX = 4294967295,
|
||||
INDEX_FILE_NAME_LENGTH = 16,
|
||||
INDEX_FILE_EXT = '.txt',
|
||||
MIN_DUMMY_FILES = 16,
|
||||
MAX_DUMMY_FILES = 24,
|
||||
MIN_DUMMY_SIZE_MUL = 0.5,
|
||||
MAX_DUMMY_SIZE_MUL = 1.5,
|
||||
SIZE_FACTOR = 100,
|
||||
LOCK_RETRY_MIN_TIME = 1 * 1000,
|
||||
LOCK_RETRY_MAX_TIME = 2 * 1000;
|
||||
|
||||
var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
|
||||
const _x = {
|
||||
get node_priv() {
|
||||
if (!node_priv || !e_key)
|
||||
throw Error("keys not set");
|
||||
return Crypto.AES.decrypt(node_priv, e_key);
|
||||
},
|
||||
set node_priv(key) {
|
||||
node_pub = floCrypto.getPubKeyHex(key);
|
||||
node_id = floCrypto.getFloID(node_pub);
|
||||
if (!key || !node_pub || !node_id)
|
||||
throw Error("Invalid Keys");
|
||||
let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
|
||||
e_key = floCrypto.randString(n);
|
||||
node_priv = Crypto.AES.encrypt(key, e_key);
|
||||
},
|
||||
args_dir: path.resolve(__dirname, '..', 'args'),
|
||||
get index_dir() {
|
||||
return path.join(this.args_dir, `indexes${_I}`)
|
||||
},
|
||||
get prime_file() {
|
||||
return path.join(this.args_dir, `prime_index${_I}.b`)
|
||||
},
|
||||
get index_file() {
|
||||
try {
|
||||
let data = fs.readFileSync(this.prime_file, PRIME_FILE_TYPE),
|
||||
fname = Crypto.AES.decrypt(data, this.node_priv);
|
||||
return path.join(this.index_dir, fname + INDEX_FILE_EXT);
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
throw Error("Prime-Index Missing/Corrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(_x.prime_file, PRIME_FILE_TYPE, (err, res) => {
|
||||
var data, cur_filename, new_filename, priv_key;
|
||||
try {
|
||||
priv_key = _x.node_priv;
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
if (!err) {
|
||||
if (res.length) { //prime file not empty
|
||||
try {
|
||||
cur_filename = Crypto.AES.decrypt(res, priv_key);
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Prime file corrupted");
|
||||
} try { //read data from index file
|
||||
let tmp = fs.readFileSync(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), INDEX_FILE_TYPE);
|
||||
tmp = Crypto.AES.decrypt(tmp, priv_key);
|
||||
JSON.parse(tmp); //check if data is JSON parse-able
|
||||
data = tmp;
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Index file corrupted");
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(_x.index_dir)) {
|
||||
fs.mkdirSync(_x.index_dir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Index directory creation failed");
|
||||
}
|
||||
try { //delete all old dummy files
|
||||
let files = fs.readdirSync(_x.index_dir);
|
||||
for (const file of files)
|
||||
if (!cur_filename || file !== cur_filename + INDEX_FILE_EXT) //check if file is current file
|
||||
fs.unlinkSync(path.join(_x.index_dir, file));
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Clear index directory failed");
|
||||
} try { //create files (dummy and new index file)
|
||||
let N = floCrypto.randInt(MIN_DUMMY_FILES, MAX_DUMMY_FILES),
|
||||
k = floCrypto.randInt(0, N);
|
||||
if (typeof data === 'undefined' || data.length == 0) //no existing data, initialize
|
||||
data = JSON.stringify({});
|
||||
let data_size = data.length;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
|
||||
if (i == k) {
|
||||
new_filename = f_name;
|
||||
f_data = data;
|
||||
} else {
|
||||
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
|
||||
f_data = floCrypto.randString(d_size, false);
|
||||
}
|
||||
f_data = Crypto.AES.encrypt(f_data, priv_key);
|
||||
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Index file creation failed");
|
||||
} try { //update prime file
|
||||
let en_filename = Crypto.AES.encrypt(new_filename, priv_key);
|
||||
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
return reject("Update prime file failed");
|
||||
}
|
||||
if (cur_filename)
|
||||
fs.unlink(path.join(_x.index_dir, cur_filename + INDEX_FILE_EXT), err => err ? console.debug(err) : null);
|
||||
shuffle.interval = setInterval(shuffle, SHUFFLE_INTERVAL);
|
||||
resolve("Key management initiated");
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function shuffle() {
|
||||
readIndexFile().then(data => {
|
||||
let new_filename, cur_filename = Crypto.AES.decrypt(fs.readFileSync(_x.prime_file, PRIME_FILE_TYPE), _x.node_priv);
|
||||
fs.readdir(_x.index_dir, (err, files) => {
|
||||
if (err)
|
||||
return console.error(err);
|
||||
data = JSON.stringify(data);
|
||||
let data_size = data.length;
|
||||
for (let file of files) {
|
||||
let f_data, f_name = floCrypto.randString(INDEX_FILE_NAME_LENGTH);
|
||||
if (file === cur_filename + INDEX_FILE_EXT) {
|
||||
new_filename = f_name;
|
||||
f_data = data;
|
||||
} else {
|
||||
let d_size = data_size * (floCrypto.randInt(MIN_DUMMY_SIZE_MUL * SIZE_FACTOR, MAX_DUMMY_SIZE_MUL * SIZE_FACTOR) / SIZE_FACTOR);
|
||||
f_data = floCrypto.randString(d_size, false);
|
||||
}
|
||||
f_data = Crypto.AES.encrypt(f_data, _x.node_priv);
|
||||
//rename and rewrite the file
|
||||
try {
|
||||
fs.renameSync(path.join(_x.index_dir, file), path.join(_x.index_dir, f_name + INDEX_FILE_EXT));
|
||||
fs.writeFileSync(path.join(_x.index_dir, f_name + INDEX_FILE_EXT), f_data, INDEX_FILE_TYPE);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
//update prime file
|
||||
if (!new_filename)
|
||||
return console.error("Index file has not been renamed");
|
||||
let en_filename = Crypto.AES.encrypt(new_filename, _x.node_priv);
|
||||
try {
|
||||
fs.writeFileSync(_x.prime_file, en_filename, PRIME_FILE_TYPE);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})
|
||||
}).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
function readIndexFile() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(_x.index_file, INDEX_FILE_TYPE, (err, data) => {
|
||||
if (err) {
|
||||
console.debug(err);
|
||||
return reject('Unable to read Index file');
|
||||
}
|
||||
try {
|
||||
data = JSON.parse(Crypto.AES.decrypt(data, _x.node_priv));
|
||||
resolve(data);
|
||||
} catch {
|
||||
reject("Index file corrupted");
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function writeIndexFile(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let en_data = Crypto.AES.encrypt(JSON.stringify(data), _x.node_priv);
|
||||
fs.writeFile(_x.index_file, en_data, INDEX_FILE_TYPE, (err) => {
|
||||
if (err) {
|
||||
console.debug(err);
|
||||
return reject('Unable to write Index file');
|
||||
} else resolve("Updated Index file");
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getShares(group, id, ignoreDiscarded = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
checkIfDiscarded(id).then(result => {
|
||||
if (ignoreDiscarded && result != false)
|
||||
return reject("Trying to get share for discarded ID");
|
||||
readIndexFile().then(data => {
|
||||
if (!(group in data))
|
||||
reject("Group not found in Index file");
|
||||
else if (!(id in data[group]))
|
||||
reject("ID not found in Index file");
|
||||
else {
|
||||
let ref = data[group][id].shift();
|
||||
DB.query("SELECT share FROM sinkShares WHERE num IN (?)", [data[group][id]])
|
||||
.then(result => resolve({ ref, shares: result.map(r => Crypto.AES.decrypt(r.share, _x.node_priv)) }))
|
||||
.catch(error => reject(error))
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function storeShareAtRandom(share) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let rand = floCrypto.randInt(UNSIGNED_INT_MIN, UNSIGNED_INT_MAX);
|
||||
DB.query("INSERT INTO sinkShares(num, share) VALUE (?)", [[rand, share]])
|
||||
.then(result => resolve(result.insertId)).catch(error => {
|
||||
if (error.code === "ER_DUP_ENTRY")
|
||||
storeShareAtRandom(share) //try again (with diff rand_num)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
else
|
||||
reject(error);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addShare(group, id, ref, share) {
|
||||
return new Promise((resolve, reject) => {
|
||||
checkIfDiscarded(id).then(result => {
|
||||
if (result != false)
|
||||
return reject("Trying to store share for discarded ID");
|
||||
lockfile.lock(_x.index_file, { retries: { forever: true, minTimeout: LOCK_RETRY_MIN_TIME, maxTimeout: LOCK_RETRY_MAX_TIME } }).then(release => {
|
||||
const releaseAndReject = err => {
|
||||
release().then(_ => null).catch(error => console.error(error));
|
||||
reject(err);
|
||||
}
|
||||
readIndexFile().then(data => {
|
||||
if (!(group in data))
|
||||
data[group] = {};
|
||||
if (!(id in data[group]))
|
||||
data[group][id] = [ref];
|
||||
else if (ref < data[group][id][0])
|
||||
return reject("reference is lower than current");
|
||||
else if (ref > data[group][id][0]) {
|
||||
let old_shares = data[group][id];
|
||||
data[group][id] = [ref];
|
||||
old_shares.shift();
|
||||
DB.query("DELETE FROM sinkShares WHERE num in (?)", [old_shares])//delete old shares
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
}
|
||||
let encrypted_share = Crypto.AES.encrypt(share, _x.node_priv);
|
||||
console.debug(ref, '|sinkID:', id, '|EnShare:', encrypted_share);
|
||||
storeShareAtRandom(encrypted_share).then(i => {
|
||||
data[group][id].push(i);
|
||||
writeIndexFile(data).then(_ => resolve(i)).catch(error => reject(error))
|
||||
.finally(_ => release().then(_ => null).catch(error => console.error(error)));
|
||||
}).catch(error => releaseAndReject(error))
|
||||
}).catch(error => releaseAndReject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function generateShares(sinkKey, total_n, min_n) {
|
||||
let shares = floCrypto.createShamirsSecretShares(sinkKey, total_n * SHARES_PER_NODE, min_n * SHARES_PER_NODE * SHARE_THRESHOLD);
|
||||
let node_shares = Array(total_n);
|
||||
for (let i = 0; i < total_n; i++)
|
||||
node_shares[i] = shares.splice(0, SHARES_PER_NODE);
|
||||
return node_shares;
|
||||
}
|
||||
|
||||
function getStoredList(group = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
readIndexFile().then(data => {
|
||||
if (group !== null) {
|
||||
if (group in data)
|
||||
resolve(Object.keys(data.group));
|
||||
else
|
||||
reject("Group not found in Index file");
|
||||
} else {
|
||||
let ids = {};
|
||||
for (let group in data)
|
||||
ids[group] = Object.keys(data[group]);
|
||||
resolve(ids);
|
||||
}
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function getDiscardedList() {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT floID, discard_time FROM discardedSinks")
|
||||
.then(result => resolve(Object.fromEntries(result.map(r => [r.floID, r.discard_time]))))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkIfDiscarded(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT discard_time FROM discardedSinks WHERE floID=?", [id])
|
||||
.then(result => resolve(result.length ? result[0].discard_time : false))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function discardSink(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("INSERT INTO discardedSinks(floID) VALUE (?)", [id])
|
||||
.then(result => resolve(`Discarded ${id}`))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
//Sink groups and chest
|
||||
const sink_groups = {
|
||||
get EXCHANGE() { return "exchange" },
|
||||
get CONVERT() { return "convert" },
|
||||
get BLOCKCHAIN_BONDS() { return "blockchain_bonds" },
|
||||
get BOBS_FUND() { return "bobs_fund" },
|
||||
get list() { //total list
|
||||
return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
|
||||
},
|
||||
get initial_list() { //list to generate when starting exchange
|
||||
return [this.EXCHANGE, this.CONVERT]
|
||||
},
|
||||
get generate_list() { //list allowed to generate
|
||||
return [this.EXCHANGE, this.CONVERT, this.BLOCKCHAIN_BONDS, this.BOBS_FUND]
|
||||
}
|
||||
};
|
||||
|
||||
const sink_ids = {}, sink_chest = {
|
||||
reset() {
|
||||
for (let i in sink_ids)
|
||||
delete sink_ids[i];
|
||||
},
|
||||
set_id(group, id, value) {
|
||||
if (!(group in sink_ids))
|
||||
sink_ids[group] = {};
|
||||
sink_ids[group][id] = value;
|
||||
},
|
||||
rm_id(group, id) {
|
||||
return delete sink_ids[group][id];
|
||||
},
|
||||
get_id(group, id) {
|
||||
return sink_ids[group][id];
|
||||
},
|
||||
list(group) {
|
||||
return Object.keys(sink_ids[group] || {});
|
||||
},
|
||||
active_list(group) {
|
||||
let ids = [];
|
||||
if (group in sink_ids)
|
||||
for (let id in sink_ids[group])
|
||||
if (sink_ids[group][id])
|
||||
ids.push(id);
|
||||
return ids;
|
||||
},
|
||||
includes(group, id) {
|
||||
return group in sink_ids ? (id in sink_ids[group]) : null;
|
||||
},
|
||||
isActive(group, id) {
|
||||
return group in sink_ids ? (id in sink_ids && sink_ids[id]) : null;
|
||||
},
|
||||
pick(group) {
|
||||
let ids = this.list(group),
|
||||
i = floCrypto.randInt(0, ids.length - 1);
|
||||
return ids[i];
|
||||
},
|
||||
active_pick(group) {
|
||||
let ids = this.active_list(group),
|
||||
i = floCrypto.randInt(0, ids.length - 1);
|
||||
return ids[i];
|
||||
},
|
||||
find_group(id) {
|
||||
let group = null;
|
||||
for (let g in sink_ids)
|
||||
if (id in sink_ids[g]) {
|
||||
group = g; break;
|
||||
}
|
||||
return group;
|
||||
},
|
||||
get_all() {
|
||||
let ids = {};
|
||||
for (let g in sink_ids)
|
||||
ids[g] = Object.keys(sink_ids[g])
|
||||
return ids;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
init: initialize,
|
||||
getShares,
|
||||
addShare,
|
||||
generateShares,
|
||||
getStoredList,
|
||||
getDiscardedList,
|
||||
checkIfDiscarded,
|
||||
discardSink,
|
||||
set node_priv(key) {
|
||||
_x.node_priv = key;
|
||||
},
|
||||
get node_priv() {
|
||||
return _x.node_priv;
|
||||
},
|
||||
get node_id() {
|
||||
return node_id;
|
||||
},
|
||||
get node_pub() {
|
||||
return node_pub;
|
||||
},
|
||||
get sink_groups() {
|
||||
return sink_groups;
|
||||
},
|
||||
get sink_chest() {
|
||||
return sink_chest;
|
||||
}
|
||||
}
|
||||
215
exchangemarket/src/main.js
Normal file
215
exchangemarket/src/main.js
Normal file
@ -0,0 +1,215 @@
|
||||
'use strict';
|
||||
global.floGlobals = require('../docs/scripts/floGlobals');
|
||||
require('./set_globals');
|
||||
require('../docs/scripts/lib');
|
||||
global.floCrypto = require('../docs/scripts/floCrypto');
|
||||
global.floBlockchainAPI = require('../docs/scripts/floBlockchainAPI');
|
||||
global.floTokenAPI = require('../docs/scripts/floTokenAPI');
|
||||
global.btcOperator = require('../docs/scripts/btcOperator');
|
||||
|
||||
(function () {
|
||||
const { adminID, application } = require("../docs/scripts/floExchangeAPI");
|
||||
floGlobals.adminID = adminID;
|
||||
floGlobals.application = application;
|
||||
})();
|
||||
|
||||
const keys = require('./keys');
|
||||
const DB = require("./database");
|
||||
const App = require('./app');
|
||||
|
||||
const backup = require('./backup/head');
|
||||
|
||||
const {
|
||||
BLOCKCHAIN_REFRESH_INTERVAL
|
||||
} = require("./_constants")["app"];
|
||||
|
||||
var app;
|
||||
|
||||
function refreshData(startup = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
refreshDataFromBlockchain().then(changes => {
|
||||
loadDataFromDB(changes, startup).then(_ => {
|
||||
if (!startup && changes.nodes)
|
||||
backup.reconstructAllActiveShares();
|
||||
app.refreshData(backup.nodeList);
|
||||
resolve("Data refresh successful")
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function refreshDataFromBlockchain() {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT txid FROM LastTx WHERE floID=?", [floGlobals.adminID]).then(result => {
|
||||
var query_options = { sentOnly: true, pattern: floGlobals.application };
|
||||
|
||||
let lastTx = result.length ? result[0].txid : undefined;
|
||||
if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
|
||||
query_options.after = lastTx;
|
||||
else if (!isNaN(lastTx))//lastTx is tx count (*backward support)
|
||||
query_options.ignoreOld = parseInt(lastTx);
|
||||
|
||||
floBlockchainAPI.readData(floGlobals.adminID, query_options).then(result => {
|
||||
let promises = [],
|
||||
nodes_change = false,
|
||||
assets_change = false,
|
||||
trusted_change = false;
|
||||
result.data.reverse().forEach(data => {
|
||||
var content = JSON.parse(data)[floGlobals.application];
|
||||
//Node List
|
||||
if (content.Nodes) {
|
||||
nodes_change = true;
|
||||
if (content.Nodes.remove)
|
||||
for (let n of content.Nodes.remove)
|
||||
promises.push(DB.query("DELETE FROM NodeList WHERE floID=?", [n]));
|
||||
if (content.Nodes.add)
|
||||
for (let n in content.Nodes.add)
|
||||
promises.push(DB.query("INSERT INTO NodeList (floID, uri) VALUE (?) ON DUPLICATE KEY UPDATE uri=?", [[n, content.Nodes.add[n]], content.Nodes.add[n]]));
|
||||
if (content.Nodes.update)
|
||||
for (let n in content.Nodes.update)
|
||||
promises.push(DB.query("UPDATE NodeList SET uri=? WHERE floID=?", [content.Nodes.update[n], n]));
|
||||
}
|
||||
//Asset List
|
||||
if (content.Assets) {
|
||||
assets_change = true;
|
||||
for (let a in content.Assets)
|
||||
promises.push(DB.query("INSERT INTO AssetList (asset, initialPrice) VALUE (?) ON DUPLICATE KEY UPDATE initialPrice=?", [[a, content.Assets[a]], content.Assets[a]]));
|
||||
}
|
||||
//Trusted List
|
||||
if (content.Trusted) {
|
||||
trusted_change = true;
|
||||
if (content.Trusted.remove)
|
||||
for (let id of content.Trusted.remove)
|
||||
promises.push(DB.query("DELETE FROM TrustedList WHERE floID=?", [id]));
|
||||
if (content.Trusted.add)
|
||||
for (let id of content.Trusted.add)
|
||||
promises.push(DB.query("INSERT INTO TrustedList (floID) VALUE (?) ON DUPLICATE KEY UPDATE floID=floID", [id]));
|
||||
}
|
||||
//Tag List with priority
|
||||
if (content.Tag) {
|
||||
if (content.Tag.remove)
|
||||
for (let t of content.Tag.remove)
|
||||
promises.push(DB.query("DELETE FROM TagList WHERE tag=?", [t]));
|
||||
if (content.Tag.add)
|
||||
for (let t in content.Tag.add)
|
||||
promises.push(DB.query("INSERT INTO TagList (tag, sellPriority, buyPriority) VALUE (?) ON DUPLICATE KEY UPDATE tag=tag", [[t, content.Tag.add[t].sellPriority, content.Tag.add[t].buyPriority]]));
|
||||
if (content.Tag.update)
|
||||
for (let t in content.Tag.update)
|
||||
for (let a in content.Tag.update[t])
|
||||
promises.push(`UPDATE TagList WHERE tag=? SET ${a}=?`, [t, content.Tag.update[t][a]]);
|
||||
}
|
||||
});
|
||||
promises.push(DB.query("INSERT INTO LastTx (floID, txid) VALUE (?) ON DUPLICATE KEY UPDATE txid=?", [[floGlobals.adminID, result.lastItem], result.lastItem]));
|
||||
//Check if all save process were successful
|
||||
Promise.allSettled(promises).then(results => {
|
||||
//console.debug(results.filter(r => r.status === "rejected"));
|
||||
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0))
|
||||
console.warn("Some blockchain data might not have been saved in database correctly");
|
||||
resolve({
|
||||
nodes: nodes_change,
|
||||
assets: assets_change,
|
||||
trusted: trusted_change
|
||||
});
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function loadDataFromDB(changes, startup) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let promises = [];
|
||||
if (startup || changes.nodes)
|
||||
promises.push(loadDataFromDB.nodeList());
|
||||
if (startup || changes.assets)
|
||||
promises.push(loadDataFromDB.assetList());
|
||||
if (startup || changes.trusted)
|
||||
promises.push(loadDataFromDB.trustedIDs());
|
||||
Promise.all(promises)
|
||||
.then(_ => resolve("Data load successful"))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
loadDataFromDB.nodeList = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT floID, uri FROM NodeList").then(result => {
|
||||
let nodes = {}
|
||||
for (let i in result)
|
||||
nodes[result[i].floID] = result[i].uri;
|
||||
//update dependents
|
||||
backup.nodeList = nodes;
|
||||
resolve(nodes);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
loadDataFromDB.assetList = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT asset FROM AssetList").then(result => {
|
||||
let assets = [];
|
||||
for (let i in result)
|
||||
assets.push(result[i].asset);
|
||||
//update dependents
|
||||
backup.assetList = assets;
|
||||
app.assetList = assets;
|
||||
resolve(assets);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
loadDataFromDB.trustedIDs = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT * FROM TrustedList").then(result => {
|
||||
let trustedIDs = [];
|
||||
for (let i in result)
|
||||
trustedIDs.push(result[i].floID);
|
||||
//update dependents
|
||||
app.trustedIDs = trustedIDs;
|
||||
resolve(trustedIDs);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function startServer() {
|
||||
let _pass, _I = "";
|
||||
for (let arg of process.argv) {
|
||||
if (/^-I=/.test(arg))
|
||||
_I = arg.split(/=(.*)/s)[1];
|
||||
else if (/^-password=/i.test(arg))
|
||||
_pass = arg.split(/=(.*)/s)[1];
|
||||
}
|
||||
const config = require(`../args/config${_I}.json`);
|
||||
try {
|
||||
let _tmp = require(`../args/keys${_I}.json`);
|
||||
_tmp = floCrypto.retrieveShamirSecret(_tmp);
|
||||
if (!_pass) {
|
||||
console.error('Password not entered!');
|
||||
process.exit(1);
|
||||
}
|
||||
keys.node_priv = Crypto.AES.decrypt(_tmp, _pass);
|
||||
} catch (error) {
|
||||
console.error('Unable to load private key!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Logged in as", keys.node_id);
|
||||
|
||||
DB.connect(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(result => {
|
||||
keys.init().then(result => {
|
||||
console.log(result);
|
||||
app = new App(config['secret']);
|
||||
refreshData(true).then(_ => {
|
||||
app.start(config['port']).then(result => {
|
||||
console.log(result);
|
||||
backup.init(app);
|
||||
setInterval(() => {
|
||||
refreshData()
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.error(error))
|
||||
}, BLOCKCHAIN_REFRESH_INTERVAL);
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error))
|
||||
}).catch(error => console.error(error));
|
||||
};
|
||||
503
exchangemarket/src/market.js
Normal file
503
exchangemarket/src/market.js
Normal file
@ -0,0 +1,503 @@
|
||||
'use strict';
|
||||
|
||||
const coupling = require('./coupling');
|
||||
const price = require("./price");
|
||||
const background = require('./background');
|
||||
const DB = require("./database");
|
||||
const blockchain = require('./blockchain');
|
||||
|
||||
const {
|
||||
TRADE_HASH_PREFIX,
|
||||
TRANSFER_HASH_PREFIX
|
||||
} = require('./_constants')["market"];
|
||||
|
||||
const eCode = require('../docs/scripts/floExchangeAPI').errorCode;
|
||||
const pCode = require('../docs/scripts/floExchangeAPI').processCode;
|
||||
|
||||
const updateBalance = background.updateBalance = coupling.updateBalance;
|
||||
|
||||
var assetList; //container for allowed assets
|
||||
|
||||
function login(floID, proxyKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("INSERT INTO UserSession (floID, proxyKey) VALUE (?) ON DUPLICATE KEY UPDATE session_time=DEFAULT, proxyKey=?", [[floID, proxyKey], proxyKey])
|
||||
.then(result => resolve("Login Successful"))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function logout(floID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("DELETE FROM UserSession WHERE floID=?", [floID])
|
||||
.then(result => resolve("Logout successful"))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function getRateHistory(asset, duration) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!asset || !assetList.includes(asset))
|
||||
reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset(${asset})`));
|
||||
else
|
||||
price.getHistory(asset, duration)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function getBalance(floID, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (floID && !floCrypto.validateAddr(floID))
|
||||
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID(${floID})`));
|
||||
else if (token && token !== floGlobals.currency && !assetList.includes(token))
|
||||
reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid token(${token})`));
|
||||
else if (!floID && !token)
|
||||
reject(INVALID(eCode.MISSING_PARAMETER, 'Missing parameters: requires atleast one (floID, token)'));
|
||||
else {
|
||||
var promise;
|
||||
if (floID && token)
|
||||
promise = getBalance.floID_token(floID, token);
|
||||
else if (floID)
|
||||
promise = getBalance.floID(floID);
|
||||
else if (token)
|
||||
promise = getBalance.token(token);
|
||||
promise.then(result => resolve(result)).catch(error => reject(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getBalance.floID_token = (floID, token) => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT quantity AS balance FROM UserBalance WHERE floID=? AND token=?", [floID, token]).then(result => resolve({
|
||||
floID,
|
||||
token,
|
||||
balance: result.length ? global.toStandardDecimal(result[0].balance) : 0
|
||||
})).catch(error => reject(error))
|
||||
});
|
||||
|
||||
getBalance.floID = (floID) => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT token, quantity AS balance FROM UserBalance WHERE floID=?", [floID]).then(result => {
|
||||
let response = {
|
||||
floID,
|
||||
balance: {}
|
||||
};
|
||||
for (let row of result)
|
||||
response.balance[row.token] = global.toStandardDecimal(row.balance);
|
||||
resolve(response);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
getBalance.token = (token) => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT floID, quantity AS balance FROM UserBalance WHERE token=?", [token]).then(result => {
|
||||
let response = {
|
||||
token: token,
|
||||
balance: {}
|
||||
};
|
||||
for (let row of result)
|
||||
response.balance[row.floID] = global.toStandardDecimal(row.balance);
|
||||
resolve(response);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
const getAssetBalance = (floID, asset) => new Promise((resolve, reject) => {
|
||||
let promises = [];
|
||||
promises.push(DB.query("SELECT IFNULL(SUM(quantity), 0) AS balance FROM UserBalance WHERE floID=? AND token=?", [floID, asset]));
|
||||
promises.push(asset === floGlobals.currency ?
|
||||
DB.query("SELECT IFNULL(SUM(quantity*maxPrice), 0) AS locked FROM BuyOrder WHERE floID=?", [floID]) :
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) 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(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset}`));
|
||||
else if (balance.net < amount)
|
||||
reject(INVALID(eCode.INSUFFICIENT_BALANCE, `Insufficient ${asset} (Some are locked in orders)`));
|
||||
else
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
function addSellOrder(floID, asset, quantity, min_price) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
|
||||
else if (typeof quantity !== "number" || quantity <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
|
||||
else if (typeof min_price !== "number" || min_price <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid min_price (${min_price})`));
|
||||
else if (!assetList.includes(asset))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset (${asset})`));
|
||||
getAssetBalance.check(floID, asset, quantity).then(_ => {
|
||||
checkSellRequirement(floID, asset, quantity, min_price).then(_ => {
|
||||
DB.query("INSERT INTO SellOrder(floID, asset, quantity, minPrice) VALUES (?)", [[floID, asset, quantity, min_price]]).then(result => {
|
||||
resolve('Sell Order placed successfully');
|
||||
coupling.initiate(asset);
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
const checkSellRequirement = (floID, asset, quantity, min_price) => new Promise((resolve, reject) => {
|
||||
Promise.all([
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS total_chips FROM SellChips WHERE floID=? AND asset=?", [floID, asset]),
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=?", [floID, asset])
|
||||
]).then(result => {
|
||||
let total = result[0][0].total_chips,
|
||||
locked = result[1][0].locked;
|
||||
if (total < locked + quantity)
|
||||
reject(INVALID(eCode.INSUFFICIENT_SELLCHIP, `Insufficient sell-chips for ${asset}`));
|
||||
else Promise.all([
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS total_chips FROM SellChips WHERE floID=? AND asset=? AND base<=?", [floID, asset, min_price]),
|
||||
DB.query("SELECT IFNULL(SUM(quantity), 0) AS locked FROM SellOrder WHERE floID=? AND asset=? AND minPrice<=?", [floID, asset, min_price])
|
||||
]).then(result => {
|
||||
let g_total = result[0][0].total_chips,
|
||||
g_locked = result[1][0].locked;
|
||||
let l_total = total - g_total,
|
||||
l_locked = locked - g_locked;
|
||||
var rem = g_total - g_locked;
|
||||
if (l_locked > l_total)
|
||||
rem -= l_locked - l_total;
|
||||
if (rem < quantity)
|
||||
reject(INVALID(eCode.GREATER_SELLCHIP_BASE, `Cannot sell below purchased price`));
|
||||
else
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
function addBuyOrder(floID, asset, quantity, max_price) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
|
||||
else if (typeof quantity !== "number" || quantity <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
|
||||
else if (typeof max_price !== "number" || max_price <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid max_price (${max_price})`));
|
||||
else if (!assetList.includes(asset))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid asset (${asset})`));
|
||||
getAssetBalance.check(floID, floGlobals.currency, quantity * max_price).then(_ => {
|
||||
DB.query("INSERT INTO BuyOrder(floID, asset, quantity, maxPrice) VALUES (?)", [[floID, asset, quantity, max_price]]).then(result => {
|
||||
resolve('Buy Order placed successfully');
|
||||
coupling.initiate(asset);
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
function cancelOrder(type, id, floID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
|
||||
let tableName;
|
||||
if (type === "buy")
|
||||
tableName = "BuyOrder";
|
||||
else if (type === "sell")
|
||||
tableName = "SellOrder";
|
||||
else
|
||||
return reject(INVALID(eCode.INVALID_TYPE, "Invalid Order type! Order type must be buy (or) sell"));
|
||||
DB.query("SELECT floID, asset FROM ?? WHERE id=?", [tableName, id]).then(result => {
|
||||
if (result.length < 1)
|
||||
return reject(INVALID(eCode.NOT_FOUND, "Order not found!"));
|
||||
else if (result[0].floID !== floID)
|
||||
return reject(INVALID(eCode.NOT_OWNER, "Order doesnt belong to the current user"));
|
||||
let asset = result[0].asset;
|
||||
//Delete the order
|
||||
DB.query("DELETE FROM ?? WHERE id=?", [tableName, id]).then(result => {
|
||||
resolve(tableName + "#" + id + " cancelled successfully");
|
||||
coupling.initiate(asset);
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
function getAccountDetails(floID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let promises = [
|
||||
DB.query("SELECT token, quantity FROM UserBalance WHERE floID=?", [floID]),
|
||||
DB.query("SELECT id, asset, quantity, minPrice, time_placed FROM SellOrder WHERE floID=?", [floID]),
|
||||
DB.query("SELECT id, asset, quantity, maxPrice, time_placed FROM BuyOrder WHERE floID=?", [floID])
|
||||
];
|
||||
Promise.allSettled(promises).then(results => {
|
||||
let response = {
|
||||
floID: floID,
|
||||
time: Date.now()
|
||||
};
|
||||
results.forEach((a, i) => {
|
||||
if (a.status === "rejected")
|
||||
console.error(a.reason);
|
||||
else
|
||||
switch (i) {
|
||||
case 0:
|
||||
response.tokenBalance = a.value;
|
||||
break;
|
||||
case 1:
|
||||
response.sellOrders = a.value;
|
||||
break;
|
||||
case 2:
|
||||
response.buyOrders = a.value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
DB.query("SELECT * FROM TradeTransactions WHERE seller=? OR buyer=?", [floID, floID])
|
||||
.then(result => response.transactions = result)
|
||||
.catch(error => console.error(error))
|
||||
.finally(_ => resolve(response));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getUserTransacts(floID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT mode, asset, amount, txid, locktime, r_status FROM VaultTransactions WHERE floID=?", [floID])
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function getTransactionDetails(txid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let tableName, type;
|
||||
if (txid.startsWith(TRANSFER_HASH_PREFIX)) {
|
||||
tableName = 'TransferTransactions';
|
||||
type = 'transfer';
|
||||
} else if (txid.startsWith(TRADE_HASH_PREFIX)) {
|
||||
tableName = 'TradeTransactions';
|
||||
type = 'trade';
|
||||
} else
|
||||
return reject(INVALID(eCode.INVALID_TX_ID, "Invalid TransactionID"));
|
||||
DB.query("SELECT * FROM ?? WHERE txid=?", [tableName, txid]).then(result => {
|
||||
if (result.length) {
|
||||
let details = result[0];
|
||||
details.type = type;
|
||||
if (tableName === 'TransferTransactions') //As json object is stored for receiver in transfer (to support one-to-many)
|
||||
details.receiver = JSON.parse(details.receiver);
|
||||
resolve(details);
|
||||
} else
|
||||
reject(INVALID(eCode.NOT_FOUND, "Transaction not found"));
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function transferToken(sender, receivers, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(sender))
|
||||
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid sender (${sender})`));
|
||||
else if (token !== floGlobals.currency && !assetList.includes(token))
|
||||
reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid token (${token})`));
|
||||
else {
|
||||
let invalidIDs = [],
|
||||
totalAmount = 0;
|
||||
for (let floID in receivers)
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
invalidIDs.push(floID);
|
||||
else
|
||||
totalAmount += receivers[floID];
|
||||
if (invalidIDs.length)
|
||||
reject(INVALID(eCode.INVALID_FLO_ID, `Invalid receiver (${invalidIDs})`));
|
||||
else getAssetBalance.check(sender, token, totalAmount).then(_ => {
|
||||
let txQueries = [];
|
||||
txQueries.push(updateBalance.consume(sender, token, totalAmount));
|
||||
for (let floID in receivers)
|
||||
txQueries.push(updateBalance.add(floID, token, receivers[floID]));
|
||||
checkDistributor(sender, token).then(result => {
|
||||
if (result)
|
||||
for (let floID in receivers)
|
||||
txQueries.push(["INSERT INTO SellChips (floID, asset, quantity) VALUES (?)", [[floID, token, receivers[floID]]]]);
|
||||
let time = Date.now();
|
||||
let hash = TRANSFER_HASH_PREFIX + Crypto.SHA256(JSON.stringify({
|
||||
sender: sender,
|
||||
receiver: receivers,
|
||||
token: token,
|
||||
totalAmount: totalAmount,
|
||||
tx_time: time,
|
||||
}));
|
||||
txQueries.push([
|
||||
"INSERT INTO TransferTransactions (sender, receiver, token, totalAmount, tx_time, txid) VALUE (?)",
|
||||
[[sender, JSON.stringify(receivers), token, totalAmount, new Date(time), hash]]
|
||||
]);
|
||||
DB.transaction(txQueries)
|
||||
.then(result => resolve(hash))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function depositFLO(floID, txid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT r_status FROM VaultTransactions WHERE txid=? AND floID=? AND asset=?", [txid, floID, "FLO"]).then(result => {
|
||||
if (result.length) {
|
||||
switch (result[0].r_status) {
|
||||
case pCode.STATUS_PENDING:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
case pCode.STATUS_REJECTED:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected"));
|
||||
case pCode.STATUS_SUCCESS:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add coins"));
|
||||
}
|
||||
} else
|
||||
DB.query("INSERT INTO VaultTransactions(floID, mode, asset_type, asset, txid, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_COIN, "FLO", txid, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Deposit request in process"))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
function withdrawFLO(floID, amount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
|
||||
else if (typeof amount !== "number" || amount <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
|
||||
getAssetBalance.check(floID, "FLO", amount).then(_ => {
|
||||
let txQueries = [];
|
||||
txQueries.push(updateBalance.consume(floID, "FLO", amount));
|
||||
DB.transaction(txQueries).then(result => {
|
||||
blockchain.withdrawAsset.init(floID, "FLO", amount);
|
||||
resolve("Withdrawal request is in process");
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
function depositToken(floID, txid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT r_status FROM VaultTransactions WHERE txid=? AND floID=? AND asset_type=?", [txid, floID, pCode.ASSET_TYPE_TOKEN]).then(result => {
|
||||
if (result.length) {
|
||||
switch (result[0].r_status) {
|
||||
case pCode.STATUS_PENDING:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
case pCode.STATUS_REJECTED:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already rejected"));
|
||||
case pCode.STATUS_SUCCESS:
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already used to add tokens"));
|
||||
}
|
||||
} else
|
||||
DB.query("INSERT INTO VaultTransactions(floID, mode, asset_type, txid, r_status) VALUES (?)", [[floID, pCode.VAULT_MODE_DEPOSIT, pCode.ASSET_TYPE_TOKEN, txid, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Deposit request in process"))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
function withdrawToken(floID, token, amount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!floCrypto.validateAddr(floID))
|
||||
return reject(INVALID(eCode.INVALID_FLO_ID, `Invalid floID (${floID})`));
|
||||
else if (typeof amount !== "number" || amount <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
|
||||
else if ((!assetList.includes(token) && token !== floGlobals.currency) || token === "FLO")
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, "Invalid Token"));
|
||||
//Check for FLO balance (transaction fee)
|
||||
let required_flo = floGlobals.sendAmt + floGlobals.fee;
|
||||
getAssetBalance.check(floID, "FLO", required_flo).then(_ => {
|
||||
getAssetBalance.check(floID, token, amount).then(_ => {
|
||||
let txQueries = [];
|
||||
txQueries.push(updateBalance.consume(floID, "FLO", required_flo));
|
||||
txQueries.push(updateBalance.consume(floID, token, amount));
|
||||
DB.transaction(txQueries).then(result => {
|
||||
//Send Token to user via token API
|
||||
blockchain.withdrawAsset.init(floID, token, amount);
|
||||
resolve("Withdrawal request is in process");
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
function addTag(floID, tag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("INSERT INTO UserTag (floID, tag) VALUE (?)", [[floID, tag]])
|
||||
.then(result => resolve(`Added ${floID} to ${tag}`))
|
||||
.catch(error => {
|
||||
if (error.code === "ER_DUP_ENTRY")
|
||||
reject(INVALID(eCode.DUPLICATE_ENTRY, `${floID} already in ${tag}`));
|
||||
else if (error.code === "ER_NO_REFERENCED_ROW")
|
||||
reject(INVALID(eCode.INVALID_TAG, `Invalid Tag`));
|
||||
else
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeTag(floID, tag) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("DELETE FROM UserTag WHERE floID=? AND tag=?", [floID, tag])
|
||||
.then(result => resolve(`Removed ${floID} from ${tag}`))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function addDistributor(floID, asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("INSERT INTO Distributors (floID, asset) VALUE (?)", [[floID, asset]])
|
||||
.then(result => resolve(`Added ${asset} distributor: ${floID}`))
|
||||
.catch(error => {
|
||||
if (error.code === "ER_DUP_ENTRY")
|
||||
reject(INVALID(eCode.DUPLICATE_ENTRY, `${floID} is already ${asset} disributor`));
|
||||
else if (error.code === "ER_NO_REFERENCED_ROW")
|
||||
reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid Asset`));
|
||||
else
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeDistributor(floID, asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("DELETE FROM Distributors WHERE floID=? AND tag=?", [floID, asset])
|
||||
.then(result => resolve(`Removed ${asset} distributor: ${floID}`))
|
||||
.catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function checkDistributor(floID, asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT id FROM Distributors WHERE floID=? AND asset=?", [floID, asset])
|
||||
.then(result => resolve(result.length ? true : false))
|
||||
.catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
login,
|
||||
logout,
|
||||
get rates() {
|
||||
return price.currentRates;
|
||||
},
|
||||
get priceCountDown() {
|
||||
return price.lastTimes;
|
||||
},
|
||||
addBuyOrder,
|
||||
addSellOrder,
|
||||
cancelOrder,
|
||||
getRateHistory,
|
||||
getBalance,
|
||||
getAccountDetails,
|
||||
getUserTransacts,
|
||||
getTransactionDetails,
|
||||
transferToken,
|
||||
depositFLO,
|
||||
withdrawFLO,
|
||||
depositToken,
|
||||
withdrawToken,
|
||||
addTag,
|
||||
removeTag,
|
||||
addDistributor,
|
||||
removeDistributor,
|
||||
set assetList(assets) {
|
||||
assetList = assets;
|
||||
background.assetList = assets;
|
||||
},
|
||||
get assetList() {
|
||||
return assetList
|
||||
}
|
||||
};
|
||||
188
exchangemarket/src/price.js
Normal file
188
exchangemarket/src/price.js
Normal file
@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
const DB = require("./database");
|
||||
|
||||
const {
|
||||
MIN_TIME,
|
||||
DOWN_RATE,
|
||||
UP_RATE,
|
||||
MAX_DOWN_PER_DAY,
|
||||
MAX_UP_PER_DAY,
|
||||
CHECK_RATED_SELLER,
|
||||
TOP_RANGE,
|
||||
REC_HISTORY_INTERVAL
|
||||
} = require("./_constants")["price"];
|
||||
|
||||
var currentRate = {}, //container for FLO price (from API or by model)
|
||||
lastTime = {}, //container for timestamp of the last tx
|
||||
noBuyOrder = {},
|
||||
noSellOrder = {};
|
||||
|
||||
const updateLastTime = asset => lastTime[asset] = Date.now();
|
||||
|
||||
//store FLO price in database every 1 hr
|
||||
function storeHistory(asset, rate) {
|
||||
DB.query("INSERT INTO PriceHistory (asset, rate) VALUE (?)", [[asset, global.toStandardDecimal(rate)]])
|
||||
.then(_ => null).catch(error => console.error(error))
|
||||
}
|
||||
|
||||
storeHistory.start = function () {
|
||||
storeHistory.stop();
|
||||
storeHistory.instance = setInterval(() => {
|
||||
for (let asset in currentRate)
|
||||
storeHistory(asset, currentRate[asset]);
|
||||
}, REC_HISTORY_INTERVAL);
|
||||
}
|
||||
|
||||
storeHistory.stop = function () {
|
||||
if (storeHistory.instance !== undefined) {
|
||||
clearInterval(storeHistory.instance);
|
||||
delete storeHistory.instance;
|
||||
}
|
||||
}
|
||||
|
||||
function getPastRate(asset, hrs = 24) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? hour ORDER BY rec_time LIMIT 1", [asset, hrs])
|
||||
.then(result => result.length ? resolve(result[0].rate) : reject('No records found in past 24hrs'))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
function getHistory(asset, duration = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
let { statement, values } = getHistory.getRateStatement(asset, duration);
|
||||
DB.query(statement, values)
|
||||
.then(result => resolve(result))
|
||||
.catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
getHistory.statement = {
|
||||
'all-time': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? GROUP BY time ORDER BY time",
|
||||
'year': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? year GROUP BY time ORDER BY time",
|
||||
'month': "SELECT DATE(rec_time) AS time, AVG(rate) as rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? month GROUP BY time ORDER BY time",
|
||||
'week': "SELECT rec_time AS time, rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? week ORDER BY time",
|
||||
'day': "SELECT rec_time AS time, rate FROM PriceHistory WHERE asset=? AND rec_time >= NOW() - INTERVAL ? day ORDER BY time"
|
||||
}
|
||||
|
||||
getHistory.getRateStatement = (asset, duration) => {
|
||||
let n = duration.match(/\d+/g),
|
||||
d = duration.match(/\D+/g);
|
||||
n = n ? n[0] || 1 : 1;
|
||||
d = d ? d[0].replace(/[-\s]/g, '') : "";
|
||||
|
||||
switch (d.toLowerCase()) {
|
||||
case "day":
|
||||
case "days":
|
||||
return { statement: getHistory.statement['day'], values: [asset, n] };
|
||||
case "week":
|
||||
case "weeks":
|
||||
return { statement: getHistory.statement['week'], values: [asset, n] };
|
||||
case "month":
|
||||
case "months":
|
||||
return { statement: getHistory.statement['month'], values: [asset, n] };
|
||||
case "year":
|
||||
case "years":
|
||||
return { statement: getHistory.statement['year'], values: [asset, n] };
|
||||
case "alltime":
|
||||
return { statement: getHistory.statement['all-time'], values: [asset] };
|
||||
default:
|
||||
return { statement: getHistory.statement['day'], values: [asset, 1] };
|
||||
}
|
||||
}
|
||||
|
||||
function loadRate(asset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof currentRate[asset] !== "undefined")
|
||||
return resolve(currentRate[asset]);
|
||||
DB.query("SELECT rate FROM PriceHistory WHERE asset=? ORDER BY rec_time DESC LIMIT 1", [asset]).then(result => {
|
||||
updateLastTime(asset);
|
||||
if (result.length)
|
||||
resolve(currentRate[asset] = result[0].rate);
|
||||
else
|
||||
DB.query("SELECT initialPrice FROM AssetList WHERE asset=?", [asset]).then(result => {
|
||||
currentRate[asset] = result[0].initialPrice;
|
||||
storeHistory(asset, currentRate[asset]);
|
||||
resolve(currentRate[asset]);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function getRates(asset, updatePrice = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadRate(asset).then(_ => {
|
||||
//console.debug(asset, currentRate[asset]);
|
||||
let cur_time = Date.now();
|
||||
if (!updatePrice || cur_time - lastTime[asset] < MIN_TIME) //Minimum time to update not crossed: No update required
|
||||
resolve(currentRate[asset]);
|
||||
else if (noBuyOrder[asset] && noSellOrder[asset]) //Both are not available: No update required
|
||||
resolve(currentRate[asset]);
|
||||
else if (noBuyOrder[asset] === null || noSellOrder[asset] === null) //An error has occured during last process: No update (might cause price to crash/jump)
|
||||
resolve(currentRate[asset]);
|
||||
else
|
||||
getPastRate(asset).then(ratePast24hr => {
|
||||
if (noBuyOrder[asset]) {
|
||||
//No Buy, But Sell available: Decrease the price
|
||||
let tmp_val = currentRate[asset] * (1 - DOWN_RATE);
|
||||
if (tmp_val >= ratePast24hr * (1 - MAX_DOWN_PER_DAY))
|
||||
currentRate[asset] = tmp_val;
|
||||
else
|
||||
console.debug("Max Price down for the day has reached");
|
||||
resolve(currentRate[asset]);
|
||||
} else if (noSellOrder[asset]) {
|
||||
//No Sell, But Buy available: Increase the price
|
||||
checkForRatedSellers(asset).then(result => {
|
||||
if (result) {
|
||||
let tmp_val = currentRate[asset] * (1 + UP_RATE);
|
||||
if (tmp_val <= ratePast24hr * (1 + MAX_UP_PER_DAY))
|
||||
currentRate[asset] = tmp_val;
|
||||
else
|
||||
console.debug("Max Price up for the day has reached");
|
||||
}
|
||||
}).catch(error => console.error(error)).finally(_ => resolve(currentRate[asset]));
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
resolve(currentRate[asset]);
|
||||
});
|
||||
}).catch(error => reject(error));
|
||||
})
|
||||
}
|
||||
|
||||
function checkForRatedSellers(asset) {
|
||||
//Check if there are best rated sellers?
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!CHECK_RATED_SELLER) //switch for the check case
|
||||
return resolve(true);
|
||||
DB.query("SELECT MAX(sellPriority) as max_p FROM TagList").then(result => {
|
||||
let ratedMin = result[0].max_p * (1 - TOP_RANGE);
|
||||
DB.query("SELECT COUNT(*) as value FROM SellOrder WHERE floID IN (" +
|
||||
" SELECT UserTag.floID FROM UserTag INNER JOIN TagList ON UserTag.tag = TagList.tag" +
|
||||
" WHERE TagList.sellPriority > ?) AND asset=?", [ratedMin, asset]).then(result => {
|
||||
resolve(result[0].value > 0);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRates,
|
||||
getHistory,
|
||||
storeHistory,
|
||||
updateLastTime,
|
||||
MIN_TIME,
|
||||
noOrder(asset, buy, sell) {
|
||||
noBuyOrder[asset] = buy;
|
||||
noSellOrder[asset] = sell;
|
||||
},
|
||||
get currentRates() {
|
||||
return Object.assign({}, currentRate);
|
||||
},
|
||||
get lastTimes() {
|
||||
let countDown = {};
|
||||
for (let asset in lastTime)
|
||||
countDown[asset] = lastTime[asset] + MIN_TIME;
|
||||
return countDown;
|
||||
}
|
||||
}
|
||||
743
exchangemarket/src/request.js
Normal file
743
exchangemarket/src/request.js
Normal file
@ -0,0 +1,743 @@
|
||||
'use strict';
|
||||
const DB = require("./database");
|
||||
|
||||
const market = require("./market");
|
||||
const conversion = require('./services/conversion');
|
||||
const blockchain_bonds = require("./services/bonds");
|
||||
const bobs_fund = require("./services/bobs-fund");
|
||||
const background = require("./background");
|
||||
const sink = require("./backup/head").sink;
|
||||
const keys = require("./keys");
|
||||
|
||||
const {
|
||||
SIGN_EXPIRE_TIME,
|
||||
MAX_SESSION_TIMEOUT
|
||||
} = require("./_constants")["request"];
|
||||
|
||||
const eCode = require('../docs/scripts/floExchangeAPI').errorCode;
|
||||
const serviceList = require('../docs/scripts/floExchangeAPI').serviceList;
|
||||
|
||||
var trustedIDs, secret; //containers for trusted IDs and secret
|
||||
|
||||
global.INVALID = function (ecode, message) {
|
||||
if (!(this instanceof INVALID))
|
||||
return new INVALID(ecode, message);
|
||||
this.message = message;
|
||||
this.ecode = ecode;
|
||||
}
|
||||
INVALID.e_code = 400;
|
||||
INVALID.prototype.toString = function () {
|
||||
return "E" + this.ecode + ": " + this.message;
|
||||
}
|
||||
INVALID.str = (ecode, message) => INVALID(ecode, message).toString();
|
||||
|
||||
global.INTERNAL = function INTERNAL(message) {
|
||||
if (!(this instanceof INTERNAL))
|
||||
return new INTERNAL(message);
|
||||
this.message = message;
|
||||
}
|
||||
INTERNAL.e_code = 500;
|
||||
INTERNAL.prototype.toString = function () {
|
||||
return "E" + eCode.INTERNAL_ERROR + ": " + this.message;
|
||||
}
|
||||
INTERNAL.str = (ecode, message) => INTERNAL(ecode, message).toString();
|
||||
|
||||
const INCORRECT_SERVER_ERROR = INVALID(eCode.INCORRECT_SERVER, "Incorrect server");
|
||||
|
||||
var serving;
|
||||
|
||||
function validateRequest(request, sign, floID, pubKey) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!serving)
|
||||
reject(INCORRECT_SERVER_ERROR);
|
||||
else if (!request.timestamp)
|
||||
reject(INVALID(eCode.MISSING_PARAMETER, "Timestamp parameter missing"));
|
||||
else if (Date.now() - SIGN_EXPIRE_TIME > request.timestamp)
|
||||
reject(INVALID(eCode.EXPIRED_SIGNATURE, "Signature Expired"));
|
||||
else if (!floCrypto.validateAddr(floID))
|
||||
reject(INVALID(eCode.INVALID_FLO_ID, "Invalid floID"));
|
||||
else if (typeof request !== "object")
|
||||
reject(INVALID(eCode.INVALID_REQUEST_FORMAT, "Request is not an object"));
|
||||
else validateRequest.getSignKey(floID, pubKey).then(signKey => {
|
||||
let req_str = Object.keys(request).sort().map(r => r + ":" + request[r]).join("|");
|
||||
try {
|
||||
if (!floCrypto.verifySign(req_str, sign, signKey))
|
||||
reject(INVALID(eCode.INVALID_SIGNATURE, "Invalid request signature"));
|
||||
else validateRequest.checkIfSignUsed(sign)
|
||||
.then(result => resolve(req_str))
|
||||
.catch(error => reject(error))
|
||||
} catch {
|
||||
reject(INVALID(eCode.INVALID_SIGNATURE, "Corrupted sign/key"));
|
||||
}
|
||||
}).catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
validateRequest.getSignKey = (floID, pubKey) => new Promise((resolve, reject) => {
|
||||
if (!pubKey)
|
||||
DB.query("SELECT session_time, proxyKey FROM UserSession WHERE floID=?", [floID]).then(result => {
|
||||
if (result.length < 1)
|
||||
reject(INVALID(eCode.SESSION_INVALID, "Session not active"));
|
||||
else if (result[0].session_time + MAX_SESSION_TIMEOUT < Date.now())
|
||||
reject(INVALID(eCode.SESSION_EXPIRED, "Session Expired! Re-login required"));
|
||||
else
|
||||
resolve(result[0].proxyKey);
|
||||
}).catch(error => reject(error));
|
||||
else if (floCrypto.getFloID(pubKey) === floID)
|
||||
resolve(pubKey);
|
||||
else
|
||||
reject(INVALID(eCode.INVALID_PUBLIC_KEY, "Invalid pubKey"));
|
||||
});
|
||||
|
||||
validateRequest.checkIfSignUsed = sign => new Promise((resolve, reject) => {
|
||||
DB.query("SELECT id FROM RequestLog WHERE sign=?", [sign]).then(result => {
|
||||
if (result.length)
|
||||
reject(INVALID(eCode.DUPLICATE_SIGNATURE, "Duplicate signature"));
|
||||
else
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
|
||||
function logRequest(floID, req_str, sign, proxy = false) {
|
||||
//console.debug(floID, req_str);
|
||||
DB.query("INSERT INTO RequestLog (floID, request, sign, proxy) VALUES (?)", [[floID, req_str, sign, proxy]])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function processRequest(res, floID, pubKey, sign, rText, validateObj, marketFn, log = true) {
|
||||
validateRequest(validateObj, sign, floID, pubKey).then(req_str => {
|
||||
marketFn().then(result => {
|
||||
if (log) logRequest(floID, req_str, sign, !pubKey);
|
||||
res.send(result);
|
||||
}).catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str(rText + " failed! Try again later!"));
|
||||
}
|
||||
})
|
||||
}).catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Request processing failed! Try again later!"));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* User Requests */
|
||||
|
||||
function Account(req, res) {
|
||||
let data = req.body;
|
||||
validateRequest({
|
||||
type: "get_account",
|
||||
timestamp: data.timestamp
|
||||
}, data.sign, data.floID, data.pubKey).then(req_str => {
|
||||
market.getAccountDetails(data.floID).then(result => {
|
||||
if (trustedIDs.includes(data.floID))
|
||||
result.subAdmin = true;
|
||||
res.send(result);
|
||||
});
|
||||
}).catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Request processing failed! Try again later!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function Login(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.code || data.hash != Crypto.SHA1(data.code + secret))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_LOGIN_CODE, "Invalid Code"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Login", {
|
||||
type: "login",
|
||||
random: data.code,
|
||||
proxyKey: data.proxyKey,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.login(data.floID, data.proxyKey));
|
||||
}
|
||||
|
||||
function Logout(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Logout", {
|
||||
type: "logout",
|
||||
timestamp: data.timestamp
|
||||
}, () => market.logout(data.floID));
|
||||
}
|
||||
|
||||
function PlaceSellOrder(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Sell order placement", {
|
||||
type: "sell_order",
|
||||
asset: data.asset,
|
||||
quantity: data.quantity,
|
||||
min_price: data.min_price,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.addSellOrder(data.floID, data.asset, data.quantity, data.min_price));
|
||||
}
|
||||
|
||||
function PlaceBuyOrder(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Buy order placement", {
|
||||
type: "buy_order",
|
||||
asset: data.asset,
|
||||
quantity: data.quantity,
|
||||
max_price: data.max_price,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.addBuyOrder(data.floID, data.asset, data.quantity, data.max_price));
|
||||
}
|
||||
|
||||
function CancelOrder(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Order cancellation", {
|
||||
type: "cancel_order",
|
||||
order: data.orderType,
|
||||
id: data.orderID,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.cancelOrder(data.orderType, data.orderID, data.floID));
|
||||
}
|
||||
|
||||
function TransferToken(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Token Transfer", {
|
||||
type: "transfer_token",
|
||||
receiver: JSON.stringify(data.receiver),
|
||||
token: data.token,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.transferToken(data.floID, data.receiver, data.token));
|
||||
}
|
||||
|
||||
function DepositFLO(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Deposit FLO", {
|
||||
type: "deposit_flo",
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.depositFLO(data.floID, data.txid));
|
||||
}
|
||||
|
||||
function WithdrawFLO(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Withdraw FLO", {
|
||||
type: "withdraw_flo",
|
||||
amount: data.amount,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.withdrawFLO(data.floID, data.amount));
|
||||
}
|
||||
|
||||
function DepositToken(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Deposit Token", {
|
||||
type: "deposit_token",
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.depositToken(data.floID, data.txid));
|
||||
}
|
||||
|
||||
function WithdrawToken(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Withdraw Token", {
|
||||
type: "withdraw_token",
|
||||
token: data.token,
|
||||
amount: data.amount,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.withdrawToken(data.floID, data.token, data.amount));
|
||||
}
|
||||
|
||||
function GetUserTransacts(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "User Transacts", {
|
||||
type: "get_transact",
|
||||
timestamp: data.timestamp
|
||||
}, () => market.getUserTransacts(data.floID));
|
||||
}
|
||||
|
||||
function AddUserTag(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Add user-tag", {
|
||||
type: "add_tag",
|
||||
user: data.user,
|
||||
tag: data.tag,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.addTag(data.user, data.tag));
|
||||
}
|
||||
|
||||
function RemoveUserTag(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Remove user-tag", {
|
||||
type: "remove_tag",
|
||||
user: data.user,
|
||||
tag: data.tag,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.removeTag(data.user, data.tag));
|
||||
}
|
||||
|
||||
function AddDistributor(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Add distributor", {
|
||||
type: "add_distributor",
|
||||
distributor: data.distributor,
|
||||
asset: data.asset,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.addDistributor(data.distributor, data.asset));
|
||||
}
|
||||
|
||||
function RemoveDistributor(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Remove distributor", {
|
||||
type: "remove_distributor",
|
||||
distributor: data.distributor,
|
||||
asset: data.asset,
|
||||
timestamp: data.timestamp
|
||||
}, () => market.removeDistributor(data.distributor, data.asset));
|
||||
}
|
||||
|
||||
function GenerateSink(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Generate Sink", {
|
||||
type: "generate_sink",
|
||||
group: data.group,
|
||||
timestamp: data.timestamp
|
||||
}, () => sink.generate(data.group));
|
||||
}
|
||||
|
||||
function ReshareSink(req, res) {
|
||||
let data = req.body;
|
||||
console.debug(data)
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else if (!floCrypto.validateAddr(data.id))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_VALUE, `Invalid ID ${data.id}`));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Reshare Sink", {
|
||||
type: "reshare_sink",
|
||||
id: data.id,
|
||||
timestamp: data.timestamp
|
||||
}, () => sink.reshare(data.id));
|
||||
}
|
||||
|
||||
function DiscardSink(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Discard Sink", {
|
||||
type: "discard_sink",
|
||||
id: data.id,
|
||||
timestamp: data.timestamp
|
||||
}, () => sink.discard(data.id));
|
||||
}
|
||||
|
||||
function ConvertTo(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Conversion", {
|
||||
type: "convert_to",
|
||||
coin: data.coin,
|
||||
amount: data.amount,
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.convertToCoin(data.floID, data.txid, data.coin, data.amount));
|
||||
}
|
||||
|
||||
function ConvertFrom(req, res) {
|
||||
let data = req.body;
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Conversion", {
|
||||
type: "convert_from",
|
||||
coin: data.coin,
|
||||
quantity: data.quantity,
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.convertFromCoin(data.floID, data.txid, data.tx_hex, data.coin, data.quantity));
|
||||
}
|
||||
|
||||
function DepositConvertCoinFund(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Conversion Fund", {
|
||||
type: "deposit_convert_coin_fund",
|
||||
coin: data.coin,
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.depositFund.coin(data.floID, data.txid, data.coin));
|
||||
}
|
||||
|
||||
function DepositConvertCurrencyFund(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Conversion Fund", {
|
||||
type: "deposit_convert_currency_fund",
|
||||
coin: data.coin,
|
||||
txid: data.txid,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.depositFund.currency(data.floID, data.txid, data.coin));
|
||||
}
|
||||
|
||||
function WithdrawConvertCoinFund(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Conversion Fund", {
|
||||
type: "withdraw_convert_coin_fund",
|
||||
coin: data.coin,
|
||||
quantity: data.quantity,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.withdrawFund.coin(data.floID, data.coin, data.quantity));
|
||||
}
|
||||
|
||||
function WithdrawConvertCurrencyFund(req, res) {
|
||||
let data = req.body;
|
||||
if (data.floID !== floGlobals.adminID)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Conversion Fund", {
|
||||
type: "withdraw_convert_currency_fund",
|
||||
coin: data.coin,
|
||||
amount: data.amount,
|
||||
timestamp: data.timestamp
|
||||
}, () => conversion.withdrawFund.currency(data.floID, data.coin, data.amount));
|
||||
}
|
||||
|
||||
function CloseBlockchainBond(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Blockchain Bond Closing", {
|
||||
type: "close_blockchain_bond",
|
||||
bond_id: data.bond_id,
|
||||
timestamp: data.timestamp
|
||||
}, () => blockchain_bonds.closeBond(data.bond_id, data.floID, `${data.timestamp}.${data.sign}`));
|
||||
}
|
||||
|
||||
function CloseBobsFund(req, res) {
|
||||
let data = req.body;
|
||||
if (!data.pubKey)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Public key missing"));
|
||||
else
|
||||
processRequest(res, data.floID, data.pubKey, data.sign, "Bob's Fund closing", {
|
||||
type: "close_bobs_fund",
|
||||
fund_id: data.fund_id,
|
||||
timestamp: data.timestamp
|
||||
}, () => bobs_fund.closeFund(data.fund_id, data.floID, `${data.timestamp}.${data.sign}`));
|
||||
}
|
||||
|
||||
function CheckBlockchainBondBalance(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Check blockchain-bond", {
|
||||
type: "check_blockchain_bond",
|
||||
prior_time: data.prior_time,
|
||||
timestamp: data.timestamp
|
||||
}, () => blockchain_bonds.checkBondBalance(data.prior_time), false);
|
||||
}
|
||||
|
||||
function CheckBobsFundBalance(req, res) {
|
||||
let data = req.body;
|
||||
if (!trustedIDs.includes(data.floID))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.ACCESS_DENIED, "Access Denied"));
|
||||
else processRequest(res, data.floID, data.pubKey, data.sign, "Check bobs-fund", {
|
||||
type: "check_bobs_fund",
|
||||
prior_time: data.prior_time,
|
||||
timestamp: data.timestamp
|
||||
}, () => bobs_fund.checkFundBalance(data.prior_time), false);
|
||||
}
|
||||
|
||||
/* Public Requests */
|
||||
|
||||
function GetLoginCode(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let randID = floCrypto.randString(8, true) + Math.round(Date.now() / 1000);
|
||||
let hash = Crypto.SHA1(randID + secret);
|
||||
res.send({
|
||||
code: randID,
|
||||
hash: hash
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ListSellOrders(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let asset = req.query.asset;
|
||||
if (asset && !market.assetList.includes(asset))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_TOKEN_NAME, "Invalid asset parameter"));
|
||||
else
|
||||
DB.query("SELECT SellOrder.floID, SellOrder.asset, SellOrder.minPrice, SellOrder.quantity, SellOrder.time_placed FROM SellOrder" +
|
||||
" INNER JOIN UserBalance ON UserBalance.floID = SellOrder.floID AND UserBalance.token = SellOrder.asset" +
|
||||
" INNER JOIN SellChips ON SellChips.floID = SellOrder.floID AND SellChips.asset = SellOrder.asset" +
|
||||
" LEFT JOIN UserTag ON UserTag.floID = SellOrder.floID" +
|
||||
" LEFT JOIN TagList ON TagList.tag = UserTag.tag" +
|
||||
" WHERE UserBalance.quantity >= SellOrder.quantity" +
|
||||
(asset ? " AND SellOrder.asset = ?" : "") +
|
||||
" GROUP BY SellOrder.id" +
|
||||
" ORDER BY MAX(TagList.sellPriority) DESC, MIN(SellChips.locktime) ASC, SellOrder.time_placed ASC" +
|
||||
" LIMIT 100", [asset || null])
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function ListBuyOrders(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let asset = req.query.asset;
|
||||
if (asset && !market.assetList.includes(asset))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_TOKEN_NAME, "Invalid asset parameter"));
|
||||
else
|
||||
DB.query("SELECT BuyOrder.floID, BuyOrder.asset, BuyOrder.maxPrice, BuyOrder.quantity, BuyOrder.time_placed FROM BuyOrder" +
|
||||
" INNER JOIN UserBalance ON UserBalance.floID = BuyOrder.floID AND UserBalance.token = ?" +
|
||||
" LEFT JOIN UserTag ON UserTag.floID = BuyOrder.floID" +
|
||||
" LEFT JOIN TagList ON TagList.tag = UserTag.tag" +
|
||||
" WHERE UserBalance.quantity >= BuyOrder.maxPrice * BuyOrder.quantity" +
|
||||
(asset ? " AND BuyOrder.asset = ?" : "") +
|
||||
" GROUP BY BuyOrder.id" +
|
||||
" ORDER BY MAX(TagList.buyPriority) DESC, BuyOrder.time_placed ASC" +
|
||||
" LIMIT 100", [floGlobals.currency, asset || null])
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ListTradeTransactions(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let asset = req.query.asset;
|
||||
if (asset && !market.assetList.includes(asset))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_TOKEN_NAME, "Invalid asset parameter"));
|
||||
else
|
||||
DB.query("SELECT * FROM TradeTransactions" +
|
||||
(asset ? " WHERE asset = ?" : "") +
|
||||
" ORDER BY tx_time DESC LIMIT 1000", [asset || null])
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Try again later!"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function GetConvertValues(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else conversion.getConvertValues()
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function GetSink(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let service = req.query.service;
|
||||
if (!service)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "Missing service parameter"));
|
||||
else if (!(Object.values(serviceList).includes(service)))
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_VALUE, "Invalid service parameter"));
|
||||
else {
|
||||
let group;
|
||||
switch (service) {
|
||||
case serviceList.EXCHANGE: group = keys.sink_groups.EXCHANGE; break;
|
||||
case serviceList.CONVERT: group = keys.sink_groups.CONVERT; break;
|
||||
case serviceList.BLOCKCHAIN_BOND: group = keys.sink_groups.BLOCKCHAIN_BONDS; break;
|
||||
case serviceList.BOBS_FUND: group = keys.sink_groups.BOBS_FUND; break;
|
||||
}
|
||||
res.send(keys.sink_chest.active_pick(group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function GetRates(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let asset = req.query.asset,
|
||||
rates = market.rates,
|
||||
countDown = market.priceCountDown;
|
||||
if (asset) {
|
||||
if (asset in rates)
|
||||
res.send({
|
||||
asset: asset,
|
||||
rate: rates[asset],
|
||||
countDown: countDown[asset]
|
||||
});
|
||||
else
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.INVALID_TOKEN_NAME, "Invalid asset parameter"));
|
||||
} else
|
||||
res.send({
|
||||
rates,
|
||||
countDown
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function GetRateHistory(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let asset = req.query.asset,
|
||||
duration = req.query.duration || "";
|
||||
market.getRateHistory(asset, duration)
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function GetTransaction(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let txid = req.query.txid;
|
||||
if (!txid)
|
||||
res.status(INVALID.e_code).send(INVALID.str(eCode.MISSING_PARAMETER, "txid (transactionID) parameter missing"));
|
||||
else market.getTransactionDetails(txid)
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function GetBalance(req, res) {
|
||||
if (!serving)
|
||||
res.status(INVALID.e_code).send(INCORRECT_SERVER_ERROR.toString());
|
||||
else {
|
||||
let floID = req.query.floID || req.query.addr,
|
||||
token = req.query.token || req.query.asset;
|
||||
market.getBalance(floID, token)
|
||||
.then(result => res.send(result))
|
||||
.catch(error => {
|
||||
if (error instanceof INVALID)
|
||||
res.status(INVALID.e_code).send(error.toString());
|
||||
else {
|
||||
console.error(error);
|
||||
res.status(INTERNAL.e_code).send(INTERNAL.str("Unable to process! Try again later!"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GetLoginCode,
|
||||
Login,
|
||||
Logout,
|
||||
PlaceBuyOrder,
|
||||
PlaceSellOrder,
|
||||
CancelOrder,
|
||||
TransferToken,
|
||||
ListSellOrders,
|
||||
ListBuyOrders,
|
||||
ListTradeTransactions,
|
||||
GetRates,
|
||||
GetRateHistory,
|
||||
GetTransaction,
|
||||
GetBalance,
|
||||
GetSink,
|
||||
Account,
|
||||
DepositFLO,
|
||||
WithdrawFLO,
|
||||
DepositToken,
|
||||
WithdrawToken,
|
||||
GetUserTransacts,
|
||||
AddUserTag,
|
||||
RemoveUserTag,
|
||||
AddDistributor,
|
||||
RemoveDistributor,
|
||||
GenerateSink,
|
||||
ReshareSink,
|
||||
DiscardSink,
|
||||
GetConvertValues,
|
||||
ConvertTo,
|
||||
ConvertFrom,
|
||||
DepositConvertCoinFund,
|
||||
DepositConvertCurrencyFund,
|
||||
WithdrawConvertCoinFund,
|
||||
WithdrawConvertCurrencyFund,
|
||||
CloseBlockchainBond,
|
||||
CloseBobsFund,
|
||||
CheckBlockchainBondBalance,
|
||||
CheckBobsFundBalance,
|
||||
set trustedIDs(ids) {
|
||||
trustedIDs = ids;
|
||||
},
|
||||
set assetList(assets) {
|
||||
market.assetList = assets;
|
||||
},
|
||||
set secret(s) {
|
||||
secret = s;
|
||||
},
|
||||
refreshData(nodeList) {
|
||||
blockchain_bonds.refresh(nodeList);
|
||||
bobs_fund.refresh(nodeList);
|
||||
},
|
||||
pause() {
|
||||
serving = false;
|
||||
background.periodicProcess.stop();
|
||||
},
|
||||
resume() {
|
||||
serving = true;
|
||||
background.periodicProcess.start();
|
||||
}
|
||||
};
|
||||
365
exchangemarket/src/services/bobs-fund.js
Normal file
365
exchangemarket/src/services/bobs-fund.js
Normal file
@ -0,0 +1,365 @@
|
||||
'use strict';
|
||||
|
||||
const DB = require("../database");
|
||||
const { sink_chest, sink_groups } = require("../keys");
|
||||
const eCode = require('../../docs/scripts/floExchangeAPI').errorCode;
|
||||
const pCode = require('../../docs/scripts/floExchangeAPI').processCode;
|
||||
const getRate = require('./conversion').getRate;
|
||||
|
||||
const bobsFund = (function () {
|
||||
const productStr = "Bobs Fund";
|
||||
|
||||
const magnitude = m => {
|
||||
switch (m) {
|
||||
case "thousand": return 1000;
|
||||
case "lakh": case "lakhs": return 100000;
|
||||
case "million": return 1000000;
|
||||
case "crore": case "crores": return 10000000;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
const parseNumber = (str) => {
|
||||
let n = 0,
|
||||
g = 0;
|
||||
str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => {
|
||||
if (!isNaN(s))
|
||||
g = parseFloat(s);
|
||||
else {
|
||||
let m = magnitude(s);
|
||||
if (m !== null) {
|
||||
n += m * g;
|
||||
g = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
return n + g;
|
||||
}
|
||||
const parsePeriod = (str) => {
|
||||
let P = '', n = 0;
|
||||
str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => {
|
||||
if (!isNaN(s))
|
||||
n = parseFloat(s);
|
||||
else switch (s) {
|
||||
case "year(s)": case "year": case "years": P += (n + 'Y'); n = 0; break;
|
||||
case "month(s)": case "month": case "months": P += (n + 'M'); n = 0; break;
|
||||
case "day(s)": case "day": case "days": P += (n + 'D'); n = 0; break;
|
||||
}
|
||||
});
|
||||
return P;
|
||||
}
|
||||
const dateFormat = (date = null) => {
|
||||
let d = (date ? new Date(date) : new Date()).toDateString();
|
||||
return [d.substring(8, 10), d.substring(4, 7), d.substring(11, 15)].join(" ");
|
||||
}
|
||||
|
||||
const dateAdder = function (start_date, duration) {
|
||||
let date = new Date(start_date);
|
||||
let y = parseInt(duration.match(/\d+Y/)),
|
||||
m = parseInt(duration.match(/\d+M/)),
|
||||
d = parseInt(duration.match(/\d+D/));
|
||||
if (!isNaN(y))
|
||||
date.setFullYear(date.getFullYear() + y);
|
||||
if (!isNaN(m))
|
||||
date.setMonth(date.getMonth() + m);
|
||||
if (!isNaN(d))
|
||||
date.setDate(date.getDate() + d);
|
||||
return date;
|
||||
}
|
||||
|
||||
function calcNetValue(BTC_base, BTC_net, USD_base, USD_net, amount, fee) {
|
||||
let gain, interest, net;
|
||||
gain = (BTC_net - BTC_base) / BTC_base;
|
||||
interest = gain * (1 - fee)
|
||||
net = amount / USD_base;
|
||||
net += net * interest;
|
||||
return net * USD_net;
|
||||
}
|
||||
|
||||
function stringify_main(BTC_base, USD_base, start_date, duration, investments, fee = 0, tapoutWindow = null, tapoutInterval = null) {
|
||||
let result = [
|
||||
`${productStr}`,
|
||||
`Base Value: ${BTC_base} USD`,
|
||||
`USD INR rate at start: ${USD_base}`,
|
||||
`Start date: ${dateFormat(start_date)}`,
|
||||
`Duration: ${duration}`,
|
||||
`Management Fee: ${fee != 0 ? fee + "%" : "0 (Zero)"}`
|
||||
];
|
||||
if (tapoutInterval) {
|
||||
if (Array.isArray(tapoutInterval)) {
|
||||
let x = tapoutInterval.pop(),
|
||||
y = tapoutInterval.join(", ")
|
||||
tapoutInterval = `${y} and ${x}`
|
||||
}
|
||||
result.push(`Tapout availability: ${tapoutWindow} after ${tapoutInterval}`);
|
||||
}
|
||||
result.push(`Investment(s) (INR): ${investments.map(f => `${f[0].trim()}-${f[1].trim()}`).join("; ")}`);
|
||||
return result.join("|");
|
||||
}
|
||||
|
||||
function stringify_continue(fund_id, investments) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`continue: ${fund_id}`,
|
||||
`Investment(s) (INR): ${investments.map(f => `${f[0].trim()}-${f[1].trim()}`).join("; ")}`
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function stringify_end(fund_id, floID, end_date, BTC_net, USD_net, amount, ref_sign, payment_ref) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`close: ${fund_id}`,
|
||||
`Investor: ${floID}`,
|
||||
`End value: ${BTC_net} USD`,
|
||||
`Date of withdrawal: ${dateFormat(end_date)}`,
|
||||
`USD INR rate at end: ${USD_net}`,
|
||||
`Amount withdrawn: Rs ${amount} via ${payment_ref}`,
|
||||
`Reference: ${ref_sign}`
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function parse_details(data) {
|
||||
let funds = {};
|
||||
funds.investments = {};
|
||||
if (!Array.isArray(data))
|
||||
data = [data];
|
||||
data.forEach(fd => {
|
||||
if (!/close: [a-z0-9]{64}\|/.test(fd)) { // not a closing tx
|
||||
let cont = /continue: [a-z0-9]{64}\|/.test(fd);
|
||||
fd.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
if (["invesment(s) (inr)", "investment(s) (inr)"].includes(d[0].toLowerCase()))
|
||||
d[1].split(";").forEach(a => {
|
||||
a = a.split("-");
|
||||
let floID = a[0].replace(/\s/g, ''); //for removing spaces (trailing) if any
|
||||
funds["investments"][floID] = funds["investments"][floID] || {};
|
||||
funds["investments"][floID].amount = parseNumber(a[1])
|
||||
});
|
||||
else if (!cont)
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "start date":
|
||||
funds["start_date"] = new Date(d[1]); break;
|
||||
case "base value":
|
||||
funds["BTC_base"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "usd inr rate at start":
|
||||
funds["USD_base"] = parseFloat(d[1]); break;
|
||||
case "duration":
|
||||
funds["duration"] = parsePeriod(d[1]); break;
|
||||
case "management fee":
|
||||
funds["fee"] = parseFloat(d[1]); break;
|
||||
case "tapout availability":
|
||||
let x = d[1].toLowerCase().split("after")
|
||||
funds["tapoutInterval"] = x[1].match(/\d+ [a-z]+/gi).map(y => parsePeriod(y))
|
||||
funds["topoutWindow"] = parsePeriod(x[0]); break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let floID, details = {};
|
||||
fd.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "investor":
|
||||
floID = d[1]; break;
|
||||
case "end value":
|
||||
details["BTC_net"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "date of withdrawal":
|
||||
details["endDate"] = new Date(d[1]); break;
|
||||
case "amount withdrawn":
|
||||
details["amountFinal"] = parseNumber(d[1].match(/\d.+ via/).toString());
|
||||
details["payment_refRef"] = d[1].match(/via .+/).toString().substring(4); break;
|
||||
case "usd inr rate at end":
|
||||
details["USD_net"] = parseFloat(d[1]); break;
|
||||
case "reference":
|
||||
details["refSign"] = d[1]; break;
|
||||
}
|
||||
});
|
||||
if (floID) {
|
||||
funds.investments[floID] = funds.investments[floID] || {};
|
||||
funds.investments[floID].closed = details;
|
||||
}
|
||||
}
|
||||
});
|
||||
return funds;
|
||||
}
|
||||
|
||||
return {
|
||||
productStr,
|
||||
dateAdder,
|
||||
dateFormat,
|
||||
calcNetValue,
|
||||
parse: parse_details,
|
||||
stringify: {
|
||||
main: stringify_main,
|
||||
continue: stringify_continue,
|
||||
end: stringify_end
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
bobsFund.config = {
|
||||
adminID: "FFXy5pJnfzu2fCDLhpUremyXQjGtFpgCDN",
|
||||
application: "BobsFund"
|
||||
}
|
||||
|
||||
function refreshBlockchainData(nodeList = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT txid FROM LastTx WHERE floID=?", [bobsFund.config.adminID]).then(result => {
|
||||
|
||||
var query_options = {
|
||||
senders: nodeList.concat(bobsFund.config.adminID),
|
||||
tx: true, filter: d => d.startsWith(bobsFund.productStr)
|
||||
};
|
||||
let lastTx = result.length ? result[0].txid : undefined;
|
||||
if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
|
||||
query_options.after = lastTx;
|
||||
else if (!isNaN(lastTx))//lastTx is tx count (*backward support)
|
||||
query_options.ignoreOld = parseInt(lastTx);
|
||||
|
||||
floBlockchainAPI.readData(bobsFund.config.adminID, query_options).then(result => {
|
||||
let txQueries = [];
|
||||
result.items.reverse().forEach(d => {
|
||||
let fund = bobsFund.parse(d.data);
|
||||
if (d.senders.has(bobsFund.config.adminID) && !/close:/.test(d.data)) {
|
||||
let fund_id = d.data.match(/continue: [a-z0-9]{64}\|/);
|
||||
if (!fund_id) {
|
||||
fund_id = d.txid;
|
||||
let values = [fund_id, fund.start_date, fund.BTC_base, fund.USD_base, fund.fee, fund.duration];
|
||||
if (fund.tapoutInterval)
|
||||
values.push(fund.topoutWindow, fund.tapoutInterval.join(','));
|
||||
else
|
||||
values.push(null, null);
|
||||
txQueries.push(["INSERT INTO BobsFund(fund_id, begin_date, btc_base, usd_base, fee, duration, tapout_window, tapout_interval) VALUE (?) ON DUPLICATE KEY UPDATE fund_id=fund_id", [values]])
|
||||
} else
|
||||
fund_id = fund_id.pop().match(/[a-z0-9]{64}/).pop();
|
||||
let investments = Object.entries(fund.investments).map(a => [fund_id, a[0], a[1].amount]);
|
||||
txQueries.push(["INSERT INTO BobsFundInvestments(fund_id, floID, amount_in) VALUES ? ON DUPLICATE KEY UPDATE floID=floID", [investments]]);
|
||||
}
|
||||
else {
|
||||
let fund_id = d.data.match(/close: [a-z0-9]{64}\|/);
|
||||
if (fund_id) {
|
||||
fund_id = fund_id.pop().match(/[a-z0-9]{64}/).pop();
|
||||
let closing_details = Object.entries(fund.investments).filter(a => typeof a[1].closed === "object" && a[1].closed.amountFinal).pop(); //only one close-fund will be there in a tx
|
||||
if (closing_details)
|
||||
txQueries.push(["UPDATE BobsFundInvestments SET close_id=?, amount_out=? WHERE fund_id=? AND floID=?",
|
||||
[d.txid, closing_details[1].closed.amountFinal, fund_id, closing_details[0]]])
|
||||
}
|
||||
}
|
||||
});
|
||||
txQueries.push(["INSERT INTO LastTx (floID, txid) VALUE (?) ON DUPLICATE KEY UPDATE txid=?",
|
||||
[[bobsFund.config.adminID, result.lastItem], result.lastItem]])
|
||||
DB.transaction(txQueries)
|
||||
.then(_ => resolve(result.lastItem))
|
||||
.catch(error => reject(["Bobs-Fund refresh data failed!", error]));
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function closeFund(fund_id, floID, ref) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT r_status, close_id FROM CloseFundTransact WHERE fund_id=? AND floID=?", [fund_id, floID]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, result[0].r_status == pCode.STATUS_SUCCESS ? `Fund investment already closed (${result[0].close_id})` : `Fund closing already in process`));
|
||||
DB.query("SELECT * FROM BobsFund WHERE fund_id=?", [fund_id]).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID(eCode.NOT_FOUND, 'Fund not found'));
|
||||
let fund = result[0];
|
||||
DB.query("SELECT * FROM BobsFundInvestments WHERE fund_id=? AND floID=?", [fund_id, floID]).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID(eCode.NOT_OWNER, 'User is not an investor of this fund'));
|
||||
let investment = result[0];
|
||||
if (investment.close_id)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, `Fund investment already closed (${investment.close_id})`));
|
||||
let cur_date = new Date();
|
||||
if (cur_date < bobsFund.dateAdder(fund.begin_date, fund.duration)) {
|
||||
let flag = false;
|
||||
if (fund.tapout_window && fund.tapout_interval) {
|
||||
let tapout_intervals = fund.tapout_interval.split(",");
|
||||
for (let ti of tapout_intervals) {
|
||||
let t_start = bobsFund.dateAdder(fund.begin_date, ti),
|
||||
t_end = bobsFund.dateAdder(t_start, fund.tapout_window);
|
||||
if (t_start < cur_date && cur_date < t_end) {
|
||||
flag = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!flag)
|
||||
return reject(INVALID(eCode.INSUFFICIENT_PERIOD, 'Fund still in lock-in period'));
|
||||
}
|
||||
getRate.BTC_USD().then(btc_rate => {
|
||||
getRate.USD_INR().then(usd_rate => {
|
||||
let net_value = bobsFund.calcNetValue(fund.btc_base, btc_rate, fund.usd_base, usd_rate, investment.amount_in, fund.fee)
|
||||
DB.query("INSERT INTO CloseFundTransact(fund_id, floID, amount, end_date, btc_net, usd_net, ref_sign, r_status) VALUE (?)", [[fund_id, floID, net_value, cur_date, btc_rate, usd_rate, ref, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve({ "USD_net": usd_rate, "BTC_net": btc_rate, "amount_out": net_value, "end_date": cur_date }))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkFundBalance(prior_time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
prior_time = new Date(prior_time);
|
||||
let cur_date = Date.now();
|
||||
if (isNaN(prior_time) || prior_time.toString() == "Invalid Date")
|
||||
return reject(INVALID(eCode.INVALID_VALUE, `Invalid Date for prior_time`));
|
||||
let sql_query = "SELECT bf.begin_date, bf.btc_base, bf.usd_base, bf.fee, bf.duration, fi.amount_in, cf.amount AS amount_close FROM BobsFund AS bf" +
|
||||
" INNER JOIN BobsFundInvestments AS fi ON bf.fund_id = fi.fund_id" +
|
||||
" LEFT JOIN CloseFundTransact AS cf ON fi.fund_id = cf.fund_id AND fi.floID = cf.floID" +
|
||||
" WHERE fi.close_id IS NULL AND (cf.r_status IS NULL OR cf.r_status NOT IN (?))";
|
||||
DB.query(sql_query, [[pCode.STATUS_SUCCESS, pCode.STATUS_CONFIRMATION]]).then(result => {
|
||||
getRate.BTC_USD().then(btc_rate => {
|
||||
getRate.USD_INR().then(usd_rate => {
|
||||
let pending = { require_amount_cash: 0, n_investment: 0 },
|
||||
ready = { require_amount_cash: 0, n_investment: 0 },
|
||||
upcoming = { require_amount_cash: 0, n_investment: 0 }
|
||||
result.forEach(i => {
|
||||
if (i.amount_close) {
|
||||
pending.require_amount_cash += i.amount_close;
|
||||
pending.n_investment++;
|
||||
} else {
|
||||
let end_date = bobsFund.dateAdder(i.begin_date, i.duration);
|
||||
if (end_date < prior_time) {
|
||||
let net_value = bobsFund.calcNetValue(i.btc_base, btc_rate, i.usd_base, usd_rate, i.amount_in, i.fee);
|
||||
if (end_date > cur_date) {
|
||||
upcoming.require_amount_cash += net_value;
|
||||
upcoming.n_investment++;
|
||||
} else {
|
||||
ready.require_amount_cash += net_value;
|
||||
ready.n_investment++;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
pending.require_amount_cash = global.toStandardDecimal(pending.require_amount_cash);
|
||||
ready.require_amount_cash = global.toStandardDecimal(ready.require_amount_cash);
|
||||
upcoming.require_amount_cash = global.toStandardDecimal(upcoming.require_amount_cash);
|
||||
pending.require_amount_btc = global.toStandardDecimal(pending.require_amount_cash / (btc_rate * usd_rate));
|
||||
ready.require_amount_btc = global.toStandardDecimal(ready.require_amount_cash / (btc_rate * usd_rate));
|
||||
upcoming.require_amount_btc = global.toStandardDecimal(upcoming.require_amount_cash / (btc_rate * usd_rate));
|
||||
Promise.allSettled(sink_chest.list(sink_groups.BOBS_FUND)
|
||||
.map(id => btcOperator.getBalance(btcOperator.convert.legacy2bech(id)))).then(result => {
|
||||
let balance = result.filter(r => r.status === 'fulfilled').reduce((a, bal) => a += bal, 0);
|
||||
resolve({ pending, ready, upcoming, balance });
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refresh(nodeList) {
|
||||
refreshBlockchainData(nodeList)
|
||||
.then(result => console.debug("Refreshed Bob's Fund data"))
|
||||
.catch(error => console.error(error));
|
||||
},
|
||||
util: bobsFund,
|
||||
checkFundBalance,
|
||||
closeFund
|
||||
}
|
||||
314
exchangemarket/src/services/bonds.js
Normal file
314
exchangemarket/src/services/bonds.js
Normal file
@ -0,0 +1,314 @@
|
||||
'use strict';
|
||||
|
||||
const DB = require("../database");
|
||||
const { sink_chest, sink_groups } = require("../keys");
|
||||
const eCode = require('../../docs/scripts/floExchangeAPI').errorCode;
|
||||
const pCode = require('../../docs/scripts/floExchangeAPI').processCode;
|
||||
const getRate = require('./conversion').getRate;
|
||||
|
||||
const blockchainBond = (function () {
|
||||
const productStr = "Product: RanchiMall Bitcoin Bond";
|
||||
|
||||
const magnitude = m => {
|
||||
switch (m) {
|
||||
case "thousand": return 1000;
|
||||
case "lakh": case "lakhs": return 100000;
|
||||
case "million": return 1000000;
|
||||
case "crore": case "crores": return 10000000;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
const parseNumber = (str) => {
|
||||
let n = 0,
|
||||
g = 0;
|
||||
str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => {
|
||||
if (!isNaN(s))
|
||||
g = parseFloat(s);
|
||||
else {
|
||||
let m = magnitude(s);
|
||||
if (m !== null) {
|
||||
n += m * g;
|
||||
g = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
return n + g;
|
||||
}
|
||||
const parsePeriod = (str) => {
|
||||
let P = '', n = 0;
|
||||
str.toLowerCase().replace(/,/g, '').split(" ").forEach(s => {
|
||||
if (!isNaN(s))
|
||||
n = parseFloat(s);
|
||||
else switch (s) {
|
||||
case "year(s)": case "year": case "years": P += (n + 'Y'); n = 0; break;
|
||||
case "month(s)": case "month": case "months": P += (n + 'M'); n = 0; break;
|
||||
case "day(s)": case "day": case "days": P += (n + 'D'); n = 0; break;
|
||||
}
|
||||
});
|
||||
return P;
|
||||
}
|
||||
const dateFormat = (date = null) => {
|
||||
let d = (date ? new Date(date) : new Date()).toDateString();
|
||||
return [d.substring(8, 10), d.substring(4, 7), d.substring(11, 15)].join(" ");
|
||||
}
|
||||
const yearDiff = (d1 = null, d2 = null) => {
|
||||
d1 = d1 ? new Date(d1) : new Date();
|
||||
d2 = d2 ? new Date(d2) : new Date();
|
||||
let y = d1.getYear() - d2.getYear(),
|
||||
m = d1.getMonth() - d2.getMonth(),
|
||||
d = d1.getDate() - d2.getDate()
|
||||
return y + m / 12 + d / 365;
|
||||
}
|
||||
|
||||
const dateAdder = function (start_date, duration) {
|
||||
let date = new Date(start_date);
|
||||
let y = parseInt(duration.match(/\d+Y/)),
|
||||
m = parseInt(duration.match(/\d+M/)),
|
||||
d = parseInt(duration.match(/\d+D/));
|
||||
if (!isNaN(y))
|
||||
date.setFullYear(date.getFullYear() + y);
|
||||
if (!isNaN(m))
|
||||
date.setMonth(date.getMonth() + m);
|
||||
if (!isNaN(d))
|
||||
date.setDate(date.getDate() + d);
|
||||
return date;
|
||||
}
|
||||
|
||||
function calcNetValue(BTC_base, BTC_net, startDate, minIpa, maxPeriod, cut, amount, USD_base, USD_net) {
|
||||
let gain, duration, interest, net;
|
||||
gain = (BTC_net - BTC_base) / BTC_base;
|
||||
duration = yearDiff(Math.min(Date.now(), dateAdder(startDate, maxPeriod).getTime()), startDate);
|
||||
interest = Math.max(cut * gain, minIpa * duration);
|
||||
net = amount / USD_base;
|
||||
net += net * interest;
|
||||
return net * USD_net;
|
||||
}
|
||||
|
||||
function stringify_main(BTC_base, start_date, guaranteed_interest, guarantee_period, gain_cut, amount, USD_base, lockin_period, floID) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`Base value: ${BTC_base} USD`,
|
||||
`Date of bond start: ${dateFormat(start_date)}`,
|
||||
`Guaranteed interest: ${guaranteed_interest}% per annum simple for ${guarantee_period}`,
|
||||
`Bond value: guaranteed interest or ${gain_cut}% of the gains whichever is higher`,
|
||||
`Amount invested: Rs ${amount}`,
|
||||
`USD INR rate at start: ${USD_base}`,
|
||||
`Lockin period: ${lockin_period}`,
|
||||
`FLO ID of Bond Holder: ${floID}`
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function parse_main(data) {
|
||||
//Data (add bond) sent by admin
|
||||
let details = {};
|
||||
data.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "base value":
|
||||
details["BTC_base"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "date of bond start":
|
||||
details["startDate"] = new Date(d[1]); break;
|
||||
case "guaranteed interest":
|
||||
details["minIpa"] = parseFloat(d[1].match(/\d+%/)) / 100;
|
||||
details["maxPeriod"] = parsePeriod(d[1].match(/for .+/).toString()); break;
|
||||
case "bond value":
|
||||
details["cut"] = parseFloat(d[1].match(/\d+%/)) / 100; break;
|
||||
case "amount invested":
|
||||
details["amount"] = parseNumber(d[1].substring(3)); break;
|
||||
case "usd inr rate at start":
|
||||
details["USD_base"] = parseFloat(d[1]); break;
|
||||
case "lockin period":
|
||||
details["lockinPeriod"] = parsePeriod(d[1]); break;
|
||||
case "flo id of bond holder":
|
||||
details["floID"] = d[1]; break;
|
||||
}
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
function stringify_end(bond_id, end_date, BTC_net, USD_net, amount, ref_sign, payment_ref) {
|
||||
return [
|
||||
`${productStr}`,
|
||||
`Bond: ${bond_id}`,
|
||||
`End value: ${BTC_net} USD`,
|
||||
`Date of bond end: ${dateFormat(end_date)}`,
|
||||
`USD INR rate at end: ${USD_net}`,
|
||||
`Amount withdrawn: Rs ${amount} via ${payment_ref}`,
|
||||
`Reference: ${ref_sign}`
|
||||
].join("|");
|
||||
}
|
||||
|
||||
function parse_end(data) {
|
||||
//Data (end bond) send by market nodes
|
||||
let details = {};
|
||||
data.split("|").forEach(d => {
|
||||
d = d.split(': ');
|
||||
switch (d[0].toLowerCase()) {
|
||||
case "bond":
|
||||
details["bondID"] = d[1]; break;
|
||||
case "end value":
|
||||
details["BTC_net"] = parseNumber(d[1].slice(0, -4)); break;
|
||||
case "date of bond end":
|
||||
details["endDate"] = new Date(d[1]); break;
|
||||
case "amount withdrawn":
|
||||
details["amountFinal"] = parseNumber(d[1].match(/\d.+ via/).toString());
|
||||
details["payment_refRef"] = d[1].match(/via .+/).toString().substring(4); break;
|
||||
case "usd inr rate at end":
|
||||
details["USD_net"] = parseFloat(d[1]); break;
|
||||
case "reference":
|
||||
details["refSign"] = d[1]; break;
|
||||
}
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
return {
|
||||
productStr,
|
||||
dateAdder,
|
||||
dateFormat,
|
||||
calcNetValue,
|
||||
parse: {
|
||||
main: parse_main,
|
||||
end: parse_end
|
||||
},
|
||||
stringify: {
|
||||
main: stringify_main,
|
||||
end: stringify_end
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
blockchainBond.config = {
|
||||
adminID: "FBBstZ2GretgQqDP55yt8iVd4KNZkdvEzH",
|
||||
application: "BlockchainBonds"
|
||||
}
|
||||
|
||||
function refreshBlockchainData(nodeList = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT txid FROM LastTx WHERE floID=?", [blockchainBond.config.adminID]).then(result => {
|
||||
|
||||
var query_options = {
|
||||
senders: nodeList.concat(blockchainBond.config.adminID),
|
||||
tx: true, filter: d => d.startsWith(blockchainBond.productStr)
|
||||
};
|
||||
let lastTx = result.length ? result[0].txid : undefined;
|
||||
if (typeof lastTx == 'string' && /^[0-9a-f]{64}/i.test(lastTx))//lastTx is txid of last tx
|
||||
query_options.after = lastTx;
|
||||
else if (!isNaN(lastTx))//lastTx is tx count (*backward support)
|
||||
query_options.ignoreOld = parseInt(lastTx);
|
||||
|
||||
floBlockchainAPI.readData(blockchainBond.config.adminID, query_options).then(result => {
|
||||
let txQueries = [];
|
||||
result.items.reverse().forEach(d => {
|
||||
let bond = d.senders.has(blockchainBond.config.adminID) ? blockchainBond.parse.main(d.data) : null;
|
||||
if (bond && bond.amount)
|
||||
txQueries.push(["INSERT INTO BlockchainBonds(bond_id, floID, amount_in, begin_date, btc_base, usd_base, gain_cut, min_ipa, max_period, lockin_period) VALUE (?) ON DUPLICATE KEY UPDATE bond_id=bond_id",
|
||||
[[d.txid, bond.floID, bond.amount, bond.startDate, bond.BTC_base, bond.USD_base, bond.cut, bond.minIpa, bond.maxPeriod, bond.lockinPeriod]]]);
|
||||
else {
|
||||
let details = blockchainBond.parse.end(d.data);
|
||||
if (details.bondID && details.amountFinal)
|
||||
txQueries.push(["UPDATE BlockchainBonds SET close_id=?, amount_out=? WHERE bond_id=?",
|
||||
[d.txid, details.amountFinal, details.bondID]]);
|
||||
}
|
||||
});
|
||||
txQueries.push(["INSERT INTO LastTx (floID, txid) VALUE (?) ON DUPLICATE KEY UPDATE txid=?",
|
||||
[[blockchainBond.config.adminID, result.lastItem], result.lastItem]])
|
||||
DB.transaction(txQueries)
|
||||
.then(_ => resolve(result.lastItem))
|
||||
.catch(error => reject(["Blockchain-bonds refresh data failed!", error]));
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function closeBond(bond_id, floID, ref) {
|
||||
return new Promise((resolve, reject) => {
|
||||
DB.query("SELECT r_status, close_id FROM CloseBondTransact WHERE bond_id=?", [bond_id]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, result[0].r_status == pCode.STATUS_SUCCESS ? `Bond already closed (${result[0].close_id})` : `Bond closing already in process`));
|
||||
DB.query("SELECT * FROM BlockchainBonds WHERE bond_id=?", [bond_id]).then(result => {
|
||||
if (!result.length)
|
||||
return reject(INVALID(eCode.NOT_FOUND, 'Bond not found'));
|
||||
let bond = result[0];
|
||||
if (bond.floID !== floID)
|
||||
return reject(INVALID(eCode.NOT_OWNER, 'Bond doesnot belong to the user'));
|
||||
if (bond.close_id)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, `Bond already closed (${bond.close_id})`));
|
||||
if (Date.now() < blockchainBond.dateAdder(bond.begin_date, bond.lockin_period).getTime())
|
||||
return reject(INVALID(eCode.INSUFFICIENT_PERIOD, 'Bond still in lock-in period'));
|
||||
getRate.BTC_USD().then(btc_rate => {
|
||||
getRate.USD_INR().then(usd_rate => {
|
||||
let end_date = new Date(),
|
||||
net_value = blockchainBond.calcNetValue(bond.btc_base, btc_rate, bond.begin_date, bond.min_ipa, bond.max_period, bond.gain_cut, bond.amount_in, bond.usd_base, usd_rate);
|
||||
DB.query("INSERT INTO CloseBondTransact(bond_id, floID, amount, end_date, btc_net, usd_net, ref_sign, r_status) VALUE (?)", [[bond_id, floID, net_value, end_date, btc_rate, usd_rate, ref, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve({ "USD_net": usd_rate, "BTC_net": btc_rate, "amount_out": net_value, "end_date": end_date }))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkBondBalance(prior_time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
prior_time = new Date(prior_time);
|
||||
let cur_date = Date.now();
|
||||
if (isNaN(prior_time) || prior_time.toString() == "Invalid Date")
|
||||
return reject(INVALID(eCode.INVALID_VALUE, `Invalid Date for prior_time`));
|
||||
let sql_query = "SELECT bb.*, cb.amount AS amount_close FROM BlockchainBonds AS bb" +
|
||||
" LEFT JOIN CloseBondTransact AS cb ON bb.bond_id = cb.bond_id" +
|
||||
" WHERE bb.close_id IS NULL AND (cb.r_status IS NULL OR cb.r_status NOT IN (?))";
|
||||
DB.query(sql_query, [[pCode.STATUS_SUCCESS, pCode.STATUS_CONFIRMATION]]).then(result => {
|
||||
getRate.BTC_USD().then(btc_rate => {
|
||||
getRate.USD_INR().then(usd_rate => {
|
||||
let pending = { require_amount_cash: 0, n_bond: 0 },
|
||||
ready = { require_amount_cash: 0, n_bond: 0 },
|
||||
upcoming = { require_amount_cash: 0, n_bond: 0 }
|
||||
result.forEach(bond => {
|
||||
if (bond.amount_close) {
|
||||
pending.require_amount_cash += bond.amount_close;
|
||||
pending.n_bond++;
|
||||
} else {
|
||||
let end_date = blockchainBond.dateAdder(bond.begin_date, bond.lockin_period)
|
||||
if (end_date < prior_time) {
|
||||
let net_value = blockchainBond.calcNetValue(bond.btc_base, btc_rate, bond.begin_date, bond.min_ipa, bond.max_period, bond.gain_cut, bond.amount_in, bond.usd_base, usd_rate);
|
||||
if (end_date > cur_date) {
|
||||
upcoming.require_amount_cash += net_value;
|
||||
upcoming.n_bond++;
|
||||
} else {
|
||||
ready.require_amount_cash += net_value;
|
||||
ready.n_bond++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
pending.require_amount_cash = global.toStandardDecimal(pending.require_amount_cash);
|
||||
ready.require_amount_cash = global.toStandardDecimal(ready.require_amount_cash);
|
||||
upcoming.require_amount_cash = global.toStandardDecimal(upcoming.require_amount_cash);
|
||||
pending.require_amount_btc = global.toStandardDecimal(pending.require_amount_cash / (btc_rate * usd_rate));
|
||||
ready.require_amount_btc = global.toStandardDecimal(ready.require_amount_cash / (btc_rate * usd_rate));
|
||||
upcoming.require_amount_btc = global.toStandardDecimal(upcoming.require_amount_cash / (btc_rate * usd_rate));
|
||||
Promise.allSettled(sink_chest.list(sink_groups.BLOCKCHAIN_BONDS)
|
||||
.map(id => btcOperator.getBalance(btcOperator.convert.legacy2bech(id)))).then(result => {
|
||||
let balance = result.filter(r => r.status === 'fulfilled').reduce((a, bal) => a += bal, 0);
|
||||
resolve({ pending, ready, upcoming, balance });
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
refresh(nodeList) {
|
||||
refreshBlockchainData(nodeList)
|
||||
.then(result => console.debug("Refreshed Blockchain-bonds data"))
|
||||
.catch(error => console.error(error));
|
||||
},
|
||||
util: blockchainBond,
|
||||
checkBondBalance,
|
||||
closeBond
|
||||
}
|
||||
283
exchangemarket/src/services/conversion.js
Normal file
283
exchangemarket/src/services/conversion.js
Normal file
@ -0,0 +1,283 @@
|
||||
'use strict';
|
||||
|
||||
const DB = require("../database");
|
||||
const eCode = require('../../docs/scripts/floExchangeAPI').errorCode;
|
||||
const pCode = require('../../docs/scripts/floExchangeAPI').processCode;
|
||||
|
||||
const {
|
||||
MIN_FUND,
|
||||
TO_FIXED_VALUES,
|
||||
TO_MAX_VALUE,
|
||||
TO_MIN_VALUE,
|
||||
FROM_FIXED_VALUES,
|
||||
FROM_MAX_VALUE,
|
||||
FROM_MIN_VALUE
|
||||
} = require('../_constants')['convert'];
|
||||
|
||||
const allowedConversion = ["BTC"];
|
||||
|
||||
function BTC_INR() {
|
||||
return new Promise((resolve, reject) => {
|
||||
BTC_USD().then(btc_usd => {
|
||||
USD_INR().then(usd_inr => {
|
||||
resolve(btc_usd * usd_inr);
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function BTC_USD() {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
function USD_INR() {
|
||||
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 getPoolAvailability(coin) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
let q = "SELECT mode, SUM(quantity) AS coin_val, SUM(amount) AS cash_val FROM (" +
|
||||
"(SELECT amount, coin, quantity, mode, r_status FROM DirectConvert) UNION " +
|
||||
"(SELECT amount, coin, quantity, mode, r_status FROM ConvertFund) " +
|
||||
") AS T1 WHERE T1.coin=? AND T1.r_status NOT IN (?) GROUP BY T1.mode";
|
||||
DB.query(q, [coin, [pCode.STATUS_REJECTED]]).then(result => {
|
||||
let coin_net = 0, cash_net = 0;
|
||||
for (let r of result)
|
||||
if (r.mode == pCode.CONVERT_MODE_GET) {
|
||||
coin_net -= r.coin_val;
|
||||
cash_net += r.cash_val;
|
||||
} else if (r.mode == pCode.CONVERT_MODE_PUT) {
|
||||
coin_net += r.coin_val;
|
||||
cash_net -= r.cash_val;
|
||||
}
|
||||
BTC_INR().then(rate => {
|
||||
coin_net = coin_net * rate;
|
||||
let cash_availability = cash_net - coin_net * MIN_FUND,
|
||||
coin_availability = (coin_net - cash_net * MIN_FUND) / rate;
|
||||
if (cash_availability < 0) cash_availability = 0;
|
||||
if (coin_availability < 0) coin_availability = 0;
|
||||
resolve({ cash: cash_availability, coin: coin_availability, rate })
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function checkPoolBalance(coin, req_value, mode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getPoolAvailability(coin).then(result => {
|
||||
let availability = -1;
|
||||
if (mode == pCode.CONVERT_MODE_GET) {
|
||||
availability = result.coin;
|
||||
req_value = req_value / result.rate; //convert to coin value
|
||||
}
|
||||
else if (mode == pCode.CONVERT_MODE_PUT) {
|
||||
availability = result.cash;
|
||||
req_value = req_value * result.rate; //convert to currency value
|
||||
}
|
||||
if (req_value > availability)
|
||||
reject(INVALID(eCode.INSUFFICIENT_FUND, `Insufficient convert! Availability: ${availability > 0 ? availability : 0}`));
|
||||
else
|
||||
resolve(true);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function getConvertValues() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getPoolAvailability("BTC").then(avail => {
|
||||
let result = {};
|
||||
if (avail.coin > 0) {
|
||||
let coin_availability = global.toStandardDecimal(avail.coin * avail.rate); //convert to currency value
|
||||
if (Array.isArray(TO_FIXED_VALUES) && TO_FIXED_VALUES.length)
|
||||
result[pCode.CONVERT_MODE_GET] = TO_FIXED_VALUES.filter(a => a < coin_availability);
|
||||
else if (!TO_MIN_VALUE || TO_MIN_VALUE <= coin_availability) {
|
||||
result[pCode.CONVERT_MODE_GET] = { min: 0 };
|
||||
result[pCode.CONVERT_MODE_GET].max = (!TO_MAX_VALUE || TO_MAX_VALUE >= coin_availability) ? coin_availability : TO_MAX_VALUE;
|
||||
}
|
||||
} else result[pCode.CONVERT_MODE_GET] = null;
|
||||
if (avail.cash > 0) {
|
||||
let cash_availability = global.toStandardDecimal(avail.cash / avail.rate); //convert to coin value
|
||||
if (Array.isArray(FROM_FIXED_VALUES) && FROM_FIXED_VALUES.length)
|
||||
result[pCode.CONVERT_MODE_PUT] = FROM_FIXED_VALUES.filter(a => a < cash_availability);
|
||||
else if (!FROM_MIN_VALUE || FROM_MIN_VALUE <= cash_availability) {
|
||||
result[pCode.CONVERT_MODE_PUT] = { min: 0 };
|
||||
result[pCode.CONVERT_MODE_PUT].max = (!FROM_MAX_VALUE || FROM_MAX_VALUE >= cash_availability) ? cash_availability : FROM_MAX_VALUE;
|
||||
}
|
||||
} else result[pCode.CONVERT_MODE_PUT] = null;
|
||||
resolve(result)
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function convertToCoin(floID, txid, coin, amount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
else if (typeof amount !== "number" || amount <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
|
||||
else if (Array.isArray(TO_FIXED_VALUES) && TO_FIXED_VALUES.length) {
|
||||
if (!TO_FIXED_VALUES.includes(amount))
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
|
||||
} else if (TO_MIN_VALUE && TO_MIN_VALUE > amount || TO_MAX_VALUE && TO_MAX_VALUE < amount)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid amount (${amount})`));
|
||||
DB.query("SELECT r_status FROM DirectConvert WHERE in_txid=? AND floID=? AND mode=?", [txid, floID, pCode.CONVERT_MODE_GET]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
checkPoolBalance(coin, amount, pCode.CONVERT_MODE_GET).then(result => {
|
||||
DB.query("INSERT INTO DirectConvert(floID, in_txid, mode, coin, amount, r_status) VALUES (?)", [[floID, txid, pCode.CONVERT_MODE_GET, coin, amount, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Conversion request in process"))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => {
|
||||
if (error instanceof INVALID && error.ecode === eCode.INSUFFICIENT_FUND)
|
||||
DB.query("INSERT INTO DirectConvert(floID, in_txid, mode, coin, amount, r_status) VALUES (?)", [[floID, txid, pCode.CONVERT_MODE_GET, coin, amount, pCode.STATUS_REJECTED]]).then(result => {
|
||||
DB.query("INSERT INTO RefundConvert(floID, in_txid, asset_type, asset, r_status) VALUES (?)", [[floID, txid, pCode.ASSET_TYPE_TOKEN, floGlobals.currency, pCode.STATUS_PENDING]])
|
||||
.then(_ => null).catch(error => console.error(error));
|
||||
}).catch(error => console.error(error))
|
||||
reject(error);
|
||||
})
|
||||
}).catch(error => reject(error))
|
||||
});
|
||||
}
|
||||
|
||||
function convertFromCoin(floID, txid, tx_hex, coin, quantity) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
else if (typeof quantity !== "number" || quantity <= 0)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
|
||||
else if (Array.isArray(FROM_FIXED_VALUES) && FROM_FIXED_VALUES.length) {
|
||||
if (!FROM_FIXED_VALUES.includes(quantity))
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
|
||||
} else if (FROM_MIN_VALUE && FROM_MIN_VALUE > quantity || FROM_MAX_VALUE && FROM_MAX_VALUE < quantity)
|
||||
return reject(INVALID(eCode.INVALID_NUMBER, `Invalid quantity (${quantity})`));
|
||||
else if (btcOperator.transactionID(tx_hex) !== txid)
|
||||
return reject(INVALID(eCode.INVALID_TX_ID, `txid ${txid} doesnt match the tx-hex`));
|
||||
DB.query("SELECT r_status FROM DirectConvert WHERE in_txid=? AND floID=? AND mode=?", [txid, floID, pCode.CONVERT_MODE_PUT]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
checkPoolBalance(coin, quantity, pCode.CONVERT_MODE_PUT).then(result => {
|
||||
btcOperator.broadcastTx(tx_hex).then(b_txid => {
|
||||
if (b_txid !== txid)
|
||||
console.warn("broadcast TX-ID is not same as calculated TX-ID");
|
||||
DB.query("INSERT INTO DirectConvert(floID, in_txid, mode, coin, quantity, r_status) VALUES (?)", [[floID, b_txid, pCode.CONVERT_MODE_PUT, coin, quantity, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Conversion request in process"))
|
||||
.catch(error => reject(error));
|
||||
}).catch(error => {
|
||||
if (error === null)
|
||||
reject(INVALID(eCode.INVALID_TX_ID, `Invalid transaction hex`));
|
||||
else
|
||||
reject(error);
|
||||
})
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function depositCurrencyFund(floID, txid, coin) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (floID !== floGlobals.adminID)
|
||||
return reject(INVALID(eCode.ACCESS_DENIED, 'Access Denied'));
|
||||
else if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
DB.query("SELECT r_status FROM ConvertFund WHERE txid=? AND mode=?", [txid, pCode.CONVERT_MODE_GET]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
DB.query("INSERT INTO ConvertFund(txid, mode, coin, r_status) VALUES (?)", [[txid, pCode.CONVERT_MODE_GET, coin, pCode.STATUS_PROCESSING]])
|
||||
.then(result => resolve("Deposit currency fund in process"))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function depositCoinFund(floID, txid, coin) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (floID !== floGlobals.adminID)
|
||||
return reject(INVALID(eCode.ACCESS_DENIED, 'Access Denied'));
|
||||
else if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
DB.query("SELECT r_status FROM ConvertFund WHERE txid=? AND mode=?", [txid, pCode.CONVERT_MODE_PUT]).then(result => {
|
||||
if (result.length)
|
||||
return reject(INVALID(eCode.DUPLICATE_ENTRY, "Transaction already in process"));
|
||||
DB.query("INSERT INTO ConvertFund(txid, mode, coin, r_status) VALUES (?)", [[txid, pCode.CONVERT_MODE_PUT, coin, pCode.STATUS_PROCESSING]])
|
||||
.then(result => resolve("Deposit coin fund in process"))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function withdrawCurrencyFund(floID, coin, amount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (floID !== floGlobals.adminID)
|
||||
return reject(INVALID(eCode.ACCESS_DENIED, 'Access Denied'));
|
||||
else if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
DB.query("SELECT SUM(amount) AS deposit_amount FROM ConvertFund WHERE mode=? AND r_status=?", [pCode.CONVERT_MODE_GET, pCode.STATUS_SUCCESS]).then(r1 => {
|
||||
DB.query("SELECT SUM(amount) AS withdraw_amount FROM ConvertFund WHERE mode=? AND r_status IN (?)", [pCode.CONVERT_MODE_PUT, [pCode.STATUS_SUCCESS, pCode.STATUS_PENDING, pCode.STATUS_CONFIRMATION]]).then(r2 => {
|
||||
let available_amount = (r1[0].deposit_amount || 0) - (r2[0].withdraw_amount || 0);
|
||||
if (available_amount < amount)
|
||||
return reject(INVALID(eCode.INSUFFICIENT_BALANCE, "Insufficient convert-fund deposits to withdraw"));
|
||||
DB.query("INSERT INTO ConvertFund(mode, coin, amount, r_status) VALUES (?)", [[pCode.CONVERT_MODE_PUT, coin, amount, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Withdraw currency fund in process"))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
function withdrawCoinFund(floID, coin, quantity) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (floID !== floGlobals.adminID)
|
||||
return reject(INVALID(eCode.ACCESS_DENIED, 'Access Denied'));
|
||||
else if (!allowedConversion.includes(coin))
|
||||
return reject(INVALID(eCode.INVALID_TOKEN_NAME, `Invalid coin (${coin})`));
|
||||
DB.query("SELECT SUM(quantity) AS deposit_quantity FROM ConvertFund WHERE mode=? AND r_status=?", [pCode.CONVERT_MODE_PUT, pCode.STATUS_SUCCESS]).then(r1 => {
|
||||
DB.query("SELECT SUM(quantity) AS withdraw_quantity FROM ConvertFund WHERE mode=? AND r_status IN (?)", [pCode.CONVERT_MODE_GET, [pCode.STATUS_SUCCESS, pCode.STATUS_PENDING]]).then(r2 => {
|
||||
let available_quantity = (r1[0].deposit_quantity || 0) - (r2[0].withdraw_quantity || 0);
|
||||
if (available_quantity < quantity)
|
||||
return reject(INVALID(eCode.INSUFFICIENT_BALANCE, "Insufficient convert-fund deposits to withdraw"));
|
||||
DB.query("INSERT INTO ConvertFund(mode, coin, quantity, r_status) VALUES (?)", [[pCode.CONVERT_MODE_GET, coin, quantity, pCode.STATUS_PENDING]])
|
||||
.then(result => resolve("Withdraw currency fund in process"))
|
||||
.catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRate: {
|
||||
BTC_USD,
|
||||
USD_INR,
|
||||
BTC_INR
|
||||
},
|
||||
getConvertValues,
|
||||
convertToCoin,
|
||||
convertFromCoin,
|
||||
depositFund: {
|
||||
coin: depositCoinFund,
|
||||
currency: depositCurrencyFund
|
||||
},
|
||||
withdrawFund: {
|
||||
coin: withdrawCoinFund,
|
||||
currency: withdrawCurrencyFund
|
||||
}
|
||||
}
|
||||
28
exchangemarket/src/set_globals.js
Normal file
28
exchangemarket/src/set_globals.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
//fetch for node js (used in floBlockchainAPI.js)
|
||||
global.fetch = require("node-fetch");
|
||||
|
||||
//Set browser paramaters from param.json (or param-default.json)
|
||||
var param;
|
||||
try {
|
||||
param = require('../args/param.json');
|
||||
} catch {
|
||||
param = require('../args/param-default.json');
|
||||
} finally {
|
||||
for (let p in param)
|
||||
global[p] = param[p];
|
||||
}
|
||||
|
||||
global.toStandardDecimal = num => parseFloat((parseInt(num * 1e8) * 1e-8).toFixed(8))
|
||||
|
||||
if (!process.argv.includes("--debug"))
|
||||
global.console.debug = () => null;
|
||||
|
||||
/*
|
||||
//Trace the debug logs in node js
|
||||
var debug = console.debug;
|
||||
console.debug = function() {
|
||||
debug.apply(console, arguments);
|
||||
console.trace();
|
||||
};
|
||||
*/
|
||||
2
exchangemarket/start.js
Normal file
2
exchangemarket/start.js
Normal file
@ -0,0 +1,2 @@
|
||||
const start = require('./src/main');
|
||||
start();
|
||||
Loading…
Reference in New Issue
Block a user