Initial commit, moving from private repo. Welcome to the light.

master
Alexander Blair 7 years ago
commit fd58d56ac5

77
.gitignore vendored

@ -0,0 +1,77 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
#NodeJs Ignores
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
config.json

@ -0,0 +1,175 @@
Setup Instructions
==================
Server Requirements
-------------------
* 4 Gb Ram
* 2 CPU Cores
* 60 Gb SSD-Backed Storage - If you're doing a multi-server install, the leaf nodes do not need this much storage. They just need enough storage to hold the blockchain for your node. The pool comes configured to use up to 24Gb of storage for LMDB. Assuming you have the longRunner worker running, it should never get near this size, but be aware that it /can/ bloat readily if things error, so be ready for this!
* Notably, this happens to be approximately the size of a 4Gb linode instance, which is where the majority of automated deployment testing happened!
Pre-Deploy
----------
* If you're planning on using e-mail, you'll want to setup an account at https://mailgun.com (It's free for 10k e-mails/month!), so you can notify miners. This also serves as the backend for password reset emails, along with other sorts of e-mails from the pool, including pool startup, pool Monerod daemon lags, etc so it's highly suggested!
* Pre-Generate the wallets, or don't, it's up to you! You'll need the addresses after the install is complete, so I'd suggest making sure you have them available. Information on suggested setups are found below.
* If you're going to be offering PPS, PLEASE make sure you load the pool wallet with XMR before you get too far along. Your pool will trigger PPS payments on it's own, and fairly readily, so you need some float in there!
* Make a non-root user, and run the installer from there!
Deployment via Installer
------------------------
1. Add your user to /etc/sudoers, this must be done so the script can sudo up and do it's job. We suggest passwordless sudo. Suggested line: <USER> ALL=(ALL) NOPASSWD:ALL. Our sample builds use: pooldaemon ALL=(ALL) NOPASSWD:ALL
2. Run the deploy script as a NON-ROOT USER. This is very important! This script will install the pool to whatever user it's running under! Also. Go get a coffee, this sucker bootstraps the monero installation
3. Once it's complete, copy config_example.json to config.json and change as appropriate. It is pre-loaded for a local install of everything, running on 127.0.0.1. This will work perfectly fine if you're using a single node setup.
4. You'll need to change the API end point for the frontend code in the xmrpoolui folder, under app/utils/services.js -- This will usually be http://<your server ip>/api unless you tweak caddy!
5. Change the path in config.json for your database directory to: /home/<username>/pool_db/ The directory's already been created during startup. Or change as appropriate! Just make sure your user has write permissions, then run: pm2 restart api to reload the API for usage
6. Hop into the web interface (Should be at http://<your server IP>/#/admin), then login with Administrator/Password123, MAKE SURE TO CHANGE THIS PASSWORD ONCE YOU LOGIN.
7. From the admin panel, you can configure all of your pool's settings for addresses, payment thresholds, etc.
8. Once you're happy with the settings, go ahead and start all the pool daemons, commands follow.
```bash
pm2 start init.js --name=blockManager -- --module=blockManager
pm2 start init.js --name=worker -- --module=worker
pm2 start init.js --name=payments -- --module=payments
pm2 start init.js --name=remoteShare -- --module=remoteShare
pm2 start init.js --name=longRunner -- --module=longRunner
pm2 start init.js --name=pool -- --module=pool
pm2 restart api
```
Assumptions for the installer
-----------------------------
The installer assumes that you will be running a single-node instance and using a clean Ubuntu 16.04 server install. The following system defaults are set:
* MySQL Username: pool
* MySQL Password: 98erhfiuehw987fh23d
* MySQL Host: 127.0.0.1
* MySQL root access is only permitted as the root user, the password is in /root/.my.cnf
* SSL Certificate is generated, self-signed, but is valid for Claymore Miners.
* The server installs and deploys Caddy as it's choice of webserver!
The following raw binaries MUST BE AVAILABLE FOR IT TO BOOTSTRAP:
* sudo
I've confirmed that the default server 16.04 installation has these requirements.
The pool comes pre-configured with values for Monero (XMR), these may need to be changed depending on the exact requirements of your coin. Other coins will likely be added down the road, and most likely will have configuration.sqls provided to overwrite the base configurations for their needs, but can be configured within the frontend as well.
The pool ALSO applies a series of patches: Fluffy Blocks, Additional Open P2P Connections, 128 Txn Bug Fix. If you don't like these, replace the auto-installed monerod fixes!
Wallet Setup
------------
The pool is designed to have a dual-wallet design, one which is a fee wallet, one which is the live pool wallet. The fee wallet is the default target for all fees owed to the pool owner. PM2 can also manage your wallet daemon, and that is the suggested run state.
1. Generate your wallets using /usr/local/src/monero/build/release/bin/monero-wallet-cli
2. Make sure to save your regeneration stuff!
3. For the pool wallet, store the password in a file, the suggestion is ~/wallet_pass
4. Change the mode of the file with chmod to 0400: chmod 0400 ~/wallet_pass
5. Start the wallet using PM2: pm2 start /usr/local/src/monero/build/release/bin/monero-wallet-rpc -- --rpc-bind-port 18082 --password-file ~/wallet_pass --wallet-file <Your wallet name here>
6. If you don't use PM2, then throw the wallet into a screen and have fun.
Manual Setup
------------
Pretty similar to the above, you may wish to dig through a few other things for sanity sake, but the installer scripts should give you a good idea of what to expect from the ground up.
Manual SQL Configuration
------------------------
Until the full frontend is released, the following SQL information needs to be updated by hand in order to bring your pool online, in module/item format. You can also edit the values in sample_config.sql, then import them into SQL directly via an update.
```
Critical/Must be done:
pool/address
pool/feeAddress
general/shareHost
Nice to have:
general/mailgunKey
general/mailgunURL
general/emailFrom
SQL import command: sudo mysql pool < ~/nodejs-pool/sample_config.sql (Adjust name/path as needed!)
```
Additional ports can be added as desired, samples can be found at the end of base.sql. If you're not comfortable with the MySQL command line, I highly suggest MySQL Workbench or a similar piece of software (I use datagrip!). Your root MySQL password can be found in /root/.my.cnf
Pool Troubleshooting
====================
API stopped updating!
---------------------
This is likely due to LMDB's MDB_SIZE being hit, or due to LMDB locking up due to a reader staying open too long, possibly due to a software crash.
The first step is to run:
```
mdb_stat -fear ~/pool_db/
```
This should give you output like:
```
Environment Info
Map address: (nil)
Map size: 51539607552
Page size: 4096
Max pages: 12582912
Number of pages used: 12582904
Last transaction ID: 74988258
Max readers: 512
Number of readers used: 24
Reader Table Status
pid thread txnid
25763 7f4f0937b740 74988258
Freelist Status
Tree depth: 3
Branch pages: 135
Leaf pages: 29917
Overflow pages: 35
Entries: 591284
Free pages: 12234698
Status of Main DB
Tree depth: 1
Branch pages: 0
Leaf pages: 1
Overflow pages: 0
Entries: 3
Status of blocks
Tree depth: 1
Branch pages: 0
Leaf pages: 1
Overflow pages: 0
Entries: 23
Status of cache
Tree depth: 3
Branch pages: 16
Leaf pages: 178
Overflow pages: 2013
Entries: 556
Status of shares
Tree depth: 2
Branch pages: 1
Leaf pages: 31
Overflow pages: 0
Entries: 4379344
```
The important thing to verify here is that the "Number of pages used" value is less than the "Max Pages" value, and that there are "Free pages" under "Freelist Status". If this is the case, them look at the "Reader Table Status" and look for the PID listed. Run:
```
ps fuax | grep <THE PID FROM ABOVE>
ex:
ps fuax | grep 25763
```
If the output is not blank, then one of your node processes is reading, this is fine. If there is no output given on one of them, then proceed forwards.
The second step is to run:
```
pm2 stop blockManager worker payments remoteShare longRunner api
pm2 start blockManager worker payments remoteShare longRunner api
```
This will restart all of your related daemons, and will clear any open reader connections, allowing LMDB to get back to a normal state.
If on the other hand, you have no "Free pages" and your Pages used is equal to the Max Pages, then you've run out of disk space for LMDB. You need to verify the cleaner is working. For reference, 4.3 million shares are stored within approximately 2-3 Gb of space, so if you're vastly exceeding this, then your cleaner (longRunner) is likely broken.
Credits
=======
[Zone117x](https://github.com/zone117x) - Original [node-cryptonote-pool](https://github.com/zone117x/node-cryptonote-pool) from which, the stratum implementation has been borrowed.
[Mesh00](https://github.com/hackfanatic) - Frontend build in Angular JS [XMRPoolUI](https://github.com/hackfanatic/xmrpoolui)
[Wolf0](https://github.com/wolf9466/)/[OhGodAGirl](https://github.com/ohgodagirl) - Rebuild of node-multi-hashing with AES-NI [node-multi-hashing](https://github.com/Snipa22/node-multi-hashing-aesni)

@ -0,0 +1,11 @@
{
"xmr": {
"funcFile": "./lib/coins/xmr.js",
"sigDigits": 1000000000000,
"shapeshift": "xmr_btc",
"xmrTo": true,
"name": "Monero",
"mixIn": 4,
"shortCode": "XMR"
}
}

@ -0,0 +1,14 @@
{
"pool_id": 0,
"bind_ip": "127.0.0.1",
"hostname": "testpool.com",
"db_storage_path": "CHANGEME",
"coin": "xmr",
"mysql": {
"connectionLimit": 20,
"host": "127.0.0.1",
"database": "pool",
"user": "pool",
"password": "98erhfiuehw987fh23d"
}
}

@ -0,0 +1,91 @@
"use strict";
let mysql = require("promise-mysql");
let fs = require("fs");
let argv = require('minimist')(process.argv.slice(2));
let config = fs.readFileSync("../config.json");
let coinConfig = fs.readFileSync("../coinConfig.json");
let protobuf = require('protocol-buffers');
const request = require('request');
global.support = require("../lib/support.js")();
global.config = JSON.parse(config);
global.mysql = mysql.createPool(global.config.mysql);
global.protos = protobuf(fs.readFileSync('../lib/data.proto'));
let comms;
let coinInc;
// Config Table Layout
// <module>.<item>
global.mysql.query("SELECT * FROM config").then(function (rows) {
rows.forEach(function (row){
if (!global.config.hasOwnProperty(row.module)){
global.config[row.module] = {};
}
if (global.config[row.module].hasOwnProperty(row.item)){
return;
}
switch(row.item_type){
case 'int':
global.config[row.module][row.item] = parseInt(row.item_value);
break;
case 'bool':
global.config[row.module][row.item] = (row.item_value === "true");
break;
case 'string':
global.config[row.module][row.item] = row.item_value;
break;
case 'float':
global.config[row.module][row.item] = parseFloat(row.item_value);
break;
}
});
}).then(function(){
global.config['coin'] = JSON.parse(coinConfig)[global.config.coin];
coinInc = require("." + global.config.coin.funcFile);
global.coinFuncs = new coinInc();
if (argv.module === 'pool'){
comms = require('../lib/remote_comms');
} else {
comms = require('../lib/local_comms');
}
global.database = new comms();
global.database.initEnv();
global.coinFuncs.blockedAddresses.push(global.config.pool.address);
global.coinFuncs.blockedAddresses.push(global.config.payout.feeAddress);
}).then(function(){
/*
message Block {
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
optional int64 value = 8;
}
*/
let invalidBlockProto = global.protos.Block.encode({
hash: "88cf2c37e1e4e8a273cbe3ec502b6975fd6c4ebe1e8889ad9d5e53a5e9cde007",
difficulty: 1002932,
shares: 0,
timestamp: Date.now(),
poolType: global.protos.POOLTYPE.PPS,
unlocked: false,
valid: true,
value:0
});
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.BLOCK,
key: global.config.api.authKey,
msg: invalidBlockProto,
exInt: 1
});
request.post({url: global.config.general.shareHost, body: wsData}, function (error, response, body) {
console.log(error);
console.log(JSON.stringify(response));
console.log(JSON.stringify(body));
});
});

@ -0,0 +1,105 @@
"use strict";
let mysql = require("promise-mysql");
let fs = require("fs");
let argv = require('minimist')(process.argv.slice(2));
let config = fs.readFileSync("../config.json");
let coinConfig = fs.readFileSync("../coinConfig.json");
let protobuf = require('protocol-buffers');
const request = require('request');
global.support = require("../lib/support.js")();
global.config = JSON.parse(config);
global.mysql = mysql.createPool(global.config.mysql);
global.protos = protobuf(fs.readFileSync('../lib/data.proto'));
let comms;
let coinInc;
// Config Table Layout
// <module>.<item>
global.mysql.query("SELECT * FROM config").then(function (rows) {
rows.forEach(function (row){
if (!global.config.hasOwnProperty(row.module)){
global.config[row.module] = {};
}
if (global.config[row.module].hasOwnProperty(row.item)){
return;
}
switch(row.item_type){
case 'int':
global.config[row.module][row.item] = parseInt(row.item_value);
break;
case 'bool':
global.config[row.module][row.item] = (row.item_value === "true");
break;
case 'string':
global.config[row.module][row.item] = row.item_value;
break;
case 'float':
global.config[row.module][row.item] = parseFloat(row.item_value);
break;
}
});
}).then(function(){
global.config['coin'] = JSON.parse(coinConfig)[global.config.coin];
coinInc = require("." + global.config.coin.funcFile);
global.coinFuncs = new coinInc();
if (argv.module === 'pool'){
comms = require('../lib/remote_comms');
} else {
comms = require('../lib/local_comms');
}
global.database = new comms();
global.database.initEnv();
global.coinFuncs.blockedAddresses.push(global.config.pool.address);
global.coinFuncs.blockedAddresses.push(global.config.payout.feeAddress);
}).then(function(){
/*
message Block {
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
optional int64 value = 8;
}
*/
global.mysql.query("SELECT * FROM blocks").then(function(rows){
rows.forEach(function(row){
let block = {
hash: row.hex,
difficulty: row.difficulty,
shares: row.shares,
timestamp: global.support.formatDateFromSQL(row.find_time)*1000,
poolType: null,
unlocked: row.unlocked === 1,
valid: row.valid === 1
};
switch(row.pool_type){
case 'pplns':
block.poolType = global.protos.POOLTYPE.PPLNS;
break;
case 'solo':
block.poolType = global.protos.POOLTYPE.SOLO;
break;
case 'prop':
block.poolType = global.protos.POOLTYPE.PROP;
break;
case 'pps':
block.poolType = global.protos.POOLTYPE.PPS;
break;
default:
block.poolType = global.protos.POOLTYPE.PPLNS;
}
global.coinFuncs.getBlockHeaderByHash(block.hash, function(header){
block.value = header.reward;
let txn = global.database.env.beginTxn();
txn.putBinary(global.database.blockDB, row.height, global.protos.Block.encode(block));
txn.commit();
});
});
});
});

@ -0,0 +1,59 @@
"use strict";
let mysql = require("promise-mysql");
let fs = require("fs");
let argv = require('minimist')(process.argv.slice(2));
let config = fs.readFileSync("../config.json");
let coinConfig = fs.readFileSync("../coinConfig.json");
let protobuf = require('protocol-buffers');
const request = require('request');
global.support = require("../lib/support.js")();
global.config = JSON.parse(config);
global.mysql = mysql.createPool(global.config.mysql);
global.protos = protobuf(fs.readFileSync('../lib/data.proto'));
let comms;
let coinInc;
// Config Table Layout
// <module>.<item>
global.mysql.query("SELECT * FROM config").then(function (rows) {
rows.forEach(function (row){
if (!global.config.hasOwnProperty(row.module)){
global.config[row.module] = {};
}
if (global.config[row.module].hasOwnProperty(row.item)){
return;
}
switch(row.item_type){
case 'int':
global.config[row.module][row.item] = parseInt(row.item_value);
break;
case 'bool':
global.config[row.module][row.item] = (row.item_value === "true");
break;
case 'string':
global.config[row.module][row.item] = row.item_value;
break;
case 'float':
global.config[row.module][row.item] = parseFloat(row.item_value);
break;
}
});
}).then(function(){
global.config['coin'] = JSON.parse(coinConfig)[global.config.coin];
coinInc = require("." + global.config.coin.funcFile);
global.coinFuncs = new coinInc();
if (argv.module === 'pool'){
comms = require('../lib/remote_comms');
} else {
comms = require('../lib/local_comms');
}
global.database = new comms();
global.database.initEnv();
global.coinFuncs.blockedAddresses.push(global.config.pool.address);
global.coinFuncs.blockedAddresses.push(global.config.payout.feeAddress);
}).then(function(){
global.database.fixBlockShares(1241110);
});

@ -0,0 +1,230 @@
CREATE DATABASE pool;
GRANT ALL ON pool.* TO pool@`127.0.0.1` IDENTIFIED BY '98erhfiuehw987fh23d';
GRANT ALL ON pool.* TO pool@localhost IDENTIFIED BY '98erhfiuehw987fh23d';
FLUSH PRIVILEGES;
USE pool;
CREATE TABLE balance
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
last_edited TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
payment_address VARCHAR(128),
payment_id VARCHAR(128) DEFAULT NULL,
pool_type VARCHAR(64),
bitcoin TINYINT(1),
amount BIGINT(26) DEFAULT '0'
);
CREATE UNIQUE INDEX balance_id_uindex ON balance (id);
CREATE UNIQUE INDEX balance_payment_address_pool_type_bitcoin_payment_id_uindex ON balance (payment_address, pool_type, bitcoin, payment_id);
CREATE INDEX balance_payment_address_payment_id_index ON balance (payment_address, payment_id);
CREATE TABLE bans
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
ip_address VARCHAR(40),
mining_address VARCHAR(200),
active TINYINT(1) DEFAULT '1',
ins_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE UNIQUE INDEX bans_id_uindex ON bans (id);
CREATE TABLE block_log
(
id INT(11) NOT NULL COMMENT 'Block Height',
orphan TINYINT(1) DEFAULT '1',
hex VARCHAR(128) PRIMARY KEY NOT NULL,
find_time TIMESTAMP,
reward BIGINT(20),
difficulty INT(11),
major_version INT(11),
minor_version INT(11)
);
CREATE UNIQUE INDEX block_log_hex_uindex ON block_log (hex);
CREATE TABLE config
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
module VARCHAR(32),
item VARCHAR(32),
item_value VARCHAR(128),
item_type VARCHAR(64),
Item_desc VARCHAR(512)
);
CREATE UNIQUE INDEX config_id_uindex ON config (id);
CREATE UNIQUE INDEX config_module_item_uindex ON config (module, item);
CREATE TABLE payments
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
unlocked_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
paid_time TIMESTAMP DEFAULT '1970-01-01 00:00:01' NOT NULL,
pool_type VARCHAR(64),
payment_address VARCHAR(125),
transaction_id INT(11) COMMENT 'Transaction ID in the transactions table',
bitcoin TINYINT(1) DEFAULT '0',
amount BIGINT(20),
block_id INT(11),
payment_id VARCHAR(128)
);
CREATE UNIQUE INDEX payments_id_uindex ON payments (id);
CREATE INDEX payments_transactions_id_fk ON payments (transaction_id);
CREATE INDEX payments_payment_address_payment_id_index ON payments (payment_address, payment_id);
CREATE TABLE pools
(
id INT(11) PRIMARY KEY NOT NULL,
ip VARCHAR(72) NOT NULL,
last_checkin TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
active TINYINT(1) NOT NULL,
blockID INT(11),
blockIDTime TIMESTAMP DEFAULT '1970-01-01 00:00:01',
hostname VARCHAR(128)
);
CREATE UNIQUE INDEX pools_id_uindex ON pools (id);
CREATE TABLE ports
(
pool_id INT(11),
network_port INT(11),
starting_diff INT(11),
port_type VARCHAR(64),
description VARCHAR(256),
hidden TINYINT(1) DEFAULT '0',
ip_address VARCHAR(256),
lastSeen TIMESTAMP DEFAULT '1970-01-01 00:00:01',
miners INT(11),
ssl_port TINYINT(1) DEFAULT '0'
);
CREATE TABLE shapeshiftTxn
(
id VARCHAR(64) PRIMARY KEY NOT NULL,
address VARCHAR(128),
paymentID VARCHAR(128),
depositType VARCHAR(16),
withdrawl VARCHAR(128),
withdrawlType VARCHAR(16),
returnAddress VARCHAR(128),
returnAddressType VARCHAR(16),
txnStatus VARCHAR(64),
amountDeposited BIGINT(26),
amountSent FLOAT,
transactionHash VARCHAR(128)
);
CREATE UNIQUE INDEX shapeshiftTxn_id_uindex ON shapeshiftTxn (id);
CREATE TABLE transactions
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
bitcoin TINYINT(1),
address VARCHAR(128),
payment_id VARCHAR(128),
xmr_amt BIGINT(26),
btc_amt BIGINT(26),
transaction_hash VARCHAR(128),
submitted_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
mixin INT(11),
fees BIGINT(26),
payees INT(11),
exchange_rate BIGINT(26),
confirmed TINYINT(1),
confirmed_time TIMESTAMP DEFAULT '1970-01-01 00:00:01',
exchange_name VARCHAR(64),
exchange_txn_id VARCHAR(128)
);
CREATE UNIQUE INDEX transactions_id_uindex ON transactions (id);
CREATE INDEX transactions_shapeshiftTxn_id_fk ON transactions (exchange_txn_id);
CREATE TABLE users
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
username VARCHAR(256) NOT NULL,
pass VARCHAR(64),
email VARCHAR(256),
admin TINYINT(1) DEFAULT '0',
payout_threshold BIGINT(16) DEFAULT '0'
);
CREATE UNIQUE INDEX users_id_uindex ON users (id);
CREATE UNIQUE INDEX users_username_uindex ON users (username);
CREATE TABLE xmrtoTxn
(
id VARCHAR(64) PRIMARY KEY NOT NULL,
address VARCHAR(128),
paymentID VARCHAR(128),
depositType VARCHAR(16),
withdrawl VARCHAR(128),
withdrawlType VARCHAR(16),
returnAddress VARCHAR(128),
returnAddressType VARCHAR(16),
txnStatus VARCHAR(64),
amountDeposited BIGINT(26),
amountSent FLOAT,
transactionHash VARCHAR(128)
);
CREATE UNIQUE INDEX xmrtoTxn_id_uindex ON xmrtoTxn (id);
CREATE TABLE port_config
(
poolPort INT(11) PRIMARY KEY NOT NULL,
difficulty INT(11) DEFAULT '1000',
portDesc VARCHAR(128),
portType VARCHAR(16),
hidden TINYINT(1) DEFAULT '0',
`ssl` TINYINT(1) DEFAULT '0'
);
CREATE UNIQUE INDEX port_config_poolPort_uindex ON port_config (poolPort);
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'minerTimeout', '900', 'int', 'Length of time before a miner is flagged inactive.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'banEnabled', 'true', 'bool', 'Enables/disabled banning of "bad" miners.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'banLength', '-15m', 'string', 'Ban duration except perma-bans');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'targetTime', '30', 'int', 'Time in seconds between share finds');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'trustThreshold', '30', 'int', 'Number of shares before miner trust can kick in.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'banPercent', '25', 'int', 'Percentage of shares that need to be invalid to be banned.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'banThreshold', '30', 'int', 'Number of shares before bans can begin');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'trustedMiners', 'true', 'bool', 'Enable the miner trust system');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'trustChange', '1', 'int', 'Change in the miner trust in percent');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'trustMin', '20', 'int', 'Minimum level of miner trust');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'trustPenalty', '30', 'int', 'Number of shares that must be successful to be trusted, reset to this value if trust share is broken');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'retargetTime', '60', 'int', 'Time between difficulty retargets');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('daemon', 'address', '127.0.0.1', 'string', 'Monero Daemon RPC IP');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('daemon', 'port', '18081', 'int', 'Monero Daemon RPC Port');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'address', '127.0.0.1', 'string', 'Monero Daemon RPC Wallet IP');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'port', '18082', 'int', 'Monero Daemon RPC Wallet Port');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('rpc', 'https', 'false', 'bool', 'Enable RPC over SSL');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'maxDifficulty', '500000', 'int', 'Maximum difficulty for VarDiff');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'minDifficulty', '100', 'int', 'Minimum difficulty for VarDiff');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'varDiffVariance', '20', 'int', 'Percentage out of the target time that difficulty changes');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'varDiffMaxChange', '125', 'int', 'Percentage amount that the difficulty may change');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'btcFee', '1.5', 'float', 'Fee charged for auto withdrawl via BTC');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'ppsFee', '6.5', 'float', 'Fee charged for usage of the PPS pool');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'pplnsFee', '.6', 'float', 'Fee charged for the usage of the PPLNS pool');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'propFee', '.7', 'float', 'Fee charged for the usage of the proportial pool');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'soloFee', '.4', 'float', 'Fee charged for usage of the solo mining pool');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'exchangeMin', '5', 'float', 'Minimum XMR balance for payout to exchange/payment ID');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'walletMin', '.3', 'float', 'Minimum XMR balance for payout to personal wallet');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '3', 'float', 'Donation to XMR core development');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '3', 'float', 'Donation to pool developer');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'denom', '.000001', 'float', 'Minimum balance that will be paid out to.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'blocksRequired', '60', 'int', 'Blocks required to validate a payout before it''s performed.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'sigDivisor', '1000000000000', 'int', 'Divisor for turning coin into human readable amounts ');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feesForTXN', '10', 'int', 'Amount of XMR that is left from the fees to pay miner fees.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'maxTxnValue', '250', 'int', 'Maximum amount of XMR to send in a single transaction');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'shapeshiftPair', 'xmr_btc', 'string', 'Pair to use in all shapeshift lookups for auto BTC payout');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'coinCode', 'XMR', 'string', 'Coincode to be loaded up w/ the shapeshift getcoins argument.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'allowBitcoin', 'false', 'bool', 'Allow the pool to auto-payout to BTC via ShapeShift');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'exchangeRate', '0', 'float', 'Current exchange rate');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'bestExchange', 'xmrto', 'string', 'Current best exchange');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'mixIn', '4', 'int', 'Mixin count for coins that support such things.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'statsBufferLength', '480', 'int', 'Number of items to be cached in the stats buffers.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pps', 'enable', 'false', 'bool', 'Enable PPS or not');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pplns', 'shareMulti', '2', 'int', 'Multiply this times difficulty to set the N in PPLNS');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pplns', 'shareMultiLog', '3', 'int', 'How many times the difficulty of the current block do we keep in shares before clearing them out');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'blockCleaner', 'true', 'bool', 'Enable the deletion of blocks or not.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pool', 'address', '', 'string', 'Address to mine to, this should be the wallet-rpc address.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeAddress', '', 'string', 'Address that pool fees are sent to.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'mailgunKey', '', 'string', 'MailGun API Key for notification');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'mailgunURL', '', 'string', 'MailGun URL for notifications');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailFrom', '', 'string', 'From address for the notification emails');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'testnet', 'false', 'bool', 'Does this pool use testnet?');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pplns', 'enable', 'true', 'bool', 'Enable PPLNS on the pool.');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('solo', 'enable', 'true', 'bool', 'Enable SOLO mining on the pool');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.011', 'float', 'Amount to charge for the txn fee');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewEnd', '4', 'float', 'Value at which txn fee amount drops to 0');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'rpcPasswordEnabled', 'false', 'bool', 'Does the wallet use a RPC password?');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'rpcPasswordPath', '', 'string', 'Path and file for the RPC password file location');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'maxPaymentTxns', '5', 'int', 'Maximum number of transactions in a single payment');
INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'shareHost', '', 'string', 'Host that receives share information');
INSERT INTO pool.users (username, pass, email, admin, payout_threshold) VALUES ('Administrator', null, 'Password123', 1, 0);
INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (3333, 1000, 'Low-End Hardware (Up to 30-40 h/s)', 'pplns', 0, 0);
INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (5555, 5000, 'Medium-Range Hardware (Up to 160 h/s)', 'pplns', 0, 0);
INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (7777, 10000, 'High-End Hardware (Anything else!)', 'pplns', 0, 0);
INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (8001, 20000, 'Claymore SSL', 'pplns', 0, 1);

@ -0,0 +1,10 @@
# Catch-all vhost
:80 {
root /var/www
proxy /leafApi 127.0.0.1:8000
proxy /api 127.0.0.1:8001 {
without /api
}
cors
gzip
}

@ -0,0 +1,91 @@
#!/bin/bash
echo "This assumes that you are doing a green-field install. If you're not, please exit in the next 15 seconds."
sleep 15
echo "Continuing install, this will prompt you for your password if you're not already running as root and you didn't enable passwordless sudo. Please do not run me as root!"
if [[ `whoami` == "root" ]]; then
echo "You ran me as root! Do not run me as root!"
exit 1
fi
ROOT_SQL_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
CURUSER=$(whoami)
echo "Etc/UTC" | sudo tee -a /etc/timezone
sudo rm -rf /etc/localtime
sudo ln -s /usr/share/zoneinfo/Zulu /etc/localtime
sudo dpkg-reconfigure -f noninteractive tzdata
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password password $ROOT_SQL_PASS"
sudo debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $ROOT_SQL_PASS"
echo -e "[client]\nuser=root\npassword=$ROOT_SQL_PASS" | sudo tee /root/.my.cnf
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install git python-virtualenv python3-virtualenv curl ntp build-essential screen cmake pkg-config libboost-all-dev libevent-dev libunbound-dev libminiupnpc-dev libunwind8-dev liblzma-dev libldns-dev libexpat1-dev libgtest-dev mysql-server lmdb-utils
cd ~
git clone https://github.com/Snipa22/nodejs-pool.git # Change this depending on how the deployment goes.
cd /usr/src/gtest
sudo cmake .
sudo make
sudo mv libg* /usr/lib/
cd ~
sudo systemctl enable ntp
cd /usr/local/src
sudo git clone https://github.com/monero-project/monero.git
cd monero
sudo git checkout 15eb2bcf6f2132c5410e937186b6a3121147d628
sudo git apply ~/nodejs-pool/deployment/fluffy.patch
sudo make -j 4
sudo cp ~/nodejs-pool/deployment/monero.service /lib/systemd/system/
sudo useradd -m monerodaemon -d /home/monerodaemon
wget -O /tmp/blockchain.raw https://downloads.getmonero.org/blockchain.raw
sudo -u monerodaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file /tmp/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/monerodaemon/.bitmonero
rm -f /tmp/blockchain.raw
sudo systemctl daemon-reload
sudo systemctl enable monero
sudo systemctl start monero
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
source ~/.nvm/nvm.sh
nvm install v6.9.2
cd ~/nodejs-pool
npm install
npm install -g pm2
openssl req -subj "/C=IT/ST=Pool/L=Daemon/O=Mining Pool/CN=mining.pool" -newkey rsa:2048 -nodes -keyout cert.key -x509 -out cert.pem -days 36500
mkdir ~/pool_db/
cp config_example.json config.json
cd ~
git clone https://github.com/hackfanatic/xmrpoolui
cd xmrpoolui
npm install
cd app
sudo ln -s `pwd` /var/www
cd /tmp/
wget -O caddy.tar.gz 'https://caddyserver.com/download/build?os=linux&arch=amd64&features=cors'
tar -xf caddy.tar.gz
sudo cp caddy /usr/local/bin
sudo chown root:root /usr/local/bin/caddy
sudo chmod 755 /usr/local/bin/caddy
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy
sudo groupadd -g 33 www-data
sudo useradd -g www-data --no-user-group --home-dir /var/www --no-create-home --shell /usr/sbin/nologin --system --uid 33 www-data
sudo mkdir /etc/caddy
sudo chown -R root:www-data /etc/caddy
sudo mkdir /etc/ssl/caddy
sudo chown -R www-data:root /etc/ssl/caddy
sudo chmod 0770 /etc/ssl/caddy
sudo cp ~/nodejs-pool/deployment/caddyfile /etc/caddy/Caddyfile
sudo chown www-data:www-data /etc/caddy/Caddyfile
sudo chmod 444 /etc/caddy/Caddyfile
sudo cp /tmp/init/linux-systemd/caddy.service /etc/systemd/system/
sudo chown root:root /etc/systemd/system/caddy.service
sudo chmod 744 /etc/systemd/system/caddy.service
sudo sed -i 's/ProtectHome=true/ProtectHome=false/' /etc/systemd/system/caddy.service
sudo systemctl daemon-reload
sudo systemctl enable caddy.service
sudo systemctl start caddy.service
cd ~
sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v6.9.2/bin `pwd`/.nvm/versions/node/v6.9.2/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd`
cd ~/nodejs-pool
sudo chown -R $CURUSER. ~/.pm2
pm2 install pm2-logrotate
mysql -u root --password=$ROOT_SQL_PASS < deployment/base.sql
mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO monero_live.config (module, item, item_value, item_type, Item_desc) VALUES ('api', 'authKey', '`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1`', 'string', 'Auth key sent with all Websocket frames for validation.')"
mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO monero_live.config (module, item, item_value, item_type, Item_desc) VALUES ('api', 'secKey', '`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1`', 'string', 'HMAC key for Passwords. JWT Secret Key. Changing this will invalidate all current logins.')"
pm2 start init.js --name=api -- --module=api
echo "You're setup! Please read the rest of the readme for the remainder of your setup and configuration. These steps include: Setting your Fee Address, Pool Address, Global Domain, and the Mailgun setup!"

@ -0,0 +1,77 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cee3d8c..c411f2b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -275,18 +275,21 @@ add_definitions("-DBLOCKCHAIN_DB=${BLOCKCHAIN_DB}")
# Can't install hook in static build on OSX, because OSX linker does not support --wrap
# On ARM, having libunwind package (with .so's only) installed breaks static link.
-if(APPLE OR (ARM AND STATIC))
- set(DEFAULT_STACK_TRACE OFF)
- set(LIBUNWIND_LIBRARIES "")
-else()
- find_package(Libunwind)
- if(LIBUNWIND_FOUND)
- set(DEFAULT_STACK_TRACE ON)
- else()
- set(DEFAULT_STACK_TRACE OFF)
- set(LIBUNWIND_LIBRARIES "")
- endif()
-endif()
+#if(APPLE OR (ARM AND STATIC))
+# set(DEFAULT_STACK_TRACE OFF)
+# set(LIBUNWIND_LIBRARIES "")
+#else()
+# find_package(Libunwind)
+# if(LIBUNWIND_FOUND)
+# set(DEFAULT_STACK_TRACE ON)
+# else()
+# set(DEFAULT_STACK_TRACE OFF)
+# set(LIBUNWIND_LIBRARIES "")
+# endif()
+#endif()
+
+set(DEFAULT_STACK_TRACE OFF)
+set(LIBUNWIND_LIBRARIES "")
option(STACK_TRACE "Install a hook that dumps stack on exception" ${DEFAULT_STACK_TRACE})
diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h
index 95cbf74..d444bcc 100644
--- a/src/cryptonote_config.h
+++ b/src/cryptonote_config.h
@@ -98,7 +98,7 @@
#define P2P_LOCAL_WHITE_PEERLIST_LIMIT 1000
#define P2P_LOCAL_GRAY_PEERLIST_LIMIT 5000
-#define P2P_DEFAULT_CONNECTIONS_COUNT 8
+#define P2P_DEFAULT_CONNECTIONS_COUNT 256
#define P2P_DEFAULT_HANDSHAKE_INTERVAL 60 //secondes
#define P2P_DEFAULT_PACKET_MAX_SIZE 50000000 //50000000 bytes maximum packet size
#define P2P_DEFAULT_PEERS_IN_HANDSHAKE 250
diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp
index 6ad1390..8a9b7fa 100644
--- a/src/cryptonote_core/tx_pool.cpp
+++ b/src/cryptonote_core/tx_pool.cpp
@@ -619,7 +619,7 @@ namespace cryptonote
LOG_PRINT_L2("Filling block template, median size " << median_size << ", " << m_txs_by_fee_and_receive_time.size() << " txes in the pool");
auto sorted_it = m_txs_by_fee_and_receive_time.begin();
- while (sorted_it != m_txs_by_fee_and_receive_time.end())
+ while (sorted_it != m_txs_by_fee_and_receive_time.end() && (bl.tx_hashes.size() < 120))
{
auto tx_it = m_transactions.find(sorted_it->second);
LOG_PRINT_L2("Considering " << tx_it->first << ", size " << tx_it->second.blob_size << ", current block size " << total_size << "/" << max_total_size << ", current coinbase " << print_money(best_coinbase));
diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
index 73e4fa7..0e9e65a 100644
--- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl
+++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
@@ -1108,7 +1108,7 @@ namespace cryptonote
{
if (peer_id && exclude_context.m_connection_id != context.m_connection_id)
{
- if(m_core.get_testnet() && (support_flags & P2P_SUPPORT_FLAG_FLUFFY_BLOCKS))
+ if(support_flags & P2P_SUPPORT_FLAG_FLUFFY_BLOCKS)
{
MDEBUG("PEER SUPPORTS FLUFFY BLOCKS - RELAYING THIN/COMPACT WHATEVER BLOCK");
fluffyConnections.push_back(context.m_connection_id);

@ -0,0 +1,51 @@
#!/bin/bash
echo "This assumes that you are doing a green-field install. If you're not, please exit in the next 15 seconds."
sleep 15
echo "Continuing install, this will prompt you for your password if you're not already running as root and you didn't enable passwordless sudo. Please do not run me as root!"
if [[ `whoami` == "root" ]]; then
echo "You ran me as root! Do not run me as root!"
exit 1
fi
CURUSER=$(whoami)
echo "Etc/UTC" | sudo tee -a /etc/timezone
sudo rm -rf /etc/localtime
sudo ln -s /usr/share/zoneinfo/Zulu /etc/localtime
sudo dpkg-reconfigure -f noninteractive tzdata
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install git python-virtualenv python3-virtualenv curl ntp build-essential screen cmake pkg-config libboost-all-dev libevent-dev libunbound-dev libminiupnpc-dev libunwind8-dev liblzma-dev libldns-dev libexpat1-dev libgtest-dev
cd ~
git clone https://github.com/Snipa22/nodejs-pool.git # Change this depending on how the deployment goes.
cd /usr/src/gtest
sudo cmake .
sudo make
sudo mv libg* /usr/lib/
cd ~
sudo systemctl enable ntp
cd /usr/local/src
sudo git clone https://github.com/monero-project/monero.git
cd monero
sudo git checkout 15eb2bcf6f2132c5410e937186b6a3121147d628
sudo git apply ~/nodejs-pool/deployment/fluffy.patch
sudo make -j 4
sudo cp ~/nodejs-pool/deployment/monero.service /lib/systemd/system/
sudo useradd -m monerodaemon -d /home/monerodaemon
wget -O /tmp/blockchain.raw https://downloads.getmonero.org/blockchain.raw
cd /home/monerodaemon
sudo -u monerodaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file /tmp/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/monerodaemon/.bitmonero
rm -f /tmp/blockchain.raw
sudo systemctl daemon-reload
sudo systemctl enable monero
sudo systemctl start monero
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash
source ~/.nvm/nvm.sh
nvm install v6.9.2
cd ~/nodejs-pool
npm install
npm install -g pm2
openssl req -subj "/C=IT/ST=Pool/L=Daemon/O=Mining Pool/CN=mining.pool" -newkey rsa:2048 -nodes -keyout cert.key -x509 -out cert.pem -days 36500
cd ~
sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v6.9.2/bin `pwd`/.nvm/versions/node/v6.9.2/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd`
sudo chown -R $CURUSER. ~/.pm2
pm2 install pm2-logrotate
echo "You're setup with a leaf node! Congrats"

@ -0,0 +1,13 @@
[Unit]
Description=Monero Daemon
After=network.target
[Service]
Type=forking
GuessMainPID=no
ExecStart=/usr/local/src/monero/build/release/bin/monerod --rpc-bind-ip 127.0.0.1 --detach --restricted-rpc
Restart=always
User=monerodaemon
[Install]
WantedBy=multi-user.target

@ -0,0 +1,97 @@
"use strict";
let mysql = require("promise-mysql");
let fs = require("fs");
let argv = require('minimist')(process.argv.slice(2));
let config = fs.readFileSync("./config.json");
let coinConfig = fs.readFileSync("./coinConfig.json");
let protobuf = require('protocol-buffers');
global.support = require("./lib/support.js")();
global.config = JSON.parse(config);
global.mysql = mysql.createPool(global.config.mysql);
global.protos = protobuf(fs.readFileSync('./lib/data.proto'));
let comms;
let coinInc;
// Config Table Layout
// <module>.<item>
global.mysql.query("SELECT * FROM config").then(function (rows) {
rows.forEach(function (row){
if (!global.config.hasOwnProperty(row.module)){
global.config[row.module] = {};
}
if (global.config[row.module].hasOwnProperty(row.item)){
return;
}
switch(row.item_type){
case 'int':
global.config[row.module][row.item] = parseInt(row.item_value);
break;
case 'bool':
global.config[row.module][row.item] = (row.item_value === "true");
break;
case 'string':
global.config[row.module][row.item] = row.item_value;
break;
case 'float':
global.config[row.module][row.item] = parseFloat(row.item_value);
break;
}
});
}).then(function(){
global.config['coin'] = JSON.parse(coinConfig)[global.config.coin];
coinInc = require(global.config.coin.funcFile);
global.coinFuncs = new coinInc();
if (argv.module === 'pool'){
comms = require('./lib/remote_comms');
} else {
comms = require('./lib/local_comms');
}
global.database = new comms();
global.database.initEnv();
global.coinFuncs.blockedAddresses.push(global.config.pool.address);
global.coinFuncs.blockedAddresses.push(global.config.payout.feeAddress);
switch(argv.module){
case 'pool':
global.config.ports = [];
global.mysql.query("SELECT * FROM port_config").then(function(rows){
rows.forEach(function(row){
row.hidden = row.hidden === 1;
row.ssl = row.ssl === 1;
global.config.ports.push({
port: row.poolPort,
difficulty: row.difficulty,
desc: row.portDesc,
portType: row.portType,
hidden: row.hidden,
ssl: row.ssl
});
});
}).then(function(){
require('./lib/pool.js');
});
break;
case 'blockManager':
require('./lib/blockManager.js');
break;
case 'payments':
require('./lib/payments.js');
break;
case 'api':
require('./lib/api.js');
break;
case 'remoteShare':
require('./lib/remoteShare.js');
break;
case 'worker':
require('./lib/worker.js');
break;
case 'longRunner':
require('./lib/longRunner.js');
break;
default:
console.error("Invalid module provided. Please provide a valid module");
process.exit(1);
}
});

@ -0,0 +1,783 @@
"use strict";
const express = require('express'); // call express
const app = express(); // define our app using express
const cluster = require('cluster');
const async = require("async");
const debug = require("debug")("api");
const btcValidator = require('wallet-address-validator');
const cnUtil = require('cryptonote-util');
let bodyParser = require('body-parser');
let jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens
const crypto = require('crypto');
let addressBase58Prefix = cnUtil.address_decode(new Buffer(global.config.pool.address));
let threadName = "";
let workerList = [];
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
// ROUTES FOR OUR API
// =============================================================================
// test route to make sure everything is working (accessed at GET http://localhost:8080/api)
// Config API
app.get('/config', function (req, res) {
res.json({
pplns_fee: global.config.payout.pplnsFee,
pps_fee: global.config.payout.ppsFee,
solo_fee: global.config.payout.soloFee,
btc_fee: global.config.payout.btcFee,
min_wallet_payout: global.config.payout.walletMin * global.config.general.sigDivisor,
min_btc_payout: global.config.payout.exchangeMin * global.config.general.sigDivisor,
min_exchange_payout: global.config.payout.exchangeMin * global.config.general.sigDivisor,
dev_donation: global.config.payout.devDonation * global.config.general.sigDivisor,
pool_dev_donation: global.config.payout.poolDevDonation * global.config.general.sigDivisor,
maturity_depth: global.config.payout.blocksRequired,
min_denom: global.config.payout.denom * global.config.general.sigDivisor
});
});
// Pool APIs
app.get('/pool/address_type/:address', function (req, res) {
let address = req.params.address;
if (addressBase58Prefix === cnUtil.address_decode(new Buffer(address))) {
res.json({valid: true, address_type: 'XMR'});
} else if (btcValidator.validate(this.address) && global.config.general.allowBitcoin) {
res.json({valid: true, address_type: 'BTC'});
} else {
res.json({valid: false});
}
});
app.get('/pool/stats', function (req, res) {
let localCache = global.database.getCache('pool_stats_global');
delete(localCache.minerHistory);
delete(localCache.hashHistory);
res.json({pool_list: ['pplns', 'pps', 'solo'], pool_statistics: localCache});
});
app.get('/pool/chart/hashrate', function (req, res) {
res.json(global.database.getCache('global_stats')['hashHistory']);
});
app.get('/pool/chart/miners', function (req, res) {
res.json(global.database.getCache('global_stats')['minerHistory']);
});
app.get('/pool/chart/hashrate/:pool_type', function (req, res) {
let pool_type = req.params.pool_type;
let localCache;
switch (pool_type) {
case 'pplns':
localCache = global.database.getCache('pplns_stats');
break;
case 'pps':
localCache = global.database.getCache('pps_stats');
break;
case 'solo':
localCache = global.database.getCache('solo_stats');
break;
case 'default':
return res.json({'error': 'Invalid pool type'});
}
res.json(localCache['hashHistory']);
});
app.get('/pool/chart/miners/:pool_type', function (req, res) {
let pool_type = req.params.pool_type;
let localCache;
switch (pool_type) {
case 'pplns':
localCache = global.database.getCache('stats_pplns');
break;
case 'pps':
localCache = global.database.getCache('stats_pps');
break;
case 'solo':
localCache = global.database.getCache('stats_solo');
break;
case 'default':
return res.json({'error': 'Invalid pool type'});
}
res.json(localCache['minerHistory']);
});
app.get('/pool/stats/:pool_type', function (req, res) {
let pool_type = req.params.pool_type;
let localCache;
switch (pool_type) {
case 'pplns':
localCache = global.database.getCache('pool_stats_pplns');
localCache.fee = global.config.payout.pplnsFee;
break;
case 'pps':
localCache = global.database.getCache('pool_stats_pps');
localCache.fee = global.config.payout.ppsFee;
break;
case 'solo':
localCache = global.database.getCache('pool_stats_solo');
localCache.fee = global.config.payout.soloFee;
break;
case 'default':
return res.json({'error': 'Invalid pool type'});
}
delete(localCache.minerHistory);
delete(localCache.hashHistory);
res.json({pool_statistics: localCache});
});
app.get('/pool/ports', function (req, res) {
res.json(global.database.getCache('poolPorts'));
});
app.get('/pool/blocks/:pool_type', function (req, res) {
res.json(global.database.getBlockList(req.params.pool_type));
});
app.get('/pool/blocks', function (req, res) {
res.json(global.database.getBlockList());
});
app.get('/pool/payments/:pool_type', function (req, res) {
let pool_type = req.params.pool_type;
switch (pool_type) {
case 'pplns':
break;
case 'pps':
break;
case 'solo':
break;
case 'default':
return res.json({'error': 'Invalid pool type'});
}
let paymentIds = [];
let query = "SELECT distinct(transaction_id) as txnID FROM payments WHERE pool_type = ? ORDER BY transaction_id";
let response = [];
global.mysql.query(query, [pool_type]).then(function (rows) {
if (rows.length === 0) {
return res.json([]);
}
rows.forEach(function (row, index, array) {
paymentIds.push(row.txnID);
if (array.length === paymentIds.length) {
global.mysql.query("SELECT * FROM transactions WHERE id IN (" + paymentIds.join() + ") ORDER BY id DESC").then(function (txnIDRows) {
txnIDRows.forEach(function (txnrow) {
let ts = new Date(txnrow.submitted_time);
response.push({
id: txnrow.id,
hash: txnrow.transaction_hash,
mixins: txnrow.mixin,
payees: txnrow.payees,
fee: txnrow.fees,
value: txnrow.xmr_amt,
ts: ts.getTime(),
});
if (response.length === txnIDRows.length) {
return res.json(response.sort(global.support.tsCompare));
}
});
});
}
});
}).catch(function (err) {
console.error(threadName + "Error getting pool payments: " + JSON.stringify(err));
return res.json({error: 'Issue getting pool payments'});
});
});
app.get('/pool/payments', function (req, res) {
let query = "SELECT * FROM transactions ORDER BY id DESC";
global.mysql.query(query).then(function (rows) {
if (rows.length === 0) {
return res.json([]);
}
let response = [];
rows.forEach(function (row, index, array) {
global.mysql.query("SELECT pool_type FROM payments WHERE transaction_id = ? LIMIT 1", [row.id]).then(function (ptRows) {
let ts = new Date(row.submitted_time);
response.push({
id: row.id,
hash: row.transaction_hash,
mixins: row.mixin,
payees: row.payees,
fee: row.fees,
value: row.xmr_amt,
ts: ts.getTime(),
pool_type: ptRows[0].pool_type
});
if (array.length === response.length) {
res.json(response.sort(global.support.tsCompare));
}
});
});
}).catch(function (err) {
console.error(threadName + "Error getting miner payments: " + JSON.stringify(err));
res.json({error: 'Issue getting miner payments'});
});
});
// Network APIs
app.get('/network/stats', function (req, res) {
res.json(global.database.getCache('networkBlockInfo'));
});
// Miner APIs
app.get('/miner/:address/identifiers', function (req, res) {
let address = req.params.address;
return res.json(global.database.getCache(address + '_identifiers'));
});
app.get('/miner/:address/payments', function (req, res) {
let address_parts = req.params.address.split('.');
let address = address_parts[0];
let payment_id = address_parts[1];
let query = "SELECT amount, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " +
"payments WHERE payment_address = ? AND paid_time < ? AND payment_id = ? ORDER BY paid_time DESC LIMIT 25";
if (typeof(payment_id) === 'undefined') {
query = "SELECT amount as amt, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " +
"payments WHERE payment_address = ? AND paid_time < ? AND payment_id IS ? ORDER BY paid_time DESC LIMIT 25";
}
let start = req.query.start || Date.now() / 1000;
start *= 1000;
let response = [];
global.mysql.query(query, [address, global.support.formatDate(start), payment_id]).then(function (rows) {
if (rows.length === 0) {
return res.json(response);
}
rows.forEach(function (row, index, array) {
debug(threadName + "Got rows from initial SQL query: " + JSON.stringify(row));
global.mysql.query("SELECT transaction_hash, mixin FROM transactions WHERE id = ? ORDER BY id DESC", [row.transaction_id]).then(function (txnrows) {
txnrows.forEach(function (txnrow) {
debug(threadName + "Got a row that's a transaction ID: " + JSON.stringify(txnrow));
response.push({
pt: row.pool_type,
ts: Math.ceil(row.ts),
amount: row.amt,
txnHash: txnrow.transaction_hash,
mixin: txnrow.mixin
});
if (array.length === response.length) {
return res.json(response);
}
});
});
});
}).catch(function (err) {
console.error(threadName + "Error getting miner payments: " + JSON.stringify(err));
return res.json({error: 'Issue getting miner payments'});
});
});
app.get('/miner/:address/stats/allWorkers', function (req, res) {
let address = req.params.address;
let identifiers = global.database.getCache(address + '_identifiers').sort();
let globalCache = global.database.getCache(address);
let returnData = {global: {
lts: Math.floor(globalCache.lastHash / 1000),
identifer: 'global',
hash: globalCache.hash,
totalHash: globalCache.totalHashes
}};
let intCounter = 0;
identifiers.forEach(function(identifier){
let cachedData = global.database.getCache(req.params.address+"_"+identifier);
returnData[identifier] = {
lts: Math.floor(cachedData.lastHash / 1000),
identifer: identifier,
hash: cachedData.hash,
totalHash: cachedData.totalHashes
};
intCounter += 1;
if (intCounter === identifiers.length){
return res.json(returnData);
}
});
});
app.get('/miner/:address/stats/:identifier', function (req, res) {
let address = req.params.address;
let identifier = req.params.identifier;
let memcKey = address + "_" + identifier;
/*
hash: Math.floor(localStats.miners[miner] / 600),
totalHashes: 0,
lastHash: localTimes.miners[miner]
*/
let cachedData = global.database.getCache(memcKey);
return res.json({
lts: Math.floor(cachedData.lastHash / 1000),
identifer: identifier,
hash: cachedData.hash,
totalHash: cachedData.totalHashes
});
});
app.get('/miner/:address/chart/hashrate', function (req, res) {
return res.json(global.database.getCache(req.params.address)['hashHistory']);
});
app.get('/miner/:address/chart/hashrate/allWorkers', function (req, res) {
let address = req.params.address;
let identifiers = global.database.getCache(address + '_identifiers').sort();
let returnData = {global: global.database.getCache(req.params.address)['hashHistory']};
let intCounter = 0;
identifiers.forEach(function(identifier){
returnData[identifier] = global.database.getCache(req.params.address+"_"+identifier)['hashHistory'];
intCounter += 1;
if (intCounter === identifiers.length){
return res.json(returnData);
}
});
});
app.get('/miner/:address/chart/hashrate/:identifier', function (req, res) {
return res.json(global.database.getCache(req.params.address + "_" + req.params.identifier)['hashHistory']);
});
app.get('/miner/:address/stats', function (req, res) {
let address = req.params.address;
let address_parts = req.params.address.split('.');
let address_pt = address_parts[0];
let payment_id = address_parts[1];
let paidQuery = "SELECT SUM(amount) as amt FROM payments WHERE payment_address = ? AND payment_id = ?";
let unpaidQuery = "SELECT SUM(amount) as amt FROM balance WHERE payment_address = ? AND payment_id = ?";
if (typeof(payment_id) === 'undefined') {
paidQuery = "SELECT SUM(amount) as amt FROM payments WHERE payment_address = ? AND payment_id IS ?";
unpaidQuery = "SELECT SUM(amount) as amt FROM balance WHERE payment_address = ? AND payment_id IS ?";
}
async.waterfall([
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for /miner/address/stats");
return callback(null, {hash: global.database.getCache(address).hash, identifier: 'global'});
},
function (returnData, callback) {
// TODO: Fixme once we have total hash counts...
returnData.totalHashes = global.database.getCache(address).totalHashes;
return callback(null, returnData);
},
function (returnData, callback) {
debug(threadName + "Checking Influx for last share for /miner/address/stats");
returnData.lastHash = Math.floor(global.database.getCache(address).lastHash / 1000);
return callback(null, returnData);
},
function (returnData, callback) {
debug(threadName + "Checking MySQL total amount paid for /miner/address/stats");
global.mysql.query(paidQuery, [address_pt, payment_id]).then(function (rows) {
if (typeof(rows[0]) === 'undefined') {
returnData.amtPaid = 0;
} else {
returnData.amtPaid = rows[0].amt;
if (returnData.amtPaid === null) {
returnData.amtPaid = 0;
}
}
return callback(null, returnData);
});
},
function (returnData, callback) {
debug(threadName + "Checking MySQL total amount unpaid for /miner/address/stats");
global.mysql.query(unpaidQuery, [address_pt, payment_id]).then(function (rows) {
if (typeof(rows[0]) === 'undefined') {
returnData.amtDue = 0;
} else {
returnData.amtDue = rows[0].amt;
if (returnData.amtDue === null) {
returnData.amtDue = 0;
}
}
return callback(true, returnData);
});
}
], function (err, result) {
debug(threadName + "Result information for " + address + ": " + JSON.stringify(result));
if (err === true) {
return res.json(result);
}
if (err) {
console.error(threadName + "Error within the miner stats identifier func");
return res.json({'error': err.toString()});
}
});
});
// Authentication
app.post('/authenticate', function (req, res) {
let hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex');
global.mysql.query("SELECT * FROM users WHERE username = ? AND ((pass IS null AND email = ?) OR (pass = ?))", [req.body.username, req.body.password, hmac]).then(function (rows) {
if (rows.length === 0) {
return res.json({'success': false, msg: 'Invalid username/password'});
}
let token = jwt.sign({id: rows[0].id, admin: rows[0].admin}, global.config.api.secKey, {expiresIn: '1d'});
return res.json({'success': true, 'msg': token});
});
});
// JWT Verification
// get an instance of the router for api routes
let secureRoutes = express.Router();
let adminRoutes = express.Router();
// route middleware to verify a token
secureRoutes.use(function (req, res, next) {
let token = req.body.token || req.query.token || req.headers['x-access-token'];
if (token) {
jwt.verify(token, global.config.api.secKey, function (err, decoded) {
if (err) {
return res.json({success: false, msg: 'Failed to authenticate token.'});
} else {
req.decoded = decoded;
next();
}
});
} else {
return res.status(403).send({
success: false,
msg: 'No token provided.'
});
}
});
// Secure/logged in routes.
secureRoutes.get('/tokenRefresh', function (req, res) {
let token = jwt.sign({id: req.decoded.id, admin: req.decoded.admin}, global.config.api.secKey, {expiresIn: '1d'});
return res.json({'msg': token});
});
secureRoutes.get('/', function (req, res) {
global.mysql.query("SELECT payout_threshold FROM users WHERE id = ?", [req.decoded.id]).then(function(row){
return res.json({msg: row[0].payout_threshold});
});
return res.json({msg: 0});
});
secureRoutes.post('/changePassword', function (req, res) {
let hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex');
global.mysql.query("UPDATE users SET pass = ? WHERE id = ?", [hmac, req.decoded.id]).then(function () {
return res.json({'msg': 'Password updated'});
});
});
secureRoutes.post('/changePayoutThreshold', function (req, res) {
let threshold = req.body.threshold;
if (threshold < global.config.payout.walletMin) {
threshold = global.config.payout.walletMin;
}
threshold = global.support.decimalToCoin(threshold);
global.mysql.query("UPDATE users SET payout_threshold = ? WHERE id = ?", [threshold, req.decoded.id]).then(function () {
return res.json({'msg': 'Threshold updated, set to: ' + global.config.support.coinToDecimal(threshold)});
});
});
// Administrative routes/APIs
adminRoutes.use(function (req, res, next) {
let token = req.body.token || req.query.token || req.headers['x-access-token'];
if (token) {
jwt.verify(token, global.config.api.secKey, function (err, decoded) {
if (decoded.admin !== 1) {
return res.status(403).send({
success: false,
msg: 'You are not an admin.'
});
}
if (err) {
return res.json({success: false, msg: 'Failed to authenticate token.'});
} else {
req.decoded = decoded;
next();
}
});
} else {
return res.status(403).send({
success: false,
msg: 'No token provided.'
});
}
});
adminRoutes.get('/stats', function (req, res) {
/*
Admin interface stats.
For each pool type + global, we need the following:
Total Owed, Total Paid, Total Mined, Total Blocks, Average Luck
*/
let intCache = {
'pplns': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
'pps': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
'solo': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
'global': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0},
'fees': {owed: 0, paid: 0, mined: 0, shares: 0, targetShares: 0}
};
async.series([
function (callback) {
global.mysql.query("select * from balance").then(function (rows) {
rows.forEach(function (row) {
intCache[row.pool_type].owed += row.amount;
intCache.global.owed += row.amount;
});
}).then(function () {
return callback(null);
});
},
function (callback) {
global.mysql.query("select * from payments").then(function (rows) {
rows.forEach(function (row) {
intCache[row.pool_type].paid += row.amount;
intCache.global.paid += row.amount;
});
}).then(function () {
return callback(null);
});
},
function (callback) {
global.database.getBlockList().forEach(function (block) {
intCache[block.pool_type].mined += block.value;
intCache.global.mined += block.value;
intCache[block.pool_type].shares += block.shares;
intCache.global.shares += block.shares;
intCache[block.pool_type].targetShares += block.diff;
intCache.global.targetShares += block.diff;
});
return callback(null);
}
], function (err) {
return res.json(intCache);
});
});
adminRoutes.get('/wallet', function (req, res) {
// Stats for the admin interface.
// Load the wallet state from cache, NOTHING HAS DIRECT ACCESS.
// walletStateInfo
return res.json(global.database.getCache('walletStateInfo'));
});
adminRoutes.get('/wallet/history', function (req, res) {
// walletHistory
if (req.decoded.admin === 1) {
return res.json(global.database.getCache('walletHistory'));
}
});
adminRoutes.get('/ports', function (req, res) {
let retVal = [];
global.mysql.query("SELECT * FROM port_config").then(function (rows) {
rows.forEach(function (row) {
retVal.push({
port: row.poolPort,
diff: row.difficulty,
desc: row.portDesc,
portType: row.portType,
hidden: row.hidden === 1,
ssl: row.ssl === 1
});
});
}).then(function () {
return res.json(retVal);
});
});
adminRoutes.post('/ports', function (req, res) {
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [req.decoded.port]).then(function (rows) {
if (rows.length !== 0) {
return "Port already exists with that port number.";
}
if (req.decoded.diff > global.config.pool.maxDifficulty || req.decoded.diff < global.config.pool.minDifficulty) {
return "Invalid difficulty.";
}
if (["pplns", "solo", "pps"].indexOf(req.decoded.portType) === -1) {
return "Invalid port type";
}
global.mysql.query("INSERT INTO port_config (poolPort, difficulty, portDesc, portType, hidden, ssl) VALUES (?, ?, ?, ?, ?, ?)",
[req.decoded.port, req.decoded.diff, req.decoded.desc, req.decoded.portType, req.decoded.hidden === 1, req.decoded.ssl === 1]);
}).then(function (err) {
if (typeof(err) === 'string') {
return res.json({success: false, msg: err});
}
return res.json({success: true, msg: "Added port to database"});
});
});
adminRoutes.put('/ports', function (req, res) {
let portNumber = Number(req.decoded.portNum);
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [portNumber]).then(function (rows) {
if (rows.length === 0) {
return "Port doesn't exist in the database";
}
if (req.decoded.diff > global.config.pool.maxDifficulty || req.decoded.diff < global.config.pool.minDifficulty) {
return "Invalid difficulty.";
}
if (["pplns", "solo", "pps"].indexOf(req.decoded.portType) === -1) {
return "Invalid port type";
}
global.mysql.query("UPDATE port_config SET difficulty=?, portDesc=?, portType=?, hidden=?, ssl=? WHERE poolPort = ?",
[req.decoded.diff, req.decoded.desc, req.decoded.portType, req.decoded.hidden === 1, req.decoded.ssl === 1, portNumber]);
}).then(function (err) {
if (typeof(err) === 'string') {
return res.json({success: false, msg: err});
}
return res.json({success: true, msg: "Updated port in database"});
});
});
adminRoutes.delete('/ports', function (req, res) {
let portNumber = Number(req.decoded.portNum);
global.mysql.query("SELECT * FROM port_config WHERE poolPort = ?", [portNumber]).then(function (rows) {
if (rows.length === 0) {
return "Port doesn't exist in the database";
}
global.mysql.query("DELETE FROM port_config WHERE poolPort = ?", [portNumber]);
}).then(function (err) {
if (typeof(err) === 'string') {
return res.json({success: false, msg: err});
}
return res.json({success: true, msg: "Added port to database"});
});
});
adminRoutes.get('/config', function (req, res) {
let retVal = [];
global.mysql.query("SELECT * FROM config").then(function (rows) {
rows.forEach(function (row) {
retVal.push({
id: row.id,
module: row.module,
item: row.item,
value: row.item_value,
type: row.item_type,
desc: row.item_desc
});
});
}).then(function () {
return res.json(retVal);
});
});
adminRoutes.put('/config', function (req, res) {
let configID = Number(req.decoded.id);
global.mysql.query("SELECT * FROM config WHERE id = ?", [configID]).then(function (rows) {
if (rows.length === 0) {
return "Config item doesn't exist in the database";
}
global.mysql.query("UPDATE config SET item_value=? WHERE id = ?", [req.decoded.value, configID]);
}).then(function (err) {
if (typeof(err) === 'string') {
return res.json({success: false, msg: err});
}
return res.json({success: true, msg: "Updated port in database"});
});
});
adminRoutes.get('/userList', function (req, res) {
/*
List of all the users in the system.
Might as well do it all, right? :3
Data Format to be documented.
*/
let intCache = {};
global.mysql.query("select sum(balance.amount) as amt_due, sum(payments.amount) as amt_paid," +
"balance.payment_address as address, balance.payment_id as payment_id from balance LEFT JOIN payments on " +
"payments.payment_address=balance.payment_address or payments.payment_id=balance.payment_id " +
"group by address, payment_id").then(function (rows) {
rows.forEach(function (row) {
let key = row.address;
if (row.payment_id !== null) {
key += '.' + row.payment_id;
}
intCache[key] = {
paid: row.amt_paid,
due: row.amt_due,
address: key,
workers: [],
lastHash: 0,
totalHashes: 0,
hashRate: 0,
goodShares: 0,
badShares: 0
};
});
}).then(function () {
let minerList = global.database.getCache('minerList');
if (minerList) {
minerList.forEach(function (miner) {
let minerData = miner.split('_');
let minerCache = global.database.getCache(miner);
if (!minerCache.hasOwnProperty('goodShares')) {
minerCache.goodShares = 0;
minerCache.badShares = 0;
}
if (!intCache.hasOwnProperty(minerData[0])) {
intCache[minerData[0]] = {paid: 0, due: 0, address: minerData[0], workers: []};
}
if (typeof(minerData[1]) !== 'undefined') {
intCache[minerData[0]].workers.push({
worker: minerData[1],
hashRate: minerCache.hash,
lastHash: minerCache.lastHash,
totalHashes: minerCache.totalHashes,
goodShares: minerCache.goodShares,
badShares: minerCache.badShares
});
} else {
intCache[minerData[0]].lastHash = minerCache.lastHash;
intCache[minerData[0]].totalHashes = minerCache.totalHashes;
intCache[minerData[0]].hashRate = minerCache.hash;
intCache[minerData[0]].goodShares = minerCache.goodShares;
intCache[minerData[0]].badShares = minerCache.badShares;
}
});
let retList = [];
for (let minerId in intCache) {
if (intCache.hasOwnProperty(minerId)) {
let miner = intCache[minerId];
retList.push(miner);
}
}
return res.json(retList);
}
return res.json([]);
});
});
// apply the routes to our application with the prefix /api
app.use('/authed', secureRoutes);
app.use('/admin', adminRoutes);
// Authenticated routes
if (cluster.isMaster) {
let numWorkers = require('os').cpus().length;
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for (let i = 0; i < numWorkers; i++) {
let worker = cluster.fork();
workerList.push(worker);
}
cluster.on('online', function (worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function (worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
worker = cluster.fork();
workerList.push(worker);
});
} else {
app.listen(8001, function () {
console.log('Process ' + process.pid + ' is listening to all incoming requests');
});
}

@ -0,0 +1,504 @@
"use strict";
const range = require("range");
const debug = require("debug")("blockManager");
const async = require("async");
// This file is for managing the block databases within the SQL database.
// Primary Tasks:
// Sync the chain into the block_log database. - Scan on startup for missing data, starting from block 0
// Maintain a check for valid blocks in the system. (Only last number of blocks required for validation of payouts) - Perform every 2 minutes. Scan on the main blocks table as well for sanity sake.
// Maintain the block_log database in order to ensure payments happen smoothly. - Scan every 1 second for a change in lastblockheader, if it changes, insert into the DB.
let blockIDCache = [];
let scanInProgress = false;
let blockHexCache = {};
let lastBlock = 0;
let balanceIDCache = {};
let blockQueue = async.queue(function (task, callback) {
global.support.rpcDaemon('getblockheaderbyheight', {"height": task.blockID}, function (body) {
let blockData = body.result.block_header;
if (blockData.hash in blockHexCache) {
return callback();
}
debug("Adding block to block_log, ID: " + task.blockID);
blockIDCache.push(task.blockID);
blockHexCache[body.result.block_header.hash] = null;
global.mysql.query("INSERT INTO block_log (id, orphan, hex, find_time, reward, difficulty, major_version, minor_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[task.blockID, blockData.orphan_status, blockData.hash, global.support.formatDate(blockData.timestamp * 1000), blockData.reward, blockData.difficulty, blockData.major_version, blockData.minor_version]).then(function () {
return calculatePPSPayments(blockData, callback);
}).catch(function (err) {
debug("BlockHexCache Check: " + blockData.hash in blockHexCache);
debug("BlockIDCache Check: " + blockIDCache.hasOwnProperty(task.blockID));
debug("Hex: " + blockData.hash + " Height:" + task.blockID);
console.error("Tried to reprocess a block that'd already been processed");
console.error(JSON.stringify(err));
return callback();
});
});
}, 16);
blockQueue.drain = function () {
console.log("Scan complete, unlocking remainder of blockManager functionality.");
scanInProgress = false;
};
let createBalanceQueue = async.queue(function (task, callback) {
let pool_type = task.pool_type;
let payment_address = task.payment_address;
let payment_id = task.payment_id;
let bitcoin = task.bitcoin;
let query = "SELECT id FROM balance WHERE payment_address = ? AND payment_id is ? AND pool_type = ? AND bitcoin = ?";
if (payment_id !== null) {
query = "SELECT id FROM balance WHERE payment_address = ? AND payment_id = ? AND pool_type = ? AND bitcoin = ?";
}
let cacheKey = payment_address + pool_type + bitcoin + payment_id;
debug("Processing a account add/check for:" + JSON.stringify(task));
global.mysql.query(query, [payment_address, payment_id, pool_type, bitcoin]).then(function (rows) {
if (rows.length === 0) {
global.mysql.query("INSERT INTO balance (payment_address, payment_id, pool_type, bitcoin) VALUES (?, ?, ?, ?)", [payment_address, payment_id, pool_type, bitcoin]).then(function (result) {
debug("Added to the SQL database: " + result.insertId);
balanceIDCache[cacheKey] = result.insertId;
return callback();
});
} else {
debug("Found it in MySQL: " + rows[0].id);
balanceIDCache[cacheKey] = rows[0].id;
return callback();
}
});
}, 1);
let balanceQueue = async.queue(function (task, callback) {
let pool_type = task.pool_type;
let payment_address = task.payment_address;
let payment_id = null;
if (typeof(task.payment_id) !== 'undefined' && task.payment_id !== null && task.payment_id.length > 10){
payment_id = task.payment_id;
}
task.payment_id = payment_id;
let bitcoin = task.bitcoin;
let amount = task.amount;
debug("Processing balance increment task: " + JSON.stringify(task));
async.waterfall([
function (intCallback) {
let cacheKey = payment_address + pool_type + bitcoin + payment_id;
if (cacheKey in balanceIDCache) {
return intCallback(null, balanceIDCache[cacheKey]);
} else {
createBalanceQueue.push(task, function () {
});
async.until(function () {
return cacheKey in balanceIDCache;
}, function (intCallback) {
createBalanceQueue.push(task, function () {
return intCallback(null, balanceIDCache[cacheKey]);
});
}, function () {
return intCallback(null, balanceIDCache[cacheKey]);
}
);
}
},
function (balance_id, intCallback) {
debug("Made it to the point that I can update the balance for: " + balance_id + " for the amount: " + amount);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE id = ?", [amount, balance_id]).then(function () {
return intCallback(null);
});
}
],
function () {
return callback();
}
)
;
}, 24
);
function calculatePPSPayments(blockHeader, callback) {
console.log("Performing PPS payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
let totalPayments = 0;
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockHeader.height) === blockHeader.height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let blockDiff = blockHeader.difficulty;
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.PPS) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'pps',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let amountToPay = Math.floor((shareData.shares / blockDiff) * rewardTotal);
let feesToPay = Math.floor(amountToPay * (global.config.payout.ppsFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(amountToPay * (global.config.payout.btcFee / 100));
}
amountToPay -= feesToPay;
paymentData[userIdentifier].amount = paymentData[userIdentifier].amount + amountToPay;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = paymentData[global.config.payout.feeAddress].amount + feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("PPS payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
return callback();
}
function calculatePPLNSPayments(blockHeader) {
console.log("Performing PPLNS payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let rewardTotal = blockHeader.reward;
let blockCheckHeight = blockHeader.height;
let totalPaid = 0;
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
async.doWhilst(function (callback) {
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockCheckHeight) === blockCheckHeight); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let blockDiff = blockHeader.difficulty;
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.PPLNS) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'pplns',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let amountToPay = Math.floor((shareData.shares / (blockDiff*2)) * rewardTotal);
if (totalPaid + amountToPay > rewardTotal) {
amountToPay = rewardTotal - totalPaid;
}
totalPaid += amountToPay;
let feesToPay = Math.floor(amountToPay * (global.config.payout.pplnsFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(amountToPay * (global.config.payout.btcFee / 100));
}
amountToPay -= feesToPay;
paymentData[userIdentifier].amount = paymentData[userIdentifier].amount + amountToPay;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = paymentData[global.config.payout.feeAddress].amount + feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
callback(null, totalPaid);
}, function (totalPayment) {
blockCheckHeight = blockCheckHeight - 1;
debug("Decrementing the block chain check height to:" + blockCheckHeight);
if (totalPayment >= rewardTotal) {
debug("Loop 1: Total Payment: " + totalPayment + " Amount Paid: " + rewardTotal + " Amount Total: " + totalPaid);
return false;
} else {
debug("Loop 2: Total Payment: " + totalPayment + " Amount Paid: " + rewardTotal + " Amount Total: " + totalPaid);
return blockCheckHeight !== 0;
}
}, function (err) {
let totalPayments = 0;
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("PPLNS payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
});
}
function calculateSoloPayments(blockHeader) {
console.log("Performing Solo payout on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward));
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
let paymentData = {};
paymentData[global.config.payout.feeAddress] = {
pool_type: 'fees',
payment_address: global.config.payout.feeAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.coinDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.coinDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
paymentData[global.coinFuncs.poolDevAddress] = {
pool_type: 'fees',
payment_address: global.coinFuncs.poolDevAddress,
payment_id: null,
bitcoin: 0,
amount: 0
};
let totalPayments = 0;
for (let found = (cursor.goToRange(blockHeader.height) === blockHeader.height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let shareData;
try {
shareData = global.protos.Share.decode(data);
} catch (e) {
console.error(e);
return;
}
let rewardTotal = blockHeader.reward;
if (shareData.poolType === global.protos.POOLTYPE.SOLO && shareData.foundBlock === true) {
let userIdentifier = shareData.paymentAddress;
if (shareData.paymentID) {
userIdentifier = userIdentifier + "." + shareData.paymentID;
}
if (!(userIdentifier in paymentData)) {
paymentData[userIdentifier] = {
pool_type: 'solo',
payment_address: shareData.paymentAddress,
payment_id: shareData.paymentID,
bitcoin: shareData.bitcoin,
amount: 0
};
}
let feesToPay = Math.floor(rewardTotal * (global.config.payout.soloFee / 100));
if (shareData.bitcoin === true) {
feesToPay += Math.floor(rewardTotal * (global.config.payout.btcFee / 100));
}
rewardTotal -= feesToPay;
paymentData[userIdentifier].amount = rewardTotal;
let donations = 0;
if(global.config.payout.devDonation > 0){
let devDonation = (feesToPay * (global.config.payout.devDonation / 100));
donations += devDonation;
paymentData[global.coinFuncs.coinDevAddress].amount = paymentData[global.coinFuncs.coinDevAddress].amount + devDonation ;
}
if(global.config.payout.poolDevDonation > 0){
let poolDevDonation = (feesToPay * (global.config.payout.poolDevDonation / 100));
donations += poolDevDonation;
paymentData[global.coinFuncs.poolDevAddress].amount = paymentData[global.coinFuncs.poolDevAddress].amount + poolDevDonation;
}
paymentData[global.config.payout.feeAddress].amount = feesToPay - donations;
}
});
}
cursor.close();
txn.abort();
Object.keys(paymentData).forEach(function (key) {
balanceQueue.push(paymentData[key], function () {
});
totalPayments += paymentData[key].amount;
});
console.log("Solo payout cycle complete on block: " + blockHeader.height + " Block Value: " + global.support.coinToDecimal(blockHeader.reward) + " Block Payouts: " + global.support.coinToDecimal(totalPayments) + " Payout Percentage: " + (totalPayments / blockHeader.reward) * 100 + "%");
}
function blockInvalidator() {
if (scanInProgress) {
debug("Skipping block invalidator run as there's a scan in progress");
return;
}
debug("Running block invalidator");
global.mysql.query("SELECT id, hex FROM block_log ORDER BY find_time DESC LIMIT ?", [global.config.payout.blocksRequired]).then(function (rows) {
rows.forEach(function (row) {
global.support.rpcDaemon('getblockheaderbyheight', {"height": row.id}, function (body) {
// DO NOT TRUST THIS. GET THE BLOCK HEADER BY HEIGHT AND VERIFY THAT THE HASHES MATCH.
if (body.result.block_header.hash !== row.hex) {
global.database.invalidateBlock(row.id);
global.mysql.query("UPDATE block_log SET orphan = true WHERE hex = ?", [row.hex]);
blockIDCache.splice(blockIDCache.indexOf(body.result.block_header.height));
console.log("Invalidating block " + body.result.block_header.height + " due to being an orphan block");
}
});
});
});
}
function blockUnlocker() {
if (scanInProgress) {
debug("Skipping block unlocker run as there's a scan in progress");
return;
}
debug("Running block unlocker");
let blockList = global.database.getValidLockedBlocks();
global.support.rpcDaemon('getlastblockheader', [], function (body) {
let blockHeight = body.result.block_header.height;
blockList.forEach(function (row) {
if (blockHeight - row.height > global.config.payout.blocksRequired) {
blockPayments(row);
}
});
});
}
function blockPayments(block) {
switch (block.poolType) {
case global.protos.POOLTYPE.PPS:
// PPS is paid out per share find per block, so this is handled in the main block-find loop.
break;
case global.protos.POOLTYPE.PPLNS:
global.coinFuncs.getBlockHeaderByHash(block.hash, function (header) {
calculatePPLNSPayments(header);
});
break;
case global.protos.POOLTYPE.SOLO:
global.coinFuncs.getBlockHeaderByHash(block.hash, function (header) {
calculateSoloPayments(header);
});
break;
default:
console.log("Unknown payment type. FREAKOUT");
break;
}
global.database.unlockBlock(block.hash);
}
function blockScanner() {
let inc_check = 0;
if (scanInProgress) {
debug("Skipping scan as there's one in progress.");
return;
}
scanInProgress = true;
global.coinFuncs.getLastBlockHeader(function (blockHeader) {
if (lastBlock === blockHeader.height) {
debug("No new work to be performed, block header matches last block");
scanInProgress = false;
return;
}
debug("Parsing data for new blocks");
lastBlock = blockHeader.height;
range.range(0, (blockHeader.height - global.config.payout.blocksRequired)).forEach(function (blockID) {
if (!blockIDCache.hasOwnProperty(blockID)) {
inc_check += 1;
blockQueue.push({blockID: blockID}, function (err) {
debug("Completed block scan on " + blockID);
if (err) {
console.error("Error processing " + blockID);
}
});
}
});
if (inc_check === 0) {
debug("No new work to be performed, initial scan complete");
scanInProgress = false;
}
});
}
function initial_sync() {
console.log("Performing boot-sync");
global.mysql.query("SELECT id, hex FROM block_log WHERE orphan = 0").then(function (rows) {
let intCount = 0;
rows.forEach(function (row) {
intCount += 1;
blockIDCache.push(row.id);
blockHexCache[row.hex] = null;
});
}).then(function () {
// Enable block scanning for 1 seconds to update the block log.
setInterval(blockScanner, 1000);
// Scan every 120 seconds for invalidated blocks
setInterval(blockInvalidator, 120000);
setInterval(blockUnlocker, 120000);
blockInvalidator();
blockUnlocker();
debug("Blocks loaded from SQL: " + blockIDCache.length);
console.log("Boot-sync from SQL complete. Pending completion of queued jobs to get back to work.");
});
}
initial_sync();

@ -0,0 +1,121 @@
"use strict";
const bignum = require('bignum');
const cnUtil = require('cryptonote-util');
const multiHashing = require('multi-hashing');
const crypto = require('crypto');
function Coin(data){
this.data = data;
let instanceId = crypto.randomBytes(4);
this.coinDevAddress = "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A"; // Developer Address
this.poolDevAddress = "44Ldv5GQQhP7K7t3ZBdZjkPA7Kg7dhHwk3ZM3RJqxxrecENSFx27Vq14NAMAd2HBvwEPUVVvydPRLcC69JCZDHLT2X5a4gr"; // Snipa Address
this.blockedAddresses = [
this.coinDevAddress,
this.poolDevAddress,
"43SLUTpyTgXCNXsL43uD8FWZ5wLAdX7Ak67BgGp7dxnGhLmrffDTXoeGm2GBRm8JjigN9PTg2gnShQn5gkgE1JGWJr4gsEU", // Wolf0's address
"42QWoLF7pdwMcTXDviJvNkWEHJ4TXnMBh2Cx6HNkVAW57E48Zfw6wLwDUYFDYJAqY7PLJUTz9cHWB5C4wUA7UJPu5wPf4sZ", // Wolf0's address
"46gq64YYgCk88LxAadXbKLeQtCJtsLSD63NiEc3XHLz8NyPAyobACP161JbgyH2SgTau3aPUsFAYyK2RX4dHQoaN1ats6iT" // Claymore's Fee Address.
];
this.exchangeAddresses = [
"46yzCCD3Mza9tRj7aqPSaxVbbePtuAeKzf8Ky2eRtcXGcEgCg1iTBio6N4sPmznfgGEUGDoBz5CLxZ2XPTyZu1yoCAG7zt6", // Shapeshift.io
"463tWEBn5XZJSxLU6uLQnQ2iY9xuNcDbjLSjkn3XAXHCbLrTTErJrBWYgHJQyrCwkNgYvyV3z8zctJLPCZy24jvb3NiTcTJ", // Bittrex
"44TVPcCSHebEQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV4UpB5n4", // Xmr.to
"47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12noXmi4ZyBZLc99e66NtnKff34fHsGRoyZk3ES1s1V4QVcB" // Poloniex
]; // These are addresses that MUST have a paymentID to perform logins with.
this.prefix = 18;
this.intPrefix = 19;
if (global.config.general.testnet === true){
this.prefix = 53;
this.intPrefix = 54;
}
this.getBlockHeaderByID = function(blockId, callback){
global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) {
if (body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
}
});
};
this.getBlockHeaderByHash = function(blockHash, callback){
global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) {
if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
return callback(false);
}
});
};
this.getLastBlockHeader = function(callback){
global.support.rpcDaemon('getlastblockheader', [], function (body) {
if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){
return callback(body.result.block_header);
} else {
console.error(JSON.stringify(body));
}
});
};
this.getBlockTemplate = function(walletAddress, callback){
global.support.rpcDaemon('getblocktemplate', {
reserve_size: 8,
wallet_address: walletAddress
}, function(body){
return callback(body);
});
};
this.baseDiff = function(){
return bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16);
};
this.validateAddress = function(address){
// This function should be able to be called from the async library, as we need to BLOCK ever so slightly to verify the address.
address = new Buffer(address);
if (cnUtil.address_decode(address) === this.prefix){
return true;
}
return cnUtil.address_decode_integrated(address) === this.intPrefix;
};
this.convertBlob = function(blobBuffer){
return cnUtil.convert_blob(blobBuffer);
};
this.constructNewBlob = function(blockTemplate, NonceBuffer){
return cnUtil.construct_block_blob(blockTemplate, NonceBuffer);
};
this.getBlockID = function(blockBuffer){
return cnUtil.get_block_id(blockBuffer);
};
this.BlockTemplate = function(template) {
this.blob = template.blocktemplate_blob;
this.difficulty = template.difficulty;
this.height = template.height;
this.reserveOffset = template.reserved_offset;
this.buffer = new Buffer(this.blob, 'hex');
instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3);
this.previous_hash = new Buffer(32);
this.buffer.copy(this.previous_hash, 0, 7, 39);
this.extraNonce = 0;
this.nextBlob = function () {
this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset);
return global.coinFuncs.convertBlob(this.buffer).toString('hex');
};
};
this.cryptoNight = multiHashing['cryptonight'];
}
module.exports = Coin;

@ -0,0 +1,51 @@
enum POOLTYPE {
PPLNS = 0;
PPS = 1;
PROP = 2;
SOLO = 3;
}
enum MESSAGETYPE {
SHARE = 0;
BLOCK = 1;
INVALIDSHARE = 2;
}
message WSData {
required MESSAGETYPE msgType = 1;
required string key = 2;
required bytes msg = 3;
required int32 exInt = 4;
}
message InvalidShare{
required string paymentAddress = 1;
optional string paymentID = 2;
required string identifier = 3;
}
message Share {
required int32 shares = 1;
required string paymentAddress = 2;
required bool foundBlock = 3;
optional string paymentID = 4;
required bool trustedShare = 5;
required POOLTYPE poolType = 6;
required int32 poolID = 7;
required int64 blockDiff = 8;
required bool bitcoin = 9;
required int32 blockHeight = 10;
required int64 timestamp = 11;
required string identifier = 12;
}
message Block {
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
optional int64 value = 8;
}

@ -0,0 +1,467 @@
"use strict";
let range = require('range');
let debug = require('debug')('db');
let async = require('async');
function Database(){
this.lmdb = require('node-lmdb');
this.env = null;
this.shareDB = null;
this.blockDB = null;
this.cacheDB = null;
this.dirtyenv = false;
this.initEnv = function(){
global.database.env = new this.lmdb.Env();
global.database.env.open({
path: global.config.db_storage_path,
maxDbs: 10,
mapSize: 24 * 1024 * 1024 * 1024,
noSync: true,
mapAsync: true,
useWritemap: true,
noMetaSync: true,
maxReaders: 512
});
global.database.shareDB = this.env.openDbi({
name: 'shares',
create: true,
dupSort: true,
dupFixed: false,
integerDup: true,
integerKey: true,
keyIsUint32: true
});
global.database.blockDB = this.env.openDbi({
name: 'blocks',
create: true,
integerKey: true,
keyIsUint32: true
});
global.database.cacheDB = this.env.openDbi({
name: 'cache',
create: true
});
global.database.intervalID = setInterval(function(){
global.database.env.sync(function(){});
}, 60000); // Sync the DB every 60 seconds
global.database.dirtyenv = false;
console.log("Database Worker: LMDB Env Initialized.");
};
this.incrementCacheData = function(key, data){
this.refreshEnv();
let txn = this.env.beginTxn();
let cached = txn.getString(this.cacheDB, key);
if (cached !== null){
cached = JSON.parse(cached);
data.forEach(function(intDict){
if (!cached.hasOwnProperty(intDict.location)){
cached[intDict.location] = 0;
}
cached[intDict.location] += intDict.value;
});
txn.putString(this.cacheDB, key, JSON.stringify(cached));
txn.commit();
return;
}
txn.abort();
};
this.getBlockList = function(pool_type){
debug("Getting block list");
switch (pool_type) {
case 'pplns':
pool_type = global.protos.POOLTYPE.PPLNS;
break;
case 'pps':
pool_type = global.protos.POOLTYPE.PPS;
break;
case 'solo':
pool_type = global.protos.POOLTYPE.SOLO;
break;
default:
pool_type = false;
}
let response = [];
try{
this.refreshEnv();
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.blockDB);
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
/*
required string hash = 1;
required int64 difficulty = 2;
required int64 shares = 3;
required int64 timestamp = 4;
required POOLTYPE poolType = 5;
required bool unlocked = 6;
required bool valid = 7;
*/
cursor.getCurrentBinary(function (key, data) { // jshint ignore:line
let blockData = global.protos.Block.decode(data);
let poolType;
switch (blockData.poolType){
case (global.protos.POOLTYPE.PPLNS):
poolType = 'pplns';
break;
case (global.protos.POOLTYPE.SOLO):
poolType = 'solo';
break;
case (global.protos.POOLTYPE.PPS):
poolType = 'pps';
break;
default:
poolType = 'Unknown';
break;
}
if (blockData.poolType === pool_type || pool_type === false) {
response.push({
ts: blockData.timestamp,
hash: blockData.hash,
diff: blockData.difficulty,
shares: blockData.shares,
height: key,
valid: blockData.valid,
unlocked: blockData.unlocked,
pool_type: poolType,
value: blockData.value
});
}
});
}
cursor.close();
txn.abort();
return response.sort(global.support.blockCompare);
} catch (e){
return response;
}
};
this.storeShare = function(blockId, shareData, callback){
// This function needs the blockID in question, and the shareData in binary format.
// The binary data should be packed as per the data.proto Share protobuf format.
try {
let share = global.protos.Share.decode(shareData);
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
this.incrementCacheData('global_stats', [{location: 'totalHashes', value: share.shares}]);
switch (share.poolType) {
case global.protos.POOLTYPE.PPLNS:
this.incrementCacheData('pplns_stats', [{location: 'totalHashes', value: share.shares}]);
break;
case global.protos.POOLTYPE.PPS:
this.incrementCacheData('pps_stats', [{location: 'totalHashes', value: share.shares}]);
break;
case global.protos.POOLTYPE.SOLO:
this.incrementCacheData('solo_stats', [{location: 'totalHashes', value: share.shares}]);
break;
}
this.incrementCacheData(minerIDWithIdentifier, [{location: 'totalHashes', value: share.shares},{location: 'goodShares', value: 1}]);
this.incrementCacheData(minerID, [{location: 'totalHashes', value: share.shares},{location: 'goodShares', value: 1}]);
} catch (e){
callback(false);
return;
}
this.refreshEnv();
let txn = this.env.beginTxn();
txn.putBinary(this.shareDB, blockId, shareData);
txn.commit();
callback(true);
};
this.storeInvalidShare = function(shareData, callback){
try {
let share = global.protos.InvalidShare.decode(shareData);
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
this.incrementCacheData(minerIDWithIdentifier, [{location: 'badShares', value: 1}]);
this.incrementCacheData(minerID, [{location: 'badShares', value: 1}]);
callback(true);
} catch (e){
console.error("Ran into an error string an invalid share. Damn!");
callback(false);
}
};
this.getLastBlock = function(blockType){
this.refreshEnv();
debug("Getting the last block for: "+ blockType);
let txn = this.env.beginTxn({readOnly: true});
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
let highestBlock = 0;
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.poolType === blockType || typeof(blockType) === 'undefined'){
if (found > highestBlock){
highestBlock = found;
}
}
});
}
cursor.close();
txn.commit();
debug("Done getting the last block for: "+ blockType + " height of: "+ highestBlock);
return highestBlock;
};
this.calculateShares = function(blockData, blockHeight){
debug("Calculating shares for "+ blockData.hash);
this.refreshEnv();
let lastBlock = this.getLastBlock(blockData.poolType);
let shareCount = 0;
range.range(lastBlock+1, blockHeight+1).forEach(function (blockID) {
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockID) === blockID); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
try{
let shareData = global.protos.Share.decode(data);
if (shareData.poolType === blockData.poolType){
shareCount = shareCount + shareData.shares;
}
} catch(e){
console.error("Invalid share");
}
});
}
cursor.close();
txn.commit();
});
blockData.shares = shareCount;
debug("Share calculator for "+ blockData.hash + " complete, found " + shareCount + " shares.");
return global.protos.Block.encode(blockData);
};
this.storeBlock = function(blockId, blockData, callback){
this.refreshEnv();
try{
let blockDataDecoded = global.protos.Block.decode(blockData);
global.coinFuncs.getBlockHeaderByHash(blockDataDecoded.hash, function(header){
if (typeof(header) === 'undefined' || !header){
return callback(false);
}
blockDataDecoded.value = header.reward;
blockData = global.database.calculateShares(blockDataDecoded, blockId);
let txn = global.database.env.beginTxn();
txn.putBinary(global.database.blockDB, blockId, blockData);
txn.commit();
return callback(true);
});
} catch (e) {
console.error("ERROR IN STORING BLOCK. LOOK INTO ME PLZ: " + JSON.stringify(e));
throw new Error("Error in block storage");
}
};
this.fixBlockShares = function(blockId){
let txn = global.database.env.beginTxn();
let blockData = txn.getBinary(global.database.blockDB, blockId);
txn.abort();
let blockDataDecoded = global.protos.Block.decode(blockData);
global.coinFuncs.getBlockHeaderByHash(blockDataDecoded.hash, function(header){
blockDataDecoded.value = header.reward;
blockData = global.database.calculateShares(blockDataDecoded, blockId);
let txn = global.database.env.beginTxn();
txn.putBinary(global.database.blockDB, blockId, blockData);
txn.commit();
});
};
this.invalidateBlock = function(blockId){
this.refreshEnv();
let txn = this.env.beginTxn();
let blockData = global.protos.Block.decode(txn.getBinary(this.blockDB, blockId));
blockData.valid = false;
txn.putBinary(this.blockDB, blockId, global.protos.Block.encode(blockData));
txn.commit();
};
this.getValidLockedBlocks = function(){
this.refreshEnv();
let txn = this.env.beginTxn({readOnly: true});
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
let blockList = [];
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.valid === true && blockData.unlocked === false){
blockData.height = key;
blockList.push(blockData);
}
});
}
cursor.close();
txn.commit();
return blockList;
};
this.unlockBlock = function(blockHex){
this.refreshEnv();
let txn = this.env.beginTxn();
let cursor = new this.lmdb.Cursor(txn, this.blockDB);
for (let found = cursor.goToFirst(); found; found = cursor.goToNext()) {
let blockDB = this.blockDB;
cursor.getCurrentBinary(function(key, data){ // jshint ignore:line
let blockData = global.protos.Block.decode(data);
if (blockData.hash === blockHex){
blockData.unlocked = true;
txn.putBinary(blockDB, key, global.protos.Block.encode(blockData));
}
});
blockDB = null;
}
cursor.close();
txn.commit();
};
this.getCache = function(cacheKey){
debug("Getting Key: "+cacheKey);
try {
this.refreshEnv();
let txn = this.env.beginTxn({readOnly: true});
let cached = txn.getString(this.cacheDB, cacheKey);
txn.abort();
if (cached !== null){
debug("Result for Key: " + cacheKey + " is: " + cached);
return JSON.parse(cached);
}
} catch (e) {
return false;
}
return false;
};
this.setCache = function(cacheKey, cacheData){
debug("Setting Key: "+cacheKey+ " Data: " + JSON.stringify(cacheData));
this.refreshEnv();
let txn = this.env.beginTxn();
txn.putString(this.cacheDB, cacheKey, JSON.stringify(cacheData));
txn.commit();
};
this.cleanShareDB = function() {
/*
This function takes the difficulty of the current block, and the last PPS block. If it's 0, save everything,
UNLESS global.config.pps.enable is FALSE, then feel free to trash it.
Due to LMDB under current config, we must delete entire keys, due to this, we save diff * shareMultiLog * 1.5
global.config.pplns.shareMultiLog should be at least 1.5x your shareMulti, in case of diff spikiing
*/
let lastPPSBlock = this.getLastBlock();
if (global.config.pps.enable){
lastPPSBlock = this.getLastBlock(global.protos.POOLTYPE.PPS);
if (lastPPSBlock === 0){
return;
}
}
let lastPPLNSBlock = this.getLastBlock(global.protos.POOLTYPE.PPLNS);
debug("Last PPS block: "+lastPPSBlock);
// Hopping into async, we need the current block height to know where to start our indexing...
async.waterfall([
function(callback){
global.coinFuncs.getLastBlockHeader(function(body){
callback(null, body.height, Math.floor(body.difficulty * 1.5 * global.config.pplns.shareMultiLog));
});
},
function (lastBlock, difficulty, callback) {
let shareCount = 0;
let ppsFound = false;
let pplnsFound = false;
let blockList = [];
debug("Scanning from: "+lastBlock + " for more than: " + difficulty + " shares");
range.range(0, lastBlock+1).forEach(function (blockID) {
blockID = (blockID - lastBlock+1) * -1;
if (blockID < 0){
return;
}
debug("Scanning block: " + blockID);
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(blockID) === blockID); found; found = cursor.goToNextDup()) {
if (ppsFound && pplnsFound){
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
if (blockList.indexOf(key) === -1){
blockList.push(key);
}
});
} else {
cursor.getCurrentBinary(function(key, data) { // jshint ignore:line
if (key < lastPPSBlock){
ppsFound = true;
}
try{
let shareData = global.protos.Share.decode(data);
if (shareData.poolType === global.protos.POOLTYPE.PPLNS){
shareCount = shareCount + shareData.shares;
}
} catch(e){
console.error("Invalid share");
}
});
if (shareCount >= difficulty){
pplnsFound = true;
}
}
}
cursor.close();
txn.abort();
});
callback(null, blockList);
}
], function(err, data){
if (global.config.general.blockCleaner === true){
if(data.length > 0){
global.database.refreshEnv();
let blockList = global.database.getBlockList();
debug("Got the block list");
let totalDeleted = 0;
data.forEach(function(block){
if ((blockList.indexOf(block) !== -1 && !blockList.unlocked) || block > lastPPLNSBlock){
// Don't delete locked blocks. ffs.
// Don't delete blocks that could contain shares. Even if it's unlikely as all getout.
debug("Skipped deleting block: " + block);
return;
}
totalDeleted += 1;
let txn = global.database.env.beginTxn();
txn.del(global.database.shareDB, block);
txn.commit();
debug("Deleted block: " + block);
});
console.log("Block cleaning enabled. Removed: " +totalDeleted+ " block share records");
}
global.database.env.sync(function(){
});
} else {
console.log("Block cleaning disabled. Would of removed: " + JSON.stringify(data));
}
});
};
this.refreshEnv = function(){
if (this.dirtyenv === true){
console.log("Database Worker: Reloading LMDB Env");
global.database.env.sync(function(){
});
clearInterval(this.intervalID);
global.database.env.close();
this.initEnv();
}
};
setInterval(function(){
global.database.dirtyenv = true;
}, 900000); // Set DB env reload for every 15 minutes.
}
module.exports = Database;

@ -0,0 +1,10 @@
"use strict";
console.log("Cleaning up the share DB");
global.database.cleanShareDB();
console.log("Done cleaning up the shareDB");
setInterval(function(){
console.log("Cleaning up the share DB");
global.database.cleanShareDB();
console.log("Done cleaning up the shareDB");
}, 3600000);

@ -0,0 +1,668 @@
"use strict";
const shapeshift = require('shapeshift.io');
const async = require("async");
const debug = require("debug")("payments");
const request = require('request-json');
const range = require('range');
let hexChars = new RegExp("[0-9a-f]+");
let bestExchange = global.config.payout.bestExchange;
let xmrAPIClient = request.createClient('https://xmr.to/api/v1/xmr2btc/');
let shapeshiftQueue = async.queue(function (task, callback) {
// Amount needs to be shifted in as a non-completed value, as the wallet will only take non-complete values..
let amount = task.amount -= task.fee;
// Address is the destination address IN BTC.
let address = task.address;
// PaymentIDs are the paymentID's to flag as paid by this transaction.
// Should be a massive list of ID's so we can bulk-update them, by merging them with 's.
// Here we go! General process: Scan shapeshift for valid amounts of funds to xfer around.
// Once there's enough funds, then we active txn
// Do a wallet call to xfer.
// Setup a monitor on the transaction
async.waterfall([
function (intCallback) {
// Verify if the coin is active in ShapeShift first.
shapeshift.coins(function (err, coinData) {
if (err) {
intCallback(err);
} else if (!coinData.hasOwnProperty(global.config.general.coinCode) || coinData[global.config.general.coinCode].status !== "available") {
intCallback("Coin " + global.config.general.coinCode + " Is not available at this time on shapeshift.");
} else {
intCallback(null);
}
});
},
function (intCallback) {
// Get the market information from shapeshift, which includes deposit limits, minimum deposits, rates, etc.
shapeshift.marketInfo(global.config.payout.shapeshiftPair, function (err, marketInfo) {
if (err) {
intCallback(err);
} else if (!marketInfo.hasOwnProperty("limit") || marketInfo.limit <= global.support.coinToDecimal(amount)) {
intCallback("Not enough coin in shapeshift to process at this time.");
} else if (!marketInfo.hasOwnProperty("min") || marketInfo.min >= global.support.coinToDecimal(amount)) {
intCallback("Not enough coin to hit the shapeshift minimum deposits.");
} else {
intCallback(null, marketInfo);
}
});
},
function (marketInfo, intCallback) {
// Validated there's enough coin. Time to make our dank txn.
// Return:
/*
{
"orderId": "cc49c556-e645-4c15-a943-d50a935274e4",
"sAddress": "46yzCCD3Mza9tRj7aqPSaxVbbePtuAeKzf8Ky2eRtcXGcEgCg1iTBio6N4sPmznfgGEUGDoBz5CLxZ2XPTyZu1yoCAG7zt6",
"deposit": "d8041668718e6e9d9d0fd335ee5ecd923e6fd074c41316d041cc18b779ade10e",
"depositType": "XMR",
"withdrawal": "1DbxcoCBSA9N7uZvkcvWxuLxSau9q9Pwiu",
"withdrawalType": "BTC",
"public": null,
"apiPubKey": "shapeshift",
"returnAddress": "46XWBqE1iwsVxSDP1qDrxhE1XvsZV6eALG5LwnoMdjbT4GPdy2bZTb99kagzxp2MMjUamTYZ4WgvZdFadvMimTjvR6Gv8hL",
"returnAddressType": "XMR"
}
Valid Statuses:
"received"
"complete"
"error"
"no_deposits"
Complete State Information:
{
"status": "complete",
"address": "d8041668718e6e9d9d0fd335ee5ecd923e6fd074c41316d041cc18b779ade10e",
"withdraw": "1DbxcoCBSA9N7uZvkcvWxuLxSau9q9Pwiu",
"incomingCoin": 3,
"incomingType": "XMR",
"outgoingCoin": "0.04186155",
"outgoingType": "BTC",
"transaction": "be9d97f6fc75262151f8f63e035c6ed638b9eb2a4e93fef43ea63124b045dbfb"
}
*/
shapeshift.shift(address, global.config.payout.shapeshiftPair, {returnAddress: global.config.pool.address}, function (err, returnData) {
if (err) {
intCallback(err);
} else {
global.mysql.query("INSERT INTO shapeshiftTxn (id, address, paymentID, depositType, withdrawl, withdrawlType, returnAddress, returnAddressType, txnStatus) VALUES (?,?,?,?,?,?,?,?,?)",
[returnData.orderId, returnData.sAddress, returnData.deposit, returnData.depositType, returnData.withdrawl, returnData.withdrawlType, returnData.returnAddress, returnData.returnAddressType, 'no_deposits']).then(function () {
intCallback(null, marketInfo, returnData);
}).catch(function (error) {
intCallback(error);
});
}
});
},
function (marketInfo, shapeshiftTxnData, intCallback) {
// Make the payment to ShapeShift
let paymentDetails = {
destinations: [
{
amount: amount,
address: shapeshiftTxnData.sAddress
}
],
mixin: global.config.payout.mixIn,
payment_id: shapeshiftTxnData.deposit
};
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
intCallback(null, marketInfo, shapeshiftTxnData, body);
} else {
intCallback("Unknown error from the wallet.");
}
});
},
function (marketInfo, shapeshiftTxnData, body, intCallback) {
// body.tx_hash = XMR transaction hash.
// Need to add transaction.
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees, exchange_rate, exchange_name, exchange_txn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[1, address, null, task.amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, global.support.decimalToCoin(marketInfo.minerFee), 1, global.support.decimalToCoin(marketInfo.rate), 'shapeshift', shapeshiftTxnData.orderId]).then(function (result) {
intCallback(null, result.insertId);
}).catch(function (error) {
intCallback(error);
});
}
], function (err, result) {
if (err) {
console.error("Error processing shapeshift txn: " + JSON.stringify(err));
callback();
} else {
// Need to fill out this data pronto!
console.log("Processed ShapeShift transaction for: " + address + " Paid out: " + result + " payments in the db");
callback(null, result);
}
});
}, 2);
let xmrToQueue = async.queue(function (task, callback) {
// http://xmrto-api.readthedocs.io/en/latest/introduction.html
// Documentation looks good!
// Amount needs to be shifted in as a non-completed value, as the wallet will only take non-complete values..
let amount = task.amount -= task.fee;
// Address is the destination address IN BTC.
let address = task.address;
// PaymentIDs are the paymentID's to flag as paid by this transaction.
// Should be a massive list of ID's so we can bulk-update them, by merging them with 's.
// Here we go! General process: Scan shapeshift for valid amounts of funds to xfer around.
// Once there's enough funds, then we active txn
// Do a wallet call to xfer.
// Setup a monitor on the transaction
async.waterfall([
function (intCallback) {
// Verify if XMR.to is ready to get to work.
xmrAPIClient.get('order_parameter_query/', function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
let amtOfBTC = ((amount / global.config.general.sigDivisor) * body.price).toPrecision(8);
console.log("Attempting to pay: " + address + " Amount: " + amtOfBTC + " BTC or " + amount / global.config.general.sigDivisor + " XMR");
console.log("Response from XMR.to: " + JSON.stringify(body));
if (body.lower_limit >= amtOfBTC) {
return intCallback("Not enough XMR to hit the minimum deposit");
} else if (body.upper_limit <= amtOfBTC) {
return intCallback("Too much XMR to pay out to xmr.to");
} else {
return intCallback(null, amtOfBTC);
}
}
});
},
function (btcValue, intCallback) {
// Validated there's enough coin. Time to make our dank txn.
// Return:
/*
{
"state": "TO_BE_CREATED",
"btc_amount": <requested_amount_in_btc_as_float>,
"btc_dest_address": "<requested_destination_address_as_string>",
"uuid": "<unique_order_identifier_as_12_character_string>"
}
Valid Statuses:
"TO_BE_CREATED"
"UNPAID"
"UNDERPAID"
"PAID_UNCONFIRMED"
"PAID"
"BTC_SENT"
"TIMED_OUT"
"NOT_FOUND"
// Create, then immediately update with the new information w/ a status call.
*/
console.log("Amount of BTC to pay: " + btcValue);
xmrAPIClient.post('order_create/', {
btc_amount: btcValue,
btc_dest_address: address
}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
return intCallback(null, body.uuid);
}
});
},
function (txnID, intCallback) {
// This function only exists because xmr.to is a pretty little fucking princess.
async.doUntil(function (xmrCallback) {
xmrAPIClient.post('order_status_query/', {uuid: txnID}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
xmrCallback(null, body.state);
}
});
},
function (xmrCallback) {
return xmrCallback !== "TO_BE_CREATED";
},
function () {
intCallback(null, txnID);
});
},
function (txnID, intCallback) {
xmrAPIClient.post('order_status_query/', {uuid: txnID}, function (err, res, body) {
if (err) {
return intCallback(err);
} else if (body.error_msg) {
return intCallback(body.error_msg);
} else {
console.log(JSON.stringify(body));
global.mysql.query("INSERT INTO xmrtoTxn (id, address, paymentID, depositType, withdrawl, withdrawlType, returnAddress, returnAddressType, txnStatus, amountDeposited, amountSent) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
[txnID, body.xmr_receiving_address, body.xmr_required_payment_id, 'XMR', body.btc_dest_address, 'BTC', global.config.pool.address, 'XMR', body.state_str, global.support.decimalToCoin(body.xmr_amount_total), global.support.decimalToCoin(body.btc_amount)]).then(function () {
return intCallback(null, body, global.support.decimalToCoin(body.xmr_amount_total));
}).catch(function (error) {
return intCallback(error);
});
}
});
},
function (orderStatus, xmrDeposit, intCallback) {
// Make the payment to ShapeShift
let paymentDetails = {
destinations: [
{
amount: xmrDeposit,
address: orderStatus.xmr_receiving_address
}
],
mixin: global.config.payout.mixIn,
payment_id: orderStatus.xmr_required_payment_id
};
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
return intCallback(null, orderStatus, body);
} else {
return intCallback("Unknown error from the wallet.");
}
});
},
function (orderStatus, body, intCallback) {
// body.tx_hash = XMR transaction hash.
// Need to add transaction.
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees, exchange_rate, exchange_name, exchange_txn_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
[1, address, null, global.support.decimalToCoin(orderStatus.xmr_amount_total), body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1, global.support.decimalToCoin(orderStatus.xmr_price_btc), 'xmrto', orderStatus.uuid]).then(function (result) {
return intCallback(null, result.insertId);
}).catch(function (error) {
return intCallback(error);
});
}
], function (err, result) {
if (err) {
console.error("Error processing XMRTo txn: " + JSON.stringify(err));
return callback("Error!");
} else {
// Need to fill out this data pronto!
console.log("Processed XMRTo transaction for: " + address + " Paid out: " + result + " payments in the db");
return callback(null, result);
}
});
}, 2);
let paymentQueue = async.queue(function (paymentDetails, callback) {
/*
support JSON URI: http://10.0.0.2:28082/json_rpc Args: {"id":"0","jsonrpc":"2.0","method":"transfer","params":{"destinations":[{"amount":68130252045355,"address":"A2MSrn49ziBPJBh8ZNEhhbfyLMou6mao4C1F5TLGUatmUnCxZArDYkcbAnVkVEopWVeak2rKDrmc8JpoS7n5dvfN9YDPBTG"}],"mixin":4,"payment_id":"7e52c5266de9fede7fb3abc0cd88f937b38b51426f7b34ff99729d28ce4e1142"}} +1ms
payments Payment made: {"id":"0","jsonrpc":"2.0","result":{"fee":40199391255,"tx_hash":"c418708643f72635edf522490bfb2cae9d42a6dc1df30dcde844862dfd88f5b3","tx_key":""}} +2s
*/
debug("Making payment based on: " + JSON.stringify(paymentDetails));
let transferFunc = 'transfer';
global.support.rpcWallet(transferFunc, paymentDetails, function (body) {
debug("Payment made: " + JSON.stringify(body));
if (body.hasOwnProperty('error')) {
console.error("Issue making payments" + JSON.stringify(body.error));
console.error("Will not make more payments until the payment daemon is restarted!");
//toAddress, subject, body
global.support.sendEmail(global.config.general.adminEmail, "Payment daemon unable to make payment",
"Hello,\r\nThe payment daemon has hit an issue making a payment: " + JSON.stringify(body.error) +
". Please investigate and restart the payment daemon as appropriate");
return;
}
if (paymentDetails.hasOwnProperty('payment_id')) {
console.log("Payment made to " + paymentDetails.destinations[0].address + " with PaymentID: " + paymentDetails.payment_id + " For: " + global.support.coinToDecimal(paymentDetails.destinations[0].amount) + " XMR with a " + global.support.coinToDecimal(body.result.fee) + " XMR Mining Fee");
return callback(body.result);
} else {
if (transferFunc === 'transfer') {
console.log("Payment made out to multiple people, total fee: " + global.support.coinToDecimal(body.result.fee) + " XMR");
}
let intCount = 0;
paymentDetails.destinations.forEach(function (details) {
console.log("Payment made to: " + details.address + " For: " + global.support.coinToDecimal(details.amount) + " XMR");
intCount += 1;
if (intCount === paymentDetails.destinations.length) {
return callback(body.result);
}
});
}
});
}, 1);
function updateShapeshiftCompletion() {
global.mysql.query("SELECT * FROM shapeshiftTxn WHERE txnStatus NOT IN ('complete', 'error')").then(function (rows) {
rows.forEach(function (row) {
shapeshift.status(row.paymentID, function (err, status, returnData) {
if (err) {
return;
}
global.mysql.query("UPDATE shapeshiftTxn SET txnStatus = ? WHERE id = ?", [status, row.id]).then(function () {
if (status === 'complete') {
global.mysql.query("UPDATE shapeshiftTxn SET amountDeposited = ?, amountSent = ?, transactionHash = ? WHERE id = ?",
[global.support.decimalToCoin(returnData.incomingCoin), global.support.bitcoinDecimalToCoin(returnData.outgoingCoin), returnData.transaction, row.id]).then(function () {
global.mysql.query("UPDATE transactions SET confirmed = 1, confirmed_time = now(), btc_amt = ? WHERE exchange_txn_id = ?", [global.support.bitcoinDecimalToCoin(returnData.outgoingCoin), row.id]);
});
} else if (status === 'error') {
// Failed txn. Need to rollback and delete all related data. Here we go!
global.mysql.query("DELETE FROM shapeshiftTxn WHERE id = ?", [row.id]);
global.mysql.query("SELECT id, xmr_amt, address FROM transactions WHERE exchange_txn_id = ?", [row.id]).then(function (rows) {
global.mysql.query("DELETE FROM transactions WHERE id = ?", [rows[0].id]);
global.mysql.query("DELETE payments WHERE transaction_id = ?", [rows[0].id]);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE payment_address = ? limit 1", [rows[0].xmr_amt, rows[0].address]);
});
console.error("Failed transaction from ShapeShift " + JSON.stringify(returnData));
}
});
});
});
});
}
function updateXMRToCompletion() {
global.mysql.query("SELECT * FROM xmrtoTxn WHERE txnStatus NOT IN ('PAID', 'TIMED_OUT', 'NOT_FOUND', 'BTC_SENT')").then(function (rows) {
rows.forEach(function (row) {
xmrAPIClient.post('order_status_query/', {uuid: row.id}, function (err, res, body) {
if (err) {
console.log("Error in getting order status: " + JSON.stringify(err));
return;
}
if (body.error_msg) {
console.log("Error in getting order status: " + body.error_msg);
return;
}
global.mysql.query("UPDATE xmrtoTxn SET txnStatus = ? WHERE id = ?", [body.state, row.id]).then(function () {
if (body.status === 'BTC_SENT') {
global.mysql.query("UPDATE xmrtoTxn SET transactionHash = ? WHERE id = ?", [body.btc_transaction_id, row.id]).then(function () {
global.mysql.query("UPDATE transactions SET confirmed = 1, confirmed_time = now(), btc_amt = ? WHERE exchange_txn_id = ?", [global.support.bitcoinDecimalToCoin(body.btc_amount), row.id]);
});
} else if (body.status === 'TIMED_OUT' || body.status === 'NOT_FOUND') {
global.mysql.query("DELETE FROM xmrtoTxn WHERE id = ?", [row.id]);
global.mysql.query("SELECT id, xmr_amt, address FROM transactions WHERE exchange_txn_id = ?", [row.id]).then(function (rows) {
global.mysql.query("DELETE FROM transactions WHERE id = ?", [rows[0].id]);
global.mysql.query("DELETE payments WHERE transaction_id = ?", [rows[0].id]);
global.mysql.query("UPDATE balance SET amount = amount+? WHERE payment_address = ? limit 1", [rows[0].xmr_amt, rows[0].address]);
});
console.error("Failed transaction from XMRto " + JSON.stringify(body));
}
});
});
});
});
}
function determineBestExchange() {
async.waterfall([
function (callback) {
// Verify if the coin is active in ShapeShift first.
shapeshift.coins(function (err, coinData) {
if (err) {
return callback(err);
} else if (!coinData.hasOwnProperty(global.config.general.coinCode) || coinData[global.config.general.coinCode].status !== "available") {
return callback("Coin " + global.config.general.coinCode + " Is not available at this time on shapeshift.");
} else {
return callback(null);
}
});
},
function (callback) {
// Get the market information from shapeshift, which includes deposit limits, minimum deposits, rates, etc.
shapeshift.marketInfo(global.config.payout.shapeshiftPair, function (err, marketInfo) {
if (err) {
return callback(err);
} else if (!marketInfo.hasOwnProperty("rate")) {
return callback("Shapeshift did not return the rate.");
} else {
return callback(null, global.support.bitcoinDecimalToCoin(marketInfo.rate));
}
});
},
function (ssValue, callback) {
xmrAPIClient.get('order_parameter_query/', function (err, res, body) {
console.log("XMR.to pricing body: " + JSON.stringify(body));
if (err) {
return callback(err);
} else if (body.error_msg) {
return callback(body.error_msg);
} else {
return callback(null, ssValue, global.support.bitcoinDecimalToCoin(body.price));
}
});
}
], function (err, ssValue, xmrToValue) {
if (err) {
return console.error("Error processing exchange value: " + JSON.stringify(err));
}
debug("ShapeShift Value: " + global.support.bitcoinCoinToDecimal(ssValue) + " XMR.to Value: " + global.support.bitcoinCoinToDecimal(xmrToValue));
if (ssValue >= xmrToValue) {
console.log("ShapeShift is the better BTC exchange, current rate: " + global.support.bitcoinCoinToDecimal(ssValue));
bestExchange = 'shapeshift';
global.mysql.query("UPDATE config SET item_value = 'shapeshift' where item='bestExchange'");
global.mysql.query("UPDATE config SET item_value = ? where item='exchangeRate'", [ssValue]);
} else {
console.log("XMR.to is the better BTC exchange, current rate: " + global.support.bitcoinCoinToDecimal(xmrToValue));
bestExchange = 'xmrto';
global.mysql.query("UPDATE config SET item_value = 'xmrto' where item='bestExchange'");
global.mysql.query("UPDATE config SET item_value = ? where item='exchangeRate'", [xmrToValue]);
}
});
}
function Payee(amount, address, paymentID, bitcoin) {
this.amount = amount;
this.address = address;
this.paymentID = paymentID;
this.bitcoin = bitcoin;
this.blockID = 0;
this.poolType = '';
this.transactionID = 0;
this.sqlID = 0;
if (paymentID === null) {
this.id = address;
} else {
this.id = address + "." + paymentID;
}
this.fee = 0;
this.baseFee = global.support.decimalToCoin(global.config.payout.feeSlewAmount);
this.setFeeAmount = function () {
if (this.amount <= global.support.decimalToCoin(global.config.payout.walletMin)) {
this.fee = this.baseFee;
} else if (this.amount <= global.support.decimalToCoin(global.config.payout.feeSlewEnd)) {
let feeValue = this.baseFee / (global.support.decimalToCoin(global.config.payout.feeSlewEnd) - global.support.decimalToCoin(global.config.payout.walletMin));
this.fee = this.baseFee - ((this.amount - global.support.decimalToCoin(global.config.payout.walletMin)) * feeValue);
}
this.fee = Math.floor(this.fee);
};
this.makePaymentWithID = function () {
let paymentDetails = {
destinations: [
{
amount: this.amount - this.fee,
address: this.address
}
],
mixin: global.config.payout.mixIn,
payment_id: this.paymentID
};
let identifier = this.id;
let amount = this.amount;
let address = this.address;
let paymentID = this.paymentID;
let payee = this;
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
debug("Successful payment sent to: " + identifier);
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[0, address, paymentID, amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1]).then(function (result) {
payee.transactionID = result.insertId;
payee.trackPayment();
});
} else {
console.error("Unknown error from the wallet.");
}
});
};
this.makePaymentAsIntegrated = function () {
let paymentDetails = {
destinations: [
{
amount: this.amount - this.fee,
address: this.address
}
],
mixin: global.config.payout.mixIn
};
let identifier = this.id;
let amount = this.amount;
let address = this.address;
let payee = this;
debug("Payment Details: " + JSON.stringify(paymentDetails));
paymentQueue.push(paymentDetails, function (body) {
if (body.fee && body.fee > 10) {
debug("Successful payment sent to: " + identifier);
global.mysql.query("INSERT INTO transactions (bitcoin, address, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?)",
[0, address, amount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, 1]).then(function (result) {
payee.transactionID = result.insertId;
payee.trackPayment();
});
} else {
console.error("Unknown error from the wallet.");
}
});
};
this.makeBitcoinPayment = function () {
let functionalData = {address: this.address, amount: this.amount};
let payee = this;
if (bestExchange === 'xmrto') {
xmrToQueue.push(functionalData, function (err, transactionID) {
if (err) {
return console.error("Error processing payment for " + functionalData.address);
}
payee.transactionID = transactionID;
payee.trackPayment();
});
} else {
shapeshiftQueue.push(functionalData, function (err, transactionID) {
if (err) {
return console.error("Error processing payment for " + functionalData.address);
}
payee.transactionID = transactionID;
payee.trackPayment();
});
}
};
this.trackPayment = function () {
global.mysql.query("UPDATE balance SET amount = amount - ? WHERE id = ?", [this.amount, this.sqlID]);
global.mysql.query("INSERT INTO payments (unlocked_time, paid_time, pool_type, payment_address, transaction_id, bitcoin, amount, payment_id, transfer_fee)" +
" VALUES (now(), now(), ?, ?, ?, ?, ?, ?, ?)", [this.poolType, this.address, this.transactionID, this.bitcoin, this.amount - this.fee, this.paymentID, this.fee]);
};
}
function makePayments() {
global.mysql.query("SELECT * FROM balance WHERE amount >= ?", [global.support.decimalToCoin(global.config.payout.walletMin)]).then(function (rows) {
console.log("Loaded all payees into the system for processing");
let paymentDestinations = [];
let totalAmount = 0;
let roundCount = 0;
let payeeList = [];
let payeeObjects = {};
rows.forEach(function (row) {
debug("Starting round for: " + JSON.stringify(row));
let payee = new Payee(row.amount, row.payment_address, row.payment_id, row.bitcoin);
payeeObjects[row.payment_address] = payee;
global.mysql.query("SELECT payout_threshold FROM users WHERE username = ?", [payee.id]).then(function (userRow) {
roundCount += 1;
let threshold = 0;
if (userRow.length !== 0) {
threshold = userRow[0].payout_threshold;
}
payee.poolType = row.pool_type;
payee.sqlID = row.id;
if (payee.poolType === "fees" && payee.address === global.config.payout.feeAddress && payee.amount >= ((global.support.decimalToCoin(global.config.payout.feesForTXN) + global.support.decimalToCoin(global.config.payout.exchangeMin)))) {
debug("This is the fee address internal check for value");
payee.amount -= global.support.decimalToCoin(global.config.payout.feesForTXN);
} else if (payee.address === global.config.payout.feeAddress && payee.poolType === "fees") {
debug("Unable to pay fee address.");
payee.amount = 0;
}
let remainder = payee.amount % (global.config.payout.denom * global.config.general.sigDivisor);
if (remainder !== 0) {
payee.amount -= remainder;
}
if (payee.amount > threshold) {
payee.setFeeAmount();
if (payee.bitcoin === 0 && payee.paymentID === null && payee.amount !== 0 && payee.amount > 0 && payee.address.length !== 106) {
debug("Adding " + payee.id + " to the list of people to pay (OG Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
paymentDestinations.push({amount: payee.amount - payee.fee, address: payee.address});
totalAmount += payee.amount;
payeeList.push(payee);
} else if (payee.bitcoin === 0 && payee.paymentID === null && payee.amount !== 0 && payee.amount > 0 && payee.address.length === 106 && (payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0))) {
// Special code to handle integrated payment addresses. What a pain in the rear.
// These are exchange addresses though, so they need to hit the exchange payout amount.
debug("Adding " + payee.id + " to the list of people to pay (Integrated Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makePaymentAsIntegrated();
} else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0)) && payee.bitcoin === 0) {
debug("Adding " + payee.id + " to the list of people to pay (Payment ID Address). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makePaymentWithID();
} else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && threshold !== 0)) && payee.bitcoin === 1) {
debug("Adding " + payee.id + " to the list of people to pay (Bitcoin Payout). Payee balance: " + global.support.coinToDecimal(payee.amount));
payee.makeBitcoinPayment();
}
}
debug("Went: " + roundCount + " With: " + paymentDestinations.length + " Possible destinations and: " + rows.length + " Rows");
if (roundCount === rows.length && paymentDestinations.length > 0) {
while (paymentDestinations.length > 0) {
let paymentDetails = {
destinations: paymentDestinations.splice(0, global.config.payout.maxPaymentTxns),
mixin: global.config.payout.mixIn
};
console.log("Paying out: " + paymentDetails.destinations.length + " people");
paymentQueue.push(paymentDetails, function (body) {
// This is the only section that could potentially contain multiple txns. Lets do this safely eh?
if (body.fee && body.fee > 10) {
debug("Made it to the SQL insert for transactions");
let totalAmount = 0;
paymentDetails.destinations.forEach(function (payeeItem) {
totalAmount += payeeObjects[payeeItem.address].amount;
totalAmount += payeeObjects[payeeItem.address].fee;
});
global.mysql.query("INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
[0, null, null, totalAmount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, body.fee, paymentDetails.destinations.length]).then(function (result) {
paymentDetails.destinations.forEach(function (payeeItem) {
payee = payeeObjects[payeeItem.address];
payee.transactionID = result.insertId;
payee.trackPayment();
});
});
} else {
console.error("Unknown error from the wallet.");
}
});
}
}
});
});
});
}
function init() {
determineBestExchange();
global.support.rpcWallet("store", [], function () {
});
if (global.config.allowBitcoin) {
setInterval(updateXMRToCompletion, 90000);
setInterval(updateShapeshiftCompletion, 90000);
}
setInterval(function () {
global.support.rpcWallet("sweep_dust", [], function () {
});
}, 86400000 * 3);
setInterval(function () {
global.support.rpcWallet("store", [], function () {
});
}, 60000);
setInterval(determineBestExchange, 60000);
setInterval(makePayments, 7200000);
makePayments();
}
init();

@ -0,0 +1,834 @@
"use strict";
const debug = require('debug')('pool');
const uuidV4 = require('uuid/v4');
const crypto = require('crypto');
const bignum = require('bignum');
const cluster = require('cluster');
const btcValidator = require('wallet-address-validator');
const async = require('async');
const net = require('net');
const tls = require('tls');
const fs = require('fs');
let nonceCheck = new RegExp("^[0-9a-f]{8}$");
let bannedIPs = [];
let bannedAddresses = [];
let baseDiff = global.coinFuncs.baseDiff();
let pastBlockTemplates = global.support.circularBuffer(4);
let activeMiners = [];
let activeBlockTemplate;
let workerList = [];
let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 18\n\nMining Pool Online';
let threadName;
let minerCount = [];
let BlockTemplate = global.coinFuncs.BlockTemplate;
Buffer.prototype.toByteArray = function () {
return Array.prototype.slice.call(this, 0);
};
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
function registerPool() {
global.mysql.query("SELECT * FROM pools WHERE id = ?", [global.config.pool_id]).then(function (rows) {
rows.forEach(function (row) {
if (row.ip !== global.config.bind_ip) {
console.error("Pool ID in use already for a different IP. Update MySQL or change pool ID.");
process.exit(1);
}
});
}).then(function () {
global.mysql.query("INSERT INTO pools (id, ip, last_checkin, active, hostname) VALUES (?, ?, now(), ?, ?) ON DUPLICATE KEY UPDATE last_checkin=now(), active=?",
[global.config.pool_id, global.config.bind_ip, true, global.config.hostname, true]);
global.mysql.query("DELETE FROM ports WHERE pool_id = ?", [global.config.pool_id]).then(function () {
global.config.ports.forEach(function (port) {
if ('ssl' in port && port['ssl'] === true) {
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 1)",
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
} else {
global.mysql.query("INSERT INTO ports (pool_id, network_port, starting_diff, port_type, description, hidden, ip_address, ssl_port) values (?, ?, ?, ?, ?, ?, ?, 0)",
[global.config.pool_id, port.port, port.difficulty, port.portType, port.desc, port.hidden, global.config.bind_ip]);
}
});
});
});
}
// Master/Slave communication Handling
function messageHandler(message) {
switch (message.type) {
case 'banIP':
debug(threadName + "Received ban IP update from nodes");
if (cluster.isMaster) {
sendToWorkers(message);
} else {
bannedIPs.push(message.data);
}
break;
case 'newBlockTemplate':
debug(threadName + "Received new block template");
if (cluster.isMaster) {
sendToWorkers(message);
newBlockTemplate(message.data);
} else {
newBlockTemplate(message.data);
}
break;
case 'removeMiner':
if (cluster.isMaster) {
minerCount[message.data] -= 1;
}
break;
case 'newMiner':
if (cluster.isMaster) {
minerCount[message.data] += 1;
}
break;
}
}
process.on('message', messageHandler);
function sendToWorkers(data) {
let goodWorkers = [];
workerList.forEach(function (worker) {
try {
if (worker.send(data)) {
goodWorkers.push(worker);
}
} catch (e) {
console.log(threadName + "Worker dead. Not sending new messages.");
}
});
workerList = goodWorkers;
}
function retargetMiners() {
debug(threadName + "Performing difficulty check on miners");
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
if (!miner.fixed_diff) {
miner.updateDifficulty();
}
}
}
}
function checkAliveMiners() {
debug(threadName + "Verifying if miners are still alive");
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
if (Date.now() - miner.lastContact > global.config.pool.minerTimeout * 1000) {
process.send({type: 'removeMiner', data: miner.port});
delete activeMiners[minerId];
}
}
}
}
function templateUpdate() {
global.coinFuncs.getBlockTemplate(global.config.pool.address, function (rpcResponse) {
if (rpcResponse) {
rpcResponse = rpcResponse.result;
let buffer = new Buffer(rpcResponse.blocktemplate_blob, 'hex');
let new_hash = new Buffer(32);
buffer.copy(new_hash, 0, 7, 39);
if (!activeBlockTemplate || new_hash.toString('hex') !== activeBlockTemplate.previous_hash.toString('hex')) {
debug(threadName + "New block template found at " + rpcResponse.height + " height with hash: " + new_hash.toString('hex'));
if (cluster.isMaster) {
sendToWorkers({type: 'newBlockTemplate', data: rpcResponse});
newBlockTemplate(rpcResponse);
} else {
process.send({type: 'newBlockTemplate', data: rpcResponse});
newBlockTemplate(rpcResponse);
}
}
}
});
}
function newBlockTemplate(template) {
let buffer = new Buffer(template.blocktemplate_blob, 'hex');
let previous_hash = new Buffer(32);
buffer.copy(previous_hash, 0, 7, 39);
console.log(threadName + 'New block to mine at height: ' + template.height + '. Difficulty: ' + template.difficulty);
if (activeBlockTemplate) {
pastBlockTemplates.enq(activeBlockTemplate);
}
activeBlockTemplate = new BlockTemplate(template);
for (let minerId in activeMiners) {
if (activeMiners.hasOwnProperty(minerId)) {
let miner = activeMiners[minerId];
debug(threadName + "Updating worker " + miner.payout + " With new work at height: " + template.height);
miner.messageSender('job', miner.getJob());
}
}
}
let VarDiff = (function () {
let variance = global.config.pool.varDiffVariance / 100 * global.config.pool.targetTime;
return {
tMin: global.config.pool.targetTime - variance,
tMax: global.config.pool.targetTime + variance
};
})();
function Miner(id, login, pass, ipAddress, startingDiff, messageSender, protoVersion, portType, port) {
// Username Layout - <address in BTC or XMR>.<Difficulty>
// Password Layout - <password>.<miner identifier>.<payment ID for XMR>
// Default function is to use the password so they can login. Identifiers can be unique, payment ID is last.
// If there is no miner identifier, then the miner identifier is set to the password
// If the password is x, aka, old-logins, we're not going to allow detailed review of miners.
// Miner Variables
let pass_split = pass.split(":");
this.error = "";
this.identifier = pass_split[0];
this.paymentID = null;
this.valid_miner = true;
this.port = port;
this.portType = portType;
switch (portType) {
case 'pplns':
this.poolTypeEnum = global.protos.POOLTYPE.PPLNS;
break;
case 'pps':
this.poolTypeEnum = global.protos.POOLTYPE.PPS;
break;
case 'solo':
this.poolTypeEnum = global.protos.POOLTYPE.SOLO;
break;
case 'prop':
this.poolTypeEnum = global.protos.POOLTYPE.PROP;
break;
}
let diffSplit = login.split("+");
let addressSplit = diffSplit[0].split('.');
this.address = addressSplit[0];
this.payout = addressSplit[0];
this.fixed_diff = false;
this.difficulty = startingDiff;
this.connectTime = Date.now();
if (diffSplit.length === 2) {
this.fixed_diff = true;
this.difficulty = Number(diffSplit[1]);
if (this.difficulty < global.config.pool.minDifficulty) {
this.difficulty = global.config.pool.minDifficulty;
}
if (this.difficulty > global.config.pool.maxDifficulty) {
this.difficulty = global.config.pool.maxDifficulty;
}
} else if (diffSplit.length > 2) {
this.error = "Too many options in the login field";
this.valid_miner = false;
}
if (typeof(addressSplit[1]) !== 'undefined' && (addressSplit[1].length === 16 || addressSplit[1].length === 64)) {
this.paymentID = addressSplit[1];
this.payout = this.address + "." + this.paymentID;
}
if (pass_split.length === 2) {
/*
Email address is: pass_split[1]
Need to do an initial registration call here. Might as well do it right...
*/
let payoutAddress = this.payout;
global.mysql.query("SELECT id FROM users WHERE username = ? LIMIT 1", [this.payout]).then(function (rows) {
if (rows.length > 0) {
return;
}
if (global.coinFuncs.blockedAddresses.indexOf(payoutAddress) !== -1) {
return;
}
global.mysql.query("INSERT INTO users (username, email) VALUES (?, ?)", [payoutAddress, pass_split[1]]);
});
} else if (pass_split.length > 2) {
this.error = "Too many options in the password field";
this.valid_miner = false;
}
if (global.coinFuncs.validateAddress(this.address)) {
this.bitcoin = 0;
} else if (btcValidator.validate(this.address) && global.config.general.allowBitcoin) {
this.bitcoin = 1;
} else if (btcValidator.validate(this.address)) {
this.error = "This pool does not allow payouts to bitcoin.";
this.valid_miner = false;
} else {
// Invalid Addresses
this.error = "Invalid payment address provided";
this.valid_miner = false;
}
if (bannedAddresses.indexOf(this.address) !== -1) {
// Banned Address
this.error = "Banned payment address provided";
this.valid_miner = false;
}
if (global.coinFuncs.exchangeAddresses.indexOf(this.address) !== -1 && !(this.paymentID)) {
this.error = "Exchange addresses need payment IDs";
this.valid_miner = false;
}
this.id = id;
this.ipAddress = ipAddress;
this.messageSender = messageSender;
this.heartbeat = function () {
this.lastContact = Date.now();
};
this.heartbeat();
// VarDiff System
this.shareTimeBuffer = global.support.circularBuffer(8);
this.shareTimeBuffer.enq(global.config.pool.targetTime);
this.lastShareTime = Date.now() / 1000 || 0;
this.validShares = 0;
this.invalidShares = 0;
this.hashes = 0;
this.logString = this.address + " ID: " + this.identifier + " IP: " + this.ipAddress;
if (global.config.pool.trustedMiners) {
this.trust = {
threshold: global.config.pool.trustThreshold,
probability: 256,
penalty: 0
};
}
this.validJobs = global.support.circularBuffer(10);
this.invalidShareProto = global.protos.InvalidShare.encode({
paymentAddress: this.address,
paymentID: this.paymentID,
identifier: this.identifier
});
// Support functions for how miners activate and run.
this.updateDifficultyOld = function () {
let now = Math.round(Date.now() / 1000);
let avg = this.shareTimeBuffer.average(this.lastShareTime);
let sinceLast = now - this.lastShareTime;
let decreaser = sinceLast > VarDiff.tMax;
let newDiff;
let direction;
if (avg > VarDiff.tMax && this.difficulty > global.config.pool.minDifficulty) {
newDiff = global.config.pool.targetTime / avg * this.difficulty;
direction = -1;
}
else if (avg < VarDiff.tMin && this.difficulty < global.config.pool.maxDifficulty) {
newDiff = global.config.pool.targetTime / avg * this.difficulty;
direction = 1;
}
else {
return;
}
if (Math.abs(newDiff - this.difficulty) / this.difficulty * 100 > global.config.pool.maxDiffChange) {
let change = global.config.pool.maxDiffChange / 100 * this.difficulty * direction;
newDiff = this.difficulty + change;
}
this.setNewDiff(newDiff);
this.shareTimeBuffer.clear();
if (decreaser) {
this.lastShareTime = now;
}
};
this.updateDifficulty = function(){
if (this.hashes > 0) {
this.setNewDiff(Math.floor(this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * global.config.pool.targetTime);
} else {
this.updateDifficultyOld();
}
};
this.setNewDiff = function (difficulty) {
this.newDiff = Math.round(difficulty);
debug(threadName + "Difficulty: " + this.newDiff + " For: " + this.logString + " Time Average: " + this.shareTimeBuffer.average(this.lastShareTime) + " Entries: " + this.shareTimeBuffer.size() + " Sum: " + this.shareTimeBuffer.sum());
if (this.newDiff > global.config.pool.maxDifficulty) {
this.newDiff = global.config.pool.maxDifficulty;
}
if (this.difficulty === this.newDiff) {
return;
}
if (this.newDiff < global.config.pool.minDifficulty) {
this.newDiff = global.config.pool.minDifficulty;
}
console.log(threadName + "Difficulty change to: " + this.newDiff + " For: " + this.logString);
if (this.hashes > 0){
debug(threadName + "Hashes: " + this.hashes + " in: " + Math.floor((Date.now() - this.connectTime)/1000) + " seconds gives: " +
Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) + " hashes/second or: " +
Math.floor(this.hashes/(Math.floor((Date.now() - this.connectTime)/1000))) * global.config.pool.targetTime + " difficulty versus: " + this.newDiff);
}
this.messageSender('job', this.getJob());
};
this.checkBan = function (validShare) {
if (!global.config.pool.banEnabled) {
return;
}
// Valid stats are stored by the pool.
if (validShare) {
this.validShares += 1;
} else {
this.invalidShares += 1;
}
if (this.validShares + this.invalidShares >= global.config.pool.banThreshold) {
if (this.invalidShares / this.validShares >= global.config.pool.banPercent / 100) {
delete activeMiners[this.id];
process.send({type: 'banIP', data: this.ipAddress});
}
else {
this.invalidShares = 0;
this.validShares = 0;
}
}
};
if (protoVersion === 1) {
this.getTargetHex = function () {
if (this.newDiff) {
this.difficulty = this.newDiff;
this.newDiff = null;
}
let padded = new Buffer(32);
padded.fill(0);
let diffBuff = baseDiff.div(this.difficulty).toBuffer();
diffBuff.copy(padded, 32 - diffBuff.length);
let buff = padded.slice(0, 4);
let buffArray = buff.toByteArray().reverse();
let buffReversed = new Buffer(buffArray);
this.target = buffReversed.readUInt32BE(0);
return buffReversed.toString('hex');
};
this.getJob = function () {
if (this.lastBlockHeight === activeBlockTemplate.height && !this.newDiff) {
return {
blob: '',
job_id: '',
target: ''
};
}
let blob = activeBlockTemplate.nextBlob();
let target = this.getTargetHex();
this.lastBlockHeight = activeBlockTemplate.height;
let newJob = {
id: uuidV4(),
extraNonce: activeBlockTemplate.extraNonce,
height: activeBlockTemplate.height,
difficulty: this.difficulty,
diffHex: this.diffHex,
submissions: []
};
this.validJobs.enq(newJob);
return {
blob: blob,
job_id: newJob.id,
target: target
};
};
}
}
function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate) {
miner.hashes += job.difficulty;
global.database.storeShare(job.height, global.protos.Share.encode({
shares: job.difficulty,
paymentAddress: miner.address,
paymentID: miner.paymentID,
foundBlock: blockCandidate,
trustedShare: shareType,
poolType: miner.poolTypeEnum,
poolID: global.config.pool_id,
blockDiff: activeBlockTemplate.difficulty,
bitcoin: miner.bitcoin,
blockHeight: job.height,
timestamp: Date.now(),
identifier: miner.identifier
}));
if (blockCandidate) {
global.database.storeBlock(job.height, global.protos.Block.encode({
hash: hashHex,
difficulty: blockTemplate.difficulty,
shares: 0,
timestamp: Date.now(),
poolType: miner.poolTypeEnum,
unlocked: false,
valid: true
}));
}
if (shareType) {
console.log(threadName + "Accepted trusted share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
} else {
console.log(threadName + "Accepted valid share at difficulty: " + job.difficulty + "/" + shareDiff + " from: " + miner.logString);
}
}
function processShare(miner, job, blockTemplate, nonce, resultHash) {
let template = new Buffer(blockTemplate.buffer.length);
blockTemplate.buffer.copy(template);
template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset);
let shareBuffer = global.coinFuncs.constructNewBlob(template, new Buffer(nonce, 'hex'));
let convertedBlob;
let hash;
let shareType;
if (global.config.pool.trustedMiners && miner.trust.threshold <= 0 && miner.trust.penalty <= 0 &&
crypto.randomBytes(1).readUIntBE(0, 1) > miner.trust.probability) {
hash = new Buffer(resultHash, 'hex');
shareType = true;
}
else {
convertedBlob = global.coinFuncs.convertBlob(shareBuffer);
hash = global.coinFuncs.cryptoNight(convertedBlob);
shareType = false;
}
if (hash.toString('hex') !== resultHash) {
console.error(threadName + "Bad hash from miner " + miner.logString);
miner.newDiff = miner.difficulty + 1;
miner.messageSender('job', miner.getJob());
return false;
}
let hashArray = hash.toByteArray().reverse();
let hashNum = bignum.fromBuffer(new Buffer(hashArray));
let hashDiff = baseDiff.div(hashNum);
if (hashDiff.ge(blockTemplate.difficulty)) {
// Submit block to the RPC Daemon.
global.support.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function (rpcResult) {
if (rpcResult.error) {
// Did not manage to submit a block. Log and continue on.
console.error(threadName + "Error submitting block at height " + job.height + " from " + miner.logString + ", share type: " + shareType + " error: " + JSON.stringify(rpcResult.error));
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
// Error on submit, so we'll submit a sanity check for good measure.
templateUpdate();
} else if (rpcResult) {
//Success! Submitted a block without an issue.
let blockFastHash = global.coinFuncs.getBlockID(shareBuffer).toString('hex');
console.log(threadName + "Block " + blockFastHash.substr(0, 6) + " found at height " + job.height + " by " + miner.logString +
", share type: " + shareType + " - submit result: " + JSON.stringify(rpcResult.result));
recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate);
templateUpdate();
} else {
// RPC bombed out massively.
console.error(threadName + "RPC Error. Please check logs for details");
}
});
}
else if (hashDiff.lt(job.difficulty)) {
console.warn(threadName + "Rejected low diff share of " + hashDiff.toString() + " from: " + miner.address + " ID: " +
miner.identifier + " IP: " + miner.ipAddress);
return false;
}
else {
recordShareData(miner, job, hashDiff.toString(), false, null, shareType);
}
return true;
}
function handleMinerData(method, params, ip, portData, sendReply, pushMessage) {
let miner = activeMiners[params.id];
// Check for ban here, so preconnected attackers can't continue to screw you
if (bannedIPs.indexOf(ip) !== -1) {
// Handle IP ban off clip.
sendReply("IP Address currently banned");
return;
}
switch (method) {
case 'login':
if (!params.login || !params.pass) {
sendReply("No login/password specified");
return;
}
let difficulty = portData.difficulty;
let minerId = uuidV4();
miner = new Miner(minerId, params.login, params.pass, ip, difficulty, pushMessage, 1, portData.portType, portData.port);
if (!miner.valid_miner) {
console.log("Invalid miner, disconnecting due to: " + miner.error);
sendReply(miner.error);
return;
}
process.send({type: 'newMiner', data: miner.port});
activeMiners[minerId] = miner;
sendReply(null, {
id: minerId,
job: miner.getJob(),
status: 'OK'
});
break;
case 'getjob':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
sendReply(null, miner.getJob());
break;
case 'submit':
if (!miner) {
sendReply('Unauthenticated');
return;
}
miner.heartbeat();
let job = miner.validJobs.toarray().filter(function (job) {
return job.id === params.job_id;
})[0];
if (!job) {
sendReply('Invalid job id');
return;
}
params.nonce = params.nonce.substr(0, 8).toLowerCase();
if (!nonceCheck.test(params.nonce)) {
console.warn(threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString);
miner.checkBan(false);
sendReply('Duplicate share');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
if (job.submissions.indexOf(params.nonce) !== -1) {
console.warn(threadName + 'Duplicate share: ' + JSON.stringify(params) + ' from ' + miner.logString);
miner.checkBan(false);
sendReply('Duplicate share');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
job.submissions.push(params.nonce);
let blockTemplate = activeBlockTemplate.height === job.height ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) {
return t.height === job.height;
})[0];
if (!blockTemplate) {
console.warn(threadName + 'Block expired, Height: ' + job.height + ' from ' + miner.logString);
miner.newDiff = miner.difficulty + 1;
miner.messageSender('job', miner.getJob());
sendReply('Block expired');
global.database.storeInvalidShare(miner.invalidShareProto);
return;
}
let shareAccepted = processShare(miner, job, blockTemplate, params.nonce, params.result);
miner.checkBan(shareAccepted);
if (global.config.pool.trustedMiners) {
if (shareAccepted) {
miner.trust.probability -= 1;
if (miner.trust.probability < (global.config.pool.trustMin)) {
miner.trust.probability = global.config.pool.trustMin;
}
miner.trust.penalty--;
miner.trust.threshold--;
}
else {
console.log(threadName + "Share trust broken by " + miner.logString);
global.database.storeInvalidShare(miner.invalidShareProto);
miner.trust.probability = 256;
miner.trust.penalty = global.config.pool.trustPenalty;
miner.trust.threshold = global.config.pool.trustThreshold;
}
}
if (!shareAccepted) {
sendReply('Low difficulty share');
return;
}
let now = Date.now() / 1000 || 0;
miner.shareTimeBuffer.enq(now - miner.lastShareTime);
miner.lastShareTime = now;
sendReply(null, {status: 'OK'});
break;
}
}
if (cluster.isMaster) {
let numWorkers = require('os').cpus().length;
global.config.ports.forEach(function (portData) {
minerCount[portData.port] = 0;
});
registerPool();
setInterval(function () {
global.mysql.query("UPDATE pools SET last_checkin = ?, active = ? WHERE id = ?", [global.support.formatDate(Date.now()), true, global.config.pool_id]);
global.mysql.query("UPDATE pools SET blockIDTime = now(), blockID = ? where id = ?", [activeBlockTemplate.height, global.config.pool_id]);
global.config.ports.forEach(function (portData) {
global.mysql.query("UPDATE ports SET lastSeen = now(), miners = ? WHERE pool_id = ? AND network_port = ?", [minerCount[portData.port], global.config.pool_id, portData.port]);
});
}, 10000);
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for (let i = 0; i < numWorkers; i++) {
let worker = cluster.fork();
worker.on('message', messageHandler);
workerList.push(worker);
}
cluster.on('online', function (worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function (worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
worker = cluster.fork();
worker.on('message', messageHandler);
workerList.push(worker);
});
setInterval(templateUpdate, 300);
global.support.sendEmail(global.config.general.adminEmail, "Pool server "+global.config.hostname+" online", "The pool server: "+global.config.hostname+" with IP: "+global.config.bind_ip+" is online");
} else {
setInterval(checkAliveMiners, 30000);
setInterval(retargetMiners, global.config.pool.retargetTime * 1000);
templateUpdate();
setInterval(function () {
bannedIPs = [];
}, 60000);
async.each(global.config.ports, function (portData) {
let handleMessage = function (socket, jsonData, pushMessage) {
if (!jsonData.id) {
console.warn('Miner RPC request missing RPC id');
return;
}
else if (!jsonData.method) {
console.warn('Miner RPC request missing RPC method');
return;
}
else if (!jsonData.params) {
console.warn('Miner RPC request missing RPC params');
return;
}
let sendReply = function (error, result) {
if (!socket.writable) {
return;
}
let sendData = JSON.stringify({
id: jsonData.id,
jsonrpc: "2.0",
error: error ? {code: -1, message: error} : null,
result: result
}) + "\n";
socket.write(sendData);
};
handleMinerData(jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage);
};
function socketConn(socket) {
socket.setKeepAlive(true);
socket.setEncoding('utf8');
let dataBuffer = '';
let pushMessage = function (method, params) {
if (!socket.writable) {
return;
}
let sendData = JSON.stringify({
jsonrpc: "2.0",
method: method,
params: params
}) + "\n";
socket.write(sendData);
};
socket.on('data', function (d) {
dataBuffer += d;
if (Buffer.byteLength(dataBuffer, 'utf8') > 10240) { //10KB
dataBuffer = null;
console.warn(threadName + 'Excessive packet size from: ' + socket.remoteAddress);
socket.destroy();
return;
}
if (dataBuffer.indexOf('\n') !== -1) {
let messages = dataBuffer.split('\n');
let incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop();
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.trim() === '') {
continue;
}
let jsonData;
try {
jsonData = JSON.parse(message);
}
catch (e) {
if (message.indexOf('GET /') === 0) {
if (message.indexOf('HTTP/1.1') !== -1) {
socket.end('HTTP/1.1' + httpResponse);
break;
}
else if (message.indexOf('HTTP/1.0') !== -1) {
socket.end('HTTP/1.0' + httpResponse);
break;
}
}
console.warn(threadName + "Malformed message from " + socket.remoteAddress + " Message: " + message);
socket.destroy();
break;
}
handleMessage(socket, jsonData, pushMessage);
}
dataBuffer = incomplete;
}
}).on('error', function (err) {
if (err.code !== 'ECONNRESET') {
console.warn(threadName + "Socket Error from " + socket.remoteAddress + " Error: " + err);
}
}).on('close', function () {
pushMessage = function () {
};
});
}
if ('ssl' in portData && portData['ssl'] === true) {
tls.createServer({
key: fs.readFileSync('cert.key'),
cert: fs.readFileSync('cert.pem')
}, socketConn).listen(portData.port, global.config.bind_ip, function (error) {
if (error) {
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
return;
}
console.log(threadName + "Started server on port: " + portData.port);
});
} else {
net.createServer(socketConn).listen(portData.port, global.config.bind_ip, function (error) {
if (error) {
console.error(threadName + "Unable to start server on: " + portData.port + " Message: " + error);
return;
}
console.log(threadName + "Started server on port: " + portData.port);
});
}
});
}

@ -0,0 +1,90 @@
"use strict";
const express = require('express'); // call express
const app = express(); // define our app using express
const cluster = require('cluster');
const debug = require("debug")("remoteShare");
let concat = require('concat-stream');
let threadName = "";
let workerList = [];
if (cluster.isMaster) {
threadName = "(Master) ";
} else {
threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") ";
}
// Websocket Stuffs.
app.use(function(req, res, next){
req.pipe(concat(function(data){
req.body = data;
next();
}));
});
app.post('/leafApi', function (req, res) {
try {
let msgData = global.protos.WSData.decode(req.body);
if (msgData.key !== global.config.api.authKey) {
return res.status(403).end();
}
switch (msgData.msgType) {
case global.protos.MESSAGETYPE.SHARE:
global.database.storeShare(msgData.exInt, msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
case global.protos.MESSAGETYPE.BLOCK:
global.database.storeBlock(msgData.exInt, msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
case global.protos.MESSAGETYPE.INVALIDSHARE:
global.database.storeInvalidShare(msgData.msg, function(data){
if (!data){
return res.status(400).end();
} else {
return res.json({'success': true});
}
});
break;
default:
return res.status(400).end();
}
} catch (e) {
console.log("Invalid WS frame");
return res.status(400).end();
}
});
if (cluster.isMaster) {
let numWorkers = require('os').cpus().length;
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for (let i = 0; i < numWorkers; i++) {
let worker = cluster.fork();
workerList.push(worker);
}
cluster.on('online', function (worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function (worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
worker = cluster.fork();
workerList.push(worker);
});
} else {
app.listen(8000, function () {
console.log('Process ' + process.pid + ' is listening to all incoming requests');
});
}

@ -0,0 +1,63 @@
"use strict";
const request = require('request');
const async = require('async');
function Database() {
this.sendQueue = async.queue(function (task, callback) {
async.doUntil(
function (intCallback) {
request.post({url: global.config.general.shareHost, body: task.body}, function (error, response, body) {
if (!error) {
return intCallback(null, response.statusCode);
}
return intCallback(null, 0);
});
},
function (data) {
return data === 200;
},
function () {
callback();
});
}, 32);
this.storeShare = function (blockId, shareData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.SHARE,
key: global.config.api.authKey,
msg: shareData,
exInt: blockId
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.storeBlock = function (blockId, blockData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.BLOCK,
key: global.config.api.authKey,
msg: blockData,
exInt: blockId
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.storeInvalidShare = function (minerData) {
let wsData = global.protos.WSData.encode({
msgType: global.protos.MESSAGETYPE.INVALIDSHARE,
key: global.config.api.authKey,
msg: minerData,
exInt: 1
});
this.sendQueue.push({body: wsData}, function () {
});
};
this.initEnv = function(){
this.data = null;
};
}
module.exports = Database;

@ -0,0 +1,185 @@
"use strict";
const CircularBuffer = require('circular-buffer');
const request = require('request');
const requestJson = require('request-json');
const moment = require('moment');
const debug = require('debug')('support');
const fs = require('fs');
function circularBuffer(size) {
let buffer = CircularBuffer(size);
buffer.sum = function () {
if (this.size() === 0) {
return 1;
}
return this.toarray().reduce(function (a, b) {
return a + b;
});
};
buffer.average = function (lastShareTime) {
if (this.size() === 0) {
return global.config.pool.targetTime * 1.5;
}
let extra_entry = (Date.now() / 1000) - lastShareTime;
return (this.sum() + Math.round(extra_entry)) / (this.size() + 1);
};
buffer.clear = function () {
let i = this.size();
while (i > 0) {
this.deq();
i = this.size();
}
};
return buffer;
}
function sendEmail(toAddress, subject, body){
request.post(global.config.general.mailgunURL + "/messages", {
auth: {
user: 'api',
pass: global.config.general.mailgunKey
},
form: {
from: global.config.general.emailFrom,
to: toAddress,
subject: subject,
text: body
}
}, function(err, response, body){
if (!err && response.statusCode === 200) {
console.log("Email sent successfully! Response: " + body);
} else {
console.error("Did not send e-mail successfully! Response: " + body + " Response: "+JSON.stringify(response));
}
});
}
function jsonRequest(host, port, data, callback, path) {
path = path || 'json_rpc';
let uri;
if (global.config.rpc.https) {
uri = "https://" + host + ":" + port + "/";
} else {
uri = "http://" + host + ":" + port + "/";
}
debug("JSON URI: " + uri + path + " Args: " + JSON.stringify(data));
let client = requestJson.createClient(uri);
client.headers["Content-Type"] = "application/json";
client.headers["Content-Length"] = data.length;
client.headers["Accept"] = "application/json";
if (global.config.payout.rpcPasswordEnabled && host === global.config.wallet.address && port === global.config.wallet.port){
fs.readFile(global.config.payout.rpcPasswordPath, 'utf8', function(err, data){
if (err){
console.error("RPC password enabled, unable to read the file due to: " + JSON.stringify(err));
return;
}
let passData = data.split(":");
client.setBasicAuth(passData[0], passData[1]);
request.post(uri, {
auth:{
user: passData[0],
pass: passData[1],
sendImmediately: false
},
data: JSON.stringify(data)
}, function (err, res, body) {
if (err) {
return callback(err);
}
return callback(body);
});
});
} else {
client.post(path, data, function (err, res, body) {
if (err) {
return callback(err);
}
return callback(body);
});
}
}
function rpc(host, port, method, params, callback) {
let data = {
id: "0",
jsonrpc: "2.0",
method: method,
params: params
};
return jsonRequest(host, port, data, callback);
}
function formatDate(date) {
// Date formatting for MySQL date time fields.
return moment(date).format('YYYY-MM-DD HH:mm:ss');
}
function formatDateFromSQL(date) {
// Date formatting for MySQL date time fields.
let ts = new Date(date);
return Math.floor(ts.getTime() / 1000);
}
function coinToDecimal(amount) {
return amount / global.config.coin.sigDigits;
}
function decimalToCoin(amount) {
return amount * global.config.coin.sigDigits;
}
function bitcoinDecimalToCoin(amount) {
return amount * 100000000;
}
function bitcoinCoinToDecimal(amount) {
return amount / 100000000;
}
function blockCompare(a, b) {
if (a.height < b.height) {
return 1;
}
if (a.height > b.height) {
return -1;
}
return 0;
}
function tsCompare(a, b) {
if (a.ts < b.ts) {
return 1;
}
if (a.ts > b.ts) {
return -1;
}
return 0;
}
module.exports = function () {
return {
rpcDaemon: function (method, params, callback) {
rpc(global.config.daemon.address, global.config.daemon.port, method, params, callback);
},
rpcWallet: function (method, params, callback) {
rpc(global.config.wallet.address, global.config.wallet.port, method, params, callback);
},
jsonRequest: jsonRequest,
circularBuffer: circularBuffer,
formatDate: formatDate,
coinToDecimal: coinToDecimal,
decimalToCoin: decimalToCoin,
bitcoinDecimalToCoin: bitcoinDecimalToCoin,
bitcoinCoinToDecimal: bitcoinCoinToDecimal,
formatDateFromSQL: formatDateFromSQL,
blockCompare: blockCompare,
sendEmail: sendEmail,
tsCompare: tsCompare
};
};

@ -0,0 +1,503 @@
"use strict";
const debug = require("debug")("worker");
const async = require("async");
let threadName = "Worker Server ";
let cycleCount = 0;
function updateShareStats() {
// This is an omni-worker to deal with all things share-stats related
// Time based averages are worked out on ring buffers.
// Buffer lengths? You guessed it, configured in SQL.
// Stats timeouts are 30 seconds, so everything for buffers should be there.
let currentTime = Math.floor(Date.now() / 1000);
async.waterfall([
function (callback) {
global.coinFuncs.getLastBlockHeader(function (body) {
callback(null, body.height + 1);
});
},
function (height, callback) {
let locTime = Date.now() - 600000;
let identifierTime = Date.now() - 1800000;
let localStats = {pplns: 0, pps: 0, solo: 0, prop: 0, global: 0, miners: {}};
let localMinerCount = {pplns: 0, pps: 0, solo: 0, prop: 0, global: 0};
let localTimes = {
pplns: locTime, pps: locTime, solo: locTime, prop: locTime,
global: locTime, miners: {}
};
let minerList = [];
let identifiers = {};
let loopBreakout = 0;
async.doUntil(function (callback_until) {
let oldestTime = Date.now();
let loopCount = 0;
let txn = global.database.env.beginTxn({readOnly: true});
let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB);
for (let found = (cursor.goToRange(height) === height); found; found = cursor.goToNextDup()) {
cursor.getCurrentBinary(function (key, share) { // jshint ignore:line
try {
share = global.protos.Share.decode(share);
} catch (e) {
console.error(share);
return;
}
if (share.timestamp < oldestTime) {
oldestTime = share.timestamp;
}
if (share.timestamp <= identifierTime) {
return;
}
let minerID = share.paymentAddress;
if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) {
minerID = minerID + '.' + share.paymentID;
}
if (minerID in identifiers && identifiers[minerID].indexOf(share.identifier) >= 0) {
loopCount += 1;
} else if (minerID in identifiers) {
identifiers[minerID].push(share.identifier);
} else {
identifiers[minerID] = [share.identifier];
}
if (share.timestamp <= locTime) {
return;
}
let minerIDWithIdentifier = minerID + "_" + share.identifier;
localStats.global += share.shares;
if (localTimes.global <= share.timestamp) {
localTimes.global = share.timestamp;
}
let minerType;
switch (share.poolType) {
case global.protos.POOLTYPE.PPLNS:
minerType = 'pplns';
localStats.pplns += share.shares;
if (localTimes.pplns <= share.timestamp) {
localTimes.pplns = share.timestamp;
}
break;
case global.protos.POOLTYPE.PPS:
localStats.pps += share.shares;
minerType = 'pps';
if (localTimes.pps <= share.timestamp) {
localTimes.pps = share.timestamp;
}
break;
case global.protos.POOLTYPE.SOLO:
localStats.solo += share.shares;
minerType = 'solo';
if (localTimes.solo <= share.timestamp) {
localTimes.solo = share.timestamp;
}
break;
}
if (Object.keys(localStats.miners).indexOf(minerID) >= 0) {
localStats.miners[minerID] += share.shares;
if (localTimes.miners[minerID] < share.timestamp) {
localTimes.miners[minerID] = share.timestamp;
}
} else {
localMinerCount[minerType] += 1;
localMinerCount.global += 1;
localStats.miners[minerID] = share.shares;
localTimes.miners[minerID] = share.timestamp;
minerList.push(minerID);
}
if (Object.keys(localStats.miners).indexOf(minerIDWithIdentifier) >= 0) {
localStats.miners[minerIDWithIdentifier] += share.shares;
if (localTimes.miners[minerIDWithIdentifier] < share.timestamp) {
localTimes.miners[minerIDWithIdentifier] = share.timestamp;
}
} else {
localStats.miners[minerIDWithIdentifier] = share.shares;
localTimes.miners[minerIDWithIdentifier] = share.timestamp;
minerList.push(minerIDWithIdentifier);
}
});
}
cursor.close();
txn.abort();
return callback_until(null, oldestTime);
}, function (oldestTime) {
height -= 1;
loopBreakout += 1;
if (loopBreakout > 60) {
return true;
}
return oldestTime <= identifierTime;
}, function (err) {
// todo: Need to finish parsing the cached data into caches for caching purproses.
let globalMinerList = global.database.getCache('minerList');
if (globalMinerList === false) {
globalMinerList = [];
}
minerList.forEach(function (miner) {
if (globalMinerList.indexOf(miner) === -1) {
globalMinerList.push(miner);
}
let cachedData = global.database.getCache(miner);
if (cachedData !== false) {
cachedData.hash = Math.floor(localStats.miners[miner] / 600);
cachedData.lastHash = localTimes.miners[miner];
if (!cachedData.hasOwnProperty("hashHistory")) {
cachedData.hashHistory = [];
}
if (cycleCount === 0){
cachedData.hashHistory.unshift({ts: Date.now(), hs: cachedData.hash});
if (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
while (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
cachedData.hashHistory.pop();
}
}
}
} else {
cachedData = {
hash: Math.floor(localStats.miners[miner] / 600),
totalHashes: 0,
lastHash: localTimes.miners[miner],
hashHistory: [{ts: currentTime, hs: cachedData.hash}],
goodShares: 0,
badShares: 0
};
}
global.database.setCache(miner, cachedData);
});
// pplns: 0, pps: 0, solo: 0, prop: 0, global: 0
['pplns', 'pps', 'solo', 'prop', 'global'].forEach(function (key) {
let cachedData = global.database.getCache(key + "_stats");
if (cachedData !== false) {
cachedData.hash = Math.floor(localStats[key] / 600);
cachedData.lastHash = localTimes[key];
cachedData.minerCount = localMinerCount[key];
if (!cachedData.hasOwnProperty("hashHistory")) {
cachedData.hashHistory = [];
cachedData.minerHistory = [];
}
if (cycleCount === 0) {
cachedData.hashHistory.unshift({ts: Date.now(), hs: cachedData.hash});
if (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
while (cachedData.hashHistory.length > global.config.general.statsBufferLength) {
cachedData.hashHistory.pop();
}
}
cachedData.minerHistory.unshift({ts: Date.now(), cn: cachedData.minerCount});
if (cachedData.minerHistory.length > global.config.general.statsBufferLength) {
while (cachedData.minerHistory.length > global.config.general.statsBufferLength) {
cachedData.minerHistory.pop();
}
}
}
} else {
cachedData = {
hash: Math.floor(localStats[key] / 600),
totalHashes: 0,
lastHash: localTimes[key],
minerCount: localMinerCount[key],
hashHistory: [{ts: currentTime, hs: cachedData.hash}],
minerHistory: [{ts: currentTime, cn: cachedData.hash}]
};
}
global.database.setCache(key + "_stats", cachedData);
});
globalMinerList.forEach(function (miner) {
if (minerList.indexOf(miner) === -1) {
let minerStats = global.database.getCache(miner);
if (minerStats.hash !== 0) {
console.log("Removing: " + miner + " as an active miner from the cache.");
if (miner.indexOf('_') > -1) {
// This is a worker case.
let address_parts = miner.split('_');
let address = address_parts[0];
let worker = address_parts[1];
global.mysql.query("SELECT email FROM users WHERE username = ? limit 1", [address]).then(function (rows) {
if (rows.length === 0) {
return;
}
// toAddress, subject, body
global.support.sendEmail(rows[0].email, "Worker " + worker + " not hashing",
"Hello,\r\n\r\nYour worker: " + worker + " has stopped submitting hashes at: " + global.support.formatDate(Date.now()) +
" UTC\r\n\r\nThank you,\r\nXMRPool.net Administration Team");
});
}
minerStats.hash = 0;
global.database.setCache(miner, minerStats);
}
}
});
Object.keys(identifiers).forEach(function (key) {
global.database.setCache(key + '_identifiers', identifiers[key]);
});
global.database.setCache('minerList', globalMinerList);
callback(null);
});
}
], function (err, result) {
cycleCount += 1;
if (cycleCount === 6){
cycleCount = 0;
}
});
setTimeout(updateShareStats, 10000);
}
function updatePoolStats(poolType) {
async.series([
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").hash || 0);
}
return callback(null, global.database.getCache("global_stats").hash || 0);
},
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").minerCount || 0);
}
return callback(null, global.database.getCache("global_stats").minerCount || 0);
},
function (callback) {
debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats");
if (typeof(poolType) !== 'undefined') {
return callback(null, global.database.getCache(poolType + "_stats").totalHashes || 0);
}
return callback(null, global.database.getCache("global_stats").totalHashes || 0);
},
function (callback) {
debug(threadName + "Checking MySQL for last block find time for pool stats");
let cacheData = global.database.getBlockList(poolType);
if (cacheData.length === 0) {
return callback(null, 0);
}
return callback(null, Math.floor(cacheData[0].ts / 1000));
},
function (callback) {
debug(threadName + "Checking MySQL for last block find time for pool stats");
let cacheData = global.database.getBlockList(poolType);
if (cacheData.length === 0) {
return callback(null, 0);
}
return callback(null, cacheData[0].height);
},
function (callback) {
debug(threadName + "Checking MySQL for block count for pool stats");
return callback(null, global.database.getBlockList(poolType).length);
},
function (callback) {
debug(threadName + "Checking MySQL for total miners paid");
if (typeof(poolType) !== 'undefined') {
global.mysql.query("SELECT id FROM payments WHERE pool_type = ? group by payment_address, payment_id", [poolType]).then(function (rows) {
return callback(null, rows.length);
});
} else {
global.mysql.query("SELECT id FROM payments group by payment_address, payment_id").then(function (rows) {
return callback(null, rows.length);
});
}
},
function (callback) {
debug(threadName + "Checking MySQL for total transactions count");
if (typeof(poolType) !== 'undefined') {
global.mysql.query("SELECT distinct(transaction_id) from payments WHERE pool_type = ?", [poolType]).then(function (rows) {
return callback(null, rows.length);
});
} else {
global.mysql.query("SELECT count(id) as txn_count FROM transactions").then(function (rows) {
if (typeof(rows[0]) !== 'undefined') {
return callback(null, rows[0].txn_count);
} else {
return callback(null, 0);
}
});
}
},
], function (err, result) {
if (typeof(poolType) === 'undefined') {
poolType = 'global';
}
global.database.setCache('pool_stats_' + poolType, {
hashRate: result[0],
miners: result[1],
totalHashes: result[2],
lastBlockFoundTime: result[3] || 0,
lastBlockFound: result[4] || 0,
totalBlocksFound: result[5] || 0,
totalMinersPaid: result[6] || 0,
totalPayments: result[7] || 0
});
});
}
function updatePoolPorts(poolServers) {
debug(threadName + "Updating pool ports");
let local_cache = {global: []};
let portCount = 0;
global.mysql.query("select * from ports where hidden = 0 and lastSeen >= NOW() - INTERVAL 10 MINUTE").then(function (rows) {
rows.forEach(function (row) {
portCount += 1;
if (!local_cache.hasOwnProperty(row.port_type)) {
local_cache[row.port_type] = [];
}
local_cache[row.port_type].push({
host: poolServers[row.pool_id],
port: row.network_port,
difficulty: row.starting_diff,
description: row.description,
miners: row.miners
});
if (portCount === rows.length) {
let local_counts = {};
let port_diff = {};
let port_miners = {};
let pool_type_count = 0;
let localPortInfo = {};
for (let pool_type in local_cache) { // jshint ignore:line
pool_type_count += 1;
local_cache[pool_type].forEach(function (portData) { // jshint ignore:line
if (!local_counts.hasOwnProperty(portData.port)) {
local_counts[portData.port] = 0;
}
if (!port_diff.hasOwnProperty(portData.port)) {
port_diff[portData.port] = portData.difficulty;
}
if (!port_miners.hasOwnProperty(portData.port)) {
port_miners[portData.port] = 0;
}
if (port_diff[portData.port] === portData.difficulty) {
local_counts[portData.port] += 1;
port_miners[portData.port] += portData.miners;
}
localPortInfo[portData.port] = portData.description;
if (local_counts[portData.port] === Object.keys(poolServers).length) {
local_cache.global.push({
host: {
blockID: local_cache[pool_type][0].host.blockID,
blockIDTime: local_cache[pool_type][0].host.blockIDTime,
hostname: global.config.pool.geoDNS,
},
port: portData.port,
pool_type: pool_type,
difficulty: portData.difficulty,
miners: port_miners[portData.port],
description: localPortInfo[portData.port]
});
}
});
if (pool_type_count === Object.keys(local_cache).length) {
debug(threadName + "Sending the following to the workers: " + JSON.stringify(local_cache));
global.database.setCache('poolPorts', local_cache);
}
}
}
});
});
}
function updatePoolInformation() {
let local_cache = {};
debug(threadName + "Updating pool information");
global.mysql.query("select * from pools where last_checkin >= NOW() - INTERVAL 10 MINUTE").then(function (rows) {
rows.forEach(function (row) {
local_cache[row.id] = {
ip: row.ip,
blockID: row.blockID,
blockIDTime: global.support.formatDateFromSQL(row.blockIDTime),
hostname: row.hostname
};
if (Object.keys(local_cache).length === rows.length) {
global.database.setCache('poolServers', local_cache);
updatePoolPorts(local_cache);
}
});
});
}
function updateBlockHeader() {
global.support.rpcDaemon('getlastblockheader', [], function (body) {
if (body.result) {
global.database.setCache('networkBlockInfo', {
difficulty: body.result.block_header.difficulty,
hash: body.result.block_header.hash,
height: body.result.block_header.height,
value: body.result.block_header.reward,
ts: body.result.block_header.timestamp
});
} else {
console.error("GetLastBlockHeader Error during block header update");
}
});
}
function updateWalletStats() {
async.waterfall([
function (callback) {
global.support.rpcWallet('getbalance', [], function (body) {
if (body.result) {
return callback(null, {
balance: body.result.balance,
unlocked: body.result.unlocked_balance,
ts: Date.now()
});
} else {
return callback(true, "Unable to process balance");
}
});
},
function (state, callback) {
global.support.rpcWallet('getheight', [], function (body) {
if (body.result) {
state.height = body.result.height;
return callback(null, state);
} else {
return callback(true, "Unable to get current wallet height");
}
});
}
], function (err, results) {
if (err) {
return console.error(err);
}
global.database.setCache('walletStateInfo', results);
let history = global.database.getCache('walletHistory');
if (history === false) {
history = [];
}
history.unshift(results);
history = history.sort(global.support.tsCompare);
if (history.length > global.config.general.statsBufferLength) {
while (history.length > global.config.general.statsBufferLength) {
history.pop();
}
}
global.database.setCache('walletHistory', history);
});
}
function monitorNodes() {
global.mysql.query("SELECT blockID FROM pools WHERE last_checkin > date_sub(now(), interval 30 minute)").then(function (rows) {
global.coinFuncs.getLastBlockHeader(function (block) {
rows.forEach(function (row) {
if (row.blockID < block.height - 3) {
global.support.sendEmail(global.config.general.adminEmail, "Pool server behind in blocks", "The pool server: "+row.hostname+" with IP: "+row.ip+" is "+block.height - row.blockID+ " blocks behind");
}
}
);
});
});
}
updateShareStats();
updateBlockHeader();
updatePoolStats();
updatePoolInformation();
updateWalletStats();
monitorNodes();
setInterval(updateBlockHeader, 10000);
setInterval(updatePoolStats, 5000);
setInterval(updatePoolStats, 5000, 'pplns');
setInterval(updatePoolStats, 5000, 'pps');
setInterval(updatePoolStats, 5000, 'solo');
setInterval(updatePoolInformation, 5000);
setInterval(updateWalletStats, 60000);
setInterval(monitorNodes, 300000);

@ -0,0 +1,41 @@
{
"name": "nodejs-pool",
"version": "0.0.1",
"description": "Fairly simple universal cryptonote pool",
"main": "init.js",
"repository": {
"type": "git",
"url": "https://github.com/Snipa22/node-crypto-pool.git"
},
"author": "Alexander Blair",
"license": "MIT",
"dependencies": {
"async": "2.1.4",
"bignum": "^0.12.5",
"bluebird": "3.4.7",
"body-parser": "^1.16.0",
"bufferutil": "^1.3.0",
"circular-buffer": "1.0.2",
"cluster": "0.7.7",
"concat-stream": "^1.6.0",
"crypto": "0.0.3",
"cryptonote-util": "git://github.com/Snipa22/node-cryptonote-util.git#xmr-Nan-2.0",
"debug": "2.5.1",
"express": "4.14.0",
"jsonwebtoken": "^7.2.1",
"minimist": "1.2.0",
"moment": "2.17.1",
"multi-hashing": "git+https://github.com/Snipa22/node-multi-hashing-aesni.git",
"mysql": "2.12.0",
"node-lmdb": "^0.4.3",
"promise-mysql": "3.0.0",
"protocol-buffers": "^3.2.1",
"range": "0.0.3",
"redis": "^2.6.5",
"request": "^2.79.0",
"request-json": "0.6.1",
"shapeshift.io": "1.3.0",
"uuid": "3.0.1",
"wallet-address-validator": "0.1.0"
}
}

@ -0,0 +1,6 @@
UPDATE pool.config SET item_value = '' WHERE module = 'pool' and item = 'address';
UPDATE pool.config SET item_value = '' WHERE module = 'pool' and item = 'feeAddress';
UPDATE pool.config SET item_value = '' WHERE module = 'general' and item = 'mailgunKey';
UPDATE pool.config SET item_value = '' WHERE module = 'general' and item = 'mailgunURL';
UPDATE pool.config SET item_value = '' WHERE module = 'general' and item = 'emailFrom';
UPDATE pool.config SET item_value = '' WHERE module = 'general' and item = 'shareHost';
Loading…
Cancel
Save