diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29af181 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..085dbad --- /dev/null +++ b/LICENSE @@ -0,0 +1,36 @@ +MIT License + +Copyright (c) 2017 Snipa22 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This work is a derivitive work from Zone117x's node-cryptonote-pool, permission +has been granted from Zone117x via IRC conversation to re-license this code as +MIT for all portions of the work derived from his original source as of 2/11/2017 +Original work: https://github.com/zone117x/node-cryptonote-pool + +Log snippet: +Feb 11 15:59:02 Hallo! Wanted to reach out as we finally released the pool software w/ the FFS funded at this point. https://github.com/Snipa22/nodejs-pool . The following code: https://pb.junaos.xyz/view/cd3d7e27 is what was snagged, and mostly rebuilt along the way for good measure. +Feb 11 15:59:02 I'm not sure how deeply the GPL requires me to go to claim it as "new code" given that the logic behind it all is about the same. + +Feb 11 16:09:39 .fr is now 1/5th of the overall mining power, DP is another 1/5th. +Feb 11 16:09:48 We're getting scary-close to the 50% attacks. +Feb 11 16:10:08 You have my permission to MIT +Feb 11 16:10:31 Thank you. I'll get our licenses updated and such today. +Feb 11 16:12:08 What payment systems did you get implemented? diff --git a/README.md b/README.md new file mode 100644 index 0000000..f17d0b8 --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +Important Info +================== +Many thanks to MoneroOcean for his job. + +Basic system is forked from last single coin repository: +* https://github.com/MoneroOcean + +Coin configs, base.sql and deployments are modified by me to use from deployment scripts + +Pool Design/Theory +================== +The nodejs-pool is built around a small series of core daemons that share access to a single LMDB table for tracking of shares, with MySQL being used to centralize configurations and ensure simple access from local/remote nodes. The core daemons follow: +```text +api - Main API for the frontend to use and pull data from. Expects to be hosted at / +remoteShare - Main API for consuming shares from remote/local pools. Expects to be hosted at /leafApi +pool - Where the miners connect to. +longRunner - Database share cleanup. +payments - Handles all payments to workers. +blockManager - Unlocks blocks and distributes payments into MySQL +worker - Does regular processing of statistics and sends status e-mails for non-active miners. +``` +API listens on port 8001, remoteShare listens on 8000 + +Xmrpool.net (The refrence implementation) uses the following setup: +* https://xmrpool.net is hosted on it's own server, as the main website is a static frontend +* https://api.xmrpool.net hosts api, remoteShare, longRunner, payments, blockManager, worker, as these must all be hosted with access to the same LMDB database. + +Sample Caddyfile for API: +```text +https://api.xmrpool.net { + proxy /leafApi 127.0.0.1:8000 + proxy / 127.0.0.1:8001 + cors + gzip +} +``` + +It is critically important that your webserver does not truncate the `/leafApi` portion of the URL for the remoteShare daemon, or it will not function! Local pool servers DO use the remoteShare daemon, as this provides a buffer in case of an error with LMDB or another bug within the system, allowing shares and blocks to queue for submission as soon as the leafApi/remoteShare daemons are back up and responding with 200's. + +Setup Instructions +================== + +Server Requirements +------------------- +* 4 Gb Ram +* 2 CPU Cores (with AES_NI) +* 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: ` ALL=(ALL) NOPASSWD:ALL`. Our sample builds use: `pooldaemon ALL=(ALL) NOPASSWD:ALL` +2. Run the [deploy script](https://raw.githubusercontent.com/arqtras/nodejs-pool/master/deployment/deploy_electroneum.bash) 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. +Install Script: curl -L https://raw.githubusercontent.com/arqtras/nodejs-pool/master/deployment/deploy_electroneum.bash | bash +3. Once it's complete, change as `config.json` 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. You will also want to run: source ~/.bashrc This will activate NVM and get things working for the following pm2 steps. +4. You'll need to change the API end point for the frontend code in the `poolui/build/globals.js` and `poolui/build/global.default.js` -- This will usually be `http(s):///api` unless you tweak caddy! +5. Check `config.json` and change as appropriate. The default database directory `/home//pool_db/` is already been created during startup. If you change the `db_storage_path` just make sure your user has write permissions for new path. Run: `pm2 restart api` to reload the API for usage. You'll also want to set `bind_ip` to the external IP of the pool server, and `hostname` to the resolvable hostname for the pool server. `pool_id` is mostly used for multi-server installations to provide unique identifiers in the backend. +6. Hop into the web interface (Should be at `http:///#/admin`), then login with `Administrator/Password123`, **MAKE SURE TO CHANGE THIS PASSWORD ONCE YOU LOGIN**. *<- This step is currently not active, we're waiting for the frontend to catch up! Head down to the Manual SQL Configuration to take a look at what needs to be done by hand for now*. +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. + +```shell +cd ~/nodejs-pool/ +pm2 start init.js --name=blockManager --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=blockManager +pm2 start init.js --name=worker --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=worker +pm2 start init.js --name=payments --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=payments +pm2 start init.js --name=remoteShare --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=remoteShare +pm2 start init.js --name=longRunner --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=longRunner +pm2 start init.js --name=pool --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=pool +pm2 restart api +``` + +Install Script: +```bash +curl -L https://raw.githubusercontent.com/ArqTras/nodejs-pool/master/deployment/deploy_electroneum.bash | bash +``` + +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/electroneum/bin/electroneum-wallet-cli` +2. Make sure to save your regeneration stuff! +2. Start the wallet using PM2: `pm2 start /usr/local/src/electroneum/bin/electroneum-wallet-rpc -- --rpc-bind-port 26969 --wallet-file --password --disable-rpc-login --trusted-daemon` +3. 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!) +``` + +The shareHost configuration is designed to be pointed at wherever the leafApi endpoint exists. For xmrpool.net, we use https://api.xmrpool.net/leafApi. If you're using the automated setup script, you can use: `http:///leafApi`, as Caddy will proxy it. If you're just using localhost and a local pool serv, http://127.0.0.1:8000/leafApi will do you quite nicely + +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` + +Final Manual Steps +------------------ +Until the main frontend is done, we suggest running the following SQL line: +``` +DELETE FROM pool.users WHERE username = 'Administrator'; +``` +This will remove the administrator user until there's an easier way to change the password. Alternatively, you can change the password to something not known by the public: +``` +UPDATE pool.users SET email='your new password here' WHERE username='Administrator'; +``` +The email field is used as the default password field until the password is changed, at which point, it's hashed and dumped into the password field instead, and using the email field as a password is disabled. + +You should take a look at the [wiki](https://github.com/Snipa22/nodejs-pool/wiki/Configuration-Details) for specific configuration settings in the system. + +Pool Update Procedures +====================== +If upgrading the pool, please do a git pull to get the latest code within the pool's directory. + +Once complete, please `cd` into `sql_sync`, then run `node sql_sync.js` + +This will update your pool with the latest config options with any defaults that the pools may set. + +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: +```shell +ps fuax | grep + +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: +```shell +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. + + +PPS Fee Thoughts +================ +If you're considering PPS, I've spoken with [Fireice_UK](https://github.com/fireice-uk/) whom kindly did some math about what you're looking at in terms of requiements to run a PPS pool without it self-impoloding under particular risk factors, based on the work found [here](https://arxiv.org/pdf/1112.4980.pdf) + +```text +Also I calculated the amount of XMR needed to for a PPS pool to stay afloat. Perhaps you should put them up in the README to stop some spectacular clusterfucks :D: +For 1 in 1000000 chance that the pool will go bankrupt: 5% fee -> 1200 2% fee -> 3000 +For 1 in 1000000000 chance: 5% fee -> 1800 2% fee -> 4500 +``` + +The developers of the pool have not verified this, but based on our own usage on https://xmrpool.net/ this seems rather reasonable. You should be wary if you're consdering PPS and take you fees into account appropriately! + +Installation/Configuration Assistance +===================================== +If you need help installing the pool from scratch, please have your servers ready, which would be Ubuntu 16.04 servers, blank and clean, DNS records pointed. These need to be x86_64 boxes with AES-NI Available. + +SSH access with a sudo-enabled user will be needed, preferably the user that is slated to run the pool. + +Assistance is not available for frontend customization at this time. + +For assitance, please contact ArqTras at support@supportaeon.com + +Donations +=================== +If you'd like to make a one time donation, the addresses are as follows: +``` +* AEON WmtWzE4zej7FLjctgMPk7Va7hiiESJF5xHfmb5KZaMAhHDSRU51pqTJQiVFZgRMmrga9KvqPg48EZTEVQ9qA47o52MYgiMqaw +* XMR 4AYuDc4cEqxfxVTUFwVqPd4JdmKLjm9dNhTjuT6Ud5gQa564wp6cxMBWbwaVe4vUMveKAzAiA4j8xgUi29TpKXpm3zc3jmn +* ETN etnkLgWfr5uE8MZSZpsabb6HjG8Mig9qaS4wQ6Hu2VVKTiJT9Ucdrzz9CqGF9tycaWbntrSRr1CwVJDqGYPtumL72GhXwtoPvs +``` +MoneroOcean for His great job with pool system +``` +* AEON Address: WmtU2EMXGYSJVmPpw65AsAGi5WhT85z5n66D8vcT3RcxRBBj4tFiDcd2CVFcQ1bBpjNQD5Z5kbXrLjVidvoKFaFK1JbHsqMRw +Required PaymentID: e8a93311d23ecd418820d43d891112ece68f5e971a99dcafb9a6c8229bf3411a +* XMR - 499fS1Phq64hGeqV8p2AfXbf6Ax7gP6FybcMJq6Wbvg8Hw6xms8tCmdYpPsTLSaTNuLEtW4kF2DDiWCFcw4u7wSvFD8wFWE +* ETN - etnkQMp3Hmsay2p7uxokuHRKANrMDNASwQjDUgFb5L2sDM3jqUkYQPKBkooQFHVWBzEaZVzfzrXoETX6RbMEvg4R4csxfRHLo1 +* SUMO - Sumoo1DGS7c9LEKZNipsiDEqRzaUB3ws7YHfUiiZpx9SQDhdYGEEbZjRET26ewuYEWAZ8uKrz6vpUZkEVY7mDCZyGnQhkLpxKmy +``` +My pools: +================ +* https://supportaeon.com +* https://supportetn.eu +* https://graft.supportcryptonight.com + +Credits +======= +* [MoneroOcean](https://github.com/moneroocean) - Based pool system with many important modifications +* [Zone117x](https://github.com/zone117x) - Original [node-cryptonote-pool](https://github.com/zone117x/node-cryptonote-pool) from which, the stratum implementation has been borrowed. +* [Miziel](https://github.com/miziel) - Frontend mod [XMRPoolUI](https://github.com/miziel/poolui) +* [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) +* [Snipa22] https://github.com/Snipa22/nodejs-pool This awsome pool software diff --git a/SQL_MIGRATIONS.md b/SQL_MIGRATIONS.md new file mode 100644 index 0000000..355b13d --- /dev/null +++ b/SQL_MIGRATIONS.md @@ -0,0 +1,35 @@ +2/12/2017 +--------- +```sql +ALTER TABLE pool.config MODIFY item_value TEXT; +``` + +2/13/2017 +--------- +```sql +ALTER TABLE pool.payments ADD transfer_fee BIGINT(20) DEFAULT 0 NULL; +``` + +2/16/2017 +--------- +```sql +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.balance CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.bans CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.block_log CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.config CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.payments CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.pools CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.port_config CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.ports CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.shapeshiftTxn CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.transactions CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.users CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +ALTER TABLE pool.xmrtoTxn CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci; +``` + +2/25/2017 +--------- +```sql +ALTER TABLE pool.users ADD enable_email BOOL DEFAULT true NULL; +``` \ No newline at end of file diff --git a/coinConfig.json b/coinConfig.json new file mode 100644 index 0000000..49af0af --- /dev/null +++ b/coinConfig.json @@ -0,0 +1,10 @@ +{ + "aeon": { + "funcFile": "./lib/coins/aeon.js", + "paymentFile": "./payment_systems/aeon.js", + "sigDigits": 1000000000000, + "name": "Aeon Coin", + "mixIn": 4, + "shortCode": "AEON" + } +} diff --git a/config_example.json b/config_example.json new file mode 100644 index 0000000..d728ef1 --- /dev/null +++ b/config_example.json @@ -0,0 +1,14 @@ +{ + "pool_id": 0, + "bind_ip": "0.0.0.0", + "hostname": "testpool.com", + "db_storage_path": "CHANGEME", + "coin": "etn", + "mysql": { + "connectionLimit": 20, + "host": "127.0.0.1", + "database": "pool", + "user": "pool", + "password": "98erhfiuehw987fh23d" + } +} diff --git a/debug_scripts/block_add.js b/debug_scripts/block_add.js new file mode 100644 index 0000000..d1fa2a7 --- /dev/null +++ b/debug_scripts/block_add.js @@ -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 +// . + +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)); + }); +}); \ No newline at end of file diff --git a/debug_scripts/block_locker.js b/debug_scripts/block_locker.js new file mode 100644 index 0000000..e164eb0 --- /dev/null +++ b/debug_scripts/block_locker.js @@ -0,0 +1,19 @@ +"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; +comms = require('../lib/local_comms'); +global.database = new comms(); +global.database.initEnv(); +global.database.lockBlock(argv.blockID); +console.log("Block "+argv.blockID+" re-locked! Exiting!"); +process.exit(); \ No newline at end of file diff --git a/debug_scripts/block_migrator_from_old_sql.js b/debug_scripts/block_migrator_from_old_sql.js new file mode 100644 index 0000000..b23db7c --- /dev/null +++ b/debug_scripts/block_migrator_from_old_sql.js @@ -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 +// . + +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(); + }); + }); + }); +}); \ No newline at end of file diff --git a/debug_scripts/block_share_fix.js b/debug_scripts/block_share_fix.js new file mode 100644 index 0000000..f2f3d71 --- /dev/null +++ b/debug_scripts/block_share_fix.js @@ -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 +// . + +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); +}); \ No newline at end of file diff --git a/debug_scripts/socket_io.html b/debug_scripts/socket_io.html new file mode 100644 index 0000000..bce2a71 --- /dev/null +++ b/debug_scripts/socket_io.html @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/deployment/aeon.service b/deployment/aeon.service new file mode 100644 index 0000000..c805bda --- /dev/null +++ b/deployment/aeon.service @@ -0,0 +1,13 @@ +[Unit] +Description=Aeon Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/aeon/build/release/bin/aeon --rpc-bind-ip 127.0.0.1 --detach --restricted-rpc +Restart=always +User=aeondaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/alloy.service b/deployment/alloy.service new file mode 100644 index 0000000..5b83e3d --- /dev/null +++ b/deployment/alloy.service @@ -0,0 +1,12 @@ +[Unit] +Description=Alloy daemon +After=network.target + +[Service] +ExecStart=/usr/local/src/alloy/build/src/alloyd --rpc-bind-ip 127.0.0.1 --rpc-bind-port 1811 --log-file /home/alloydaemon/alloyd.log --enable_blockexplorer +WorkingDirectory=/home/alloydaemon/ +User=pooldaemon +Type=simple + +[Install] +WantedBy=multi-user.target diff --git a/deployment/baseaeon.sql b/deployment/baseaeon.sql new file mode 100644 index 0000000..89907c1 --- /dev/null +++ b/deployment/baseaeon.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '11181', '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', '11182', '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', '200000', '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', '.3', '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', '1', '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', '.25', 'float', 'Minimum XMR balance for payout to personal wallet'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '0', 'float', 'Donation to XMR core development'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '.2', 'float', 'Donation to pool developer'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'denom', '.001', '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', '1', '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', 'aeon_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', 'AEON', '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', 'aeon_to', 'string', 'Current best exchange'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'mixIn', '3', 'int', 'Mixin count for coins that support such things.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'statsBufferLength', '240', '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', 'WmtusoThH5t7kWACr3nhQGBmEPA1bxKPQD9RVEGyNB6k8E5f9WG1qvteFhvnzW7WA67HBGQfN824LRn2vYgSNq5Q1Jm6nmeV7', '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', 'mailgunNoCert', 'false', 'bool', 'Disable certificate check for MailGun'); +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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.01', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '60', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/baseaon.sql b/deployment/baseaon.sql new file mode 100644 index 0000000..39247ca --- /dev/null +++ b/deployment/baseaon.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '11181', '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', '11182', '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', '200000', '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', '.3', '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', '1', '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', '.25', 'float', 'Minimum XMR balance for payout to personal wallet'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '0', 'float', 'Donation to XMR core development'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '.2', 'float', 'Donation to pool developer'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'denom', '.001', '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', '1', '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', 'aeon_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', 'AEON', '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', 'aeon_to', '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', '240', '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', 'WmtusoThH5t7kWACr3nhQGBmEPA1bxKPQD9RVEGyNB6k8E5f9WG1qvteFhvnzW7WA67HBGQfN824LRn2vYgSNq5Q1Jm6nmeV7', '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', 'mailgunNoCert', 'false', 'bool', 'Disable certificate check for MailGun'); +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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.01', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '60', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/baseetn.sql b/deployment/baseetn.sql new file mode 100644 index 0000000..855a17a --- /dev/null +++ b/deployment/baseetn.sql @@ -0,0 +1,240 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '26968', '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', '26969', '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', '0.2', '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', '0.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', '0.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', '50', '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', '20', 'float', 'Minimum XMR balance for payout to personal wallet'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '0', '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', '0.1', 'float', 'Minimum balance that will be paid out to.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'blocksRequired', '30', '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', '100', 'int', 'Divisor for turning coin into human readable amounts '); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feesForTXN', '20', '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', '25000', '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', 'ETN', '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', '0', '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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.01', 'float', 'Amount to charge for the txn fee'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewEnd', '20', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '3', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '3', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/basegrf.sql b/deployment/basegrf.sql new file mode 100644 index 0000000..d042b59 --- /dev/null +++ b/deployment/basegrf.sql @@ -0,0 +1,240 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `GRFtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `GRFtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '120', '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', 'Graft Daemon RPC IP'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('daemon', 'port', '18981', 'int', 'Graft Daemon RPC Port'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'address', '127.0.0.1', 'string', 'graft Daemon RPC Wallet IP'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'port', '18982', 'int', 'Graft 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', '2', 'float', 'Fee charged for usage of the PPS pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'pplnsFee', '2', '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', '100', 'float', 'Minimum GRF balance for payout to exchange/payment ID'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'walletMin', '10', 'float', 'Minimum GRF balance for payout to personal wallet'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '2', 'float', 'Donation to GRF core development'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '2', 'float', 'Donation to pool developer'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'denom', '.0001', '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', '10000000000', '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 GRF 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', '5000', 'int', 'Maximum amount of GRF to send in a single transaction'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'shapeshiftPair', 'GRF_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', 'GRF', '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', 'GRFto', 'string', 'Current best exchange'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'mixIn', '5', '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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.4', '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', '15', '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.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '30', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '10', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting.'); +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 (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/baseipbc.sql b/deployment/baseipbc.sql new file mode 100644 index 0000000..82efa19 --- /dev/null +++ b/deployment/baseipbc.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '24182', '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', '24184', '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', '.00000001', 'float', 'Minimum balance that will be paid out to.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'blocksRequired', '10', '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', '100000000', 'int', 'Divisor for turning coin into human readable amounts '); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feesForTXN', '0.001', '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', '500', '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', 'IPBC', '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', '0', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '30', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 400000, 'Nicehash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/baseitns.sql b/deployment/baseitns.sql new file mode 100644 index 0000000..a9871b8 --- /dev/null +++ b/deployment/baseitns.sql @@ -0,0 +1,245 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(255) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '120', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 400000, 'Nicehash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'discordWebhook', '', 'string', 'Webhook path for Discord messages, this should be the part AFTER https://discordapp.com/api/webhooks/'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'discordBlockAnnounce', 'true', 'bool', 'Announce block finds in Discord'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'discordPayoutAnnounce', 'true', 'bool', 'Announce payout transactions in Discord'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pplns', 'minEffortRetention', '750', 'int', 'Minimum difficulty of worth of shares to retain, measured in round effort. This helps make sure that multiShare * difficulty worth of shares is available even when network difficulty is extemely volatile.'); diff --git a/deployment/basestl.sql b/deployment/basestl.sql new file mode 100644 index 0000000..735b44c --- /dev/null +++ b/deployment/basestl.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '60', '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', '20189', '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', '26969', 'int', 'Ned to put custom 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', '1', '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', '0.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', '0.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', '500', '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', '100', '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', '0.01', 'float', 'Minimum balance that will be paid out to.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'blocksRequired', '18', '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', '100', 'int', 'Divisor for turning coin into human readable amounts '); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feesForTXN', '0.01', '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', '25000', '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', 'STL', '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', '0', '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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '.01', 'float', 'Amount to charge for the txn fee'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewEnd', '20', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '3', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '3', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 10000, 'NiceHash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/basesumo.sql b/deployment/basesumo.sql new file mode 100644 index 0000000..eb3ee49 --- /dev/null +++ b/deployment/basesumo.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '19734', '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', '19735', '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', '0.2', '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', '0.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', '0.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', '10', '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', '0.5', 'float', 'Minimum XMR balance for payout to personal wallet'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'devDonation', '0.3', 'float', 'Donation to XMR core development'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '0.3', 'float', 'Donation to pool developer'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'denom', '0.001', 'float', 'Minimum balance that will be paid out to.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'blocksRequired', '30', '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', '1000000000', 'int', 'Divisor for turning coin into human readable amounts '); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feesForTXN', '0.001', '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', 'SUMO', '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', '12', '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', 'false', 'bool', 'Enable SOLO mining on the pool'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewAmount', '0.01', 'float', 'Amount to charge for the txn fee'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'feeSlewEnd', '50', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '30', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '1', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 400000, 'Nicehash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/basexao.sql b/deployment/basexao.sql new file mode 100644 index 0000000..115c8cd --- /dev/null +++ b/deployment/basexao.sql @@ -0,0 +1,242 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(255) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(255) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(255) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', '180', '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', 'Alloy Daemon RPC IP'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('daemon', 'port', '1811', 'int', 'Alloy Daemon RPC Port'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'address', '127.0.0.1', 'string', 'Alloy Daemon RPC Wallet IP'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('wallet', 'port', '8070', 'int', 'Alloy 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', '.4', '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', '0.2', 'float', 'Donation to XMR core development'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'poolDevDonation', '0.2', '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', 'XAO', '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', 'false', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '120', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 400000, 'Nicehash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('pplns', 'minEffortRetention', '750', 'int', 'Minimum difficulty of worth of shares to retain, measured in round effort. This helps make sure that multiShare * difficulty worth of shares is available even when network difficulty is extemely volatile.'); diff --git a/deployment/basexmv.sql b/deployment/basexmv.sql new file mode 100644 index 0000000..1057213 --- /dev/null +++ b/deployment/basexmv.sql @@ -0,0 +1,241 @@ +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; +ALTER DATABASE pool DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE TABLE `balance` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `last_edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `payment_address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `pool_type` varchar(64) DEFAULT NULL, + `bitcoin` tinyint(1) DEFAULT NULL, + `amount` bigint(26) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `balance_id_uindex` (`id`), + UNIQUE KEY `balance_payment_address_pool_type_bitcoin_payment_id_uindex` (`payment_address`,`pool_type`,`bitcoin`,`payment_id`), + KEY `balance_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `bans` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `ip_address` varchar(40) DEFAULT NULL, + `mining_address` varchar(200) DEFAULT NULL, + `active` tinyint(1) DEFAULT '1', + `ins_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `bans_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `block_log` ( + `id` int(11) NOT NULL COMMENT 'Block Height', + `orphan` tinyint(1) DEFAULT '1', + `hex` varchar(128) NOT NULL, + `find_time` timestamp NULL DEFAULT NULL, + `reward` bigint(20) DEFAULT NULL, + `difficulty` bigint(20) DEFAULT NULL, + `major_version` int(11) DEFAULT NULL, + `minor_version` int(11) DEFAULT NULL, + PRIMARY KEY (`hex`), + UNIQUE KEY `block_log_hex_uindex` (`hex`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `module` varchar(32) DEFAULT NULL, + `item` varchar(32) DEFAULT NULL, + `item_value` mediumtext, + `item_type` varchar(64) DEFAULT NULL, + `Item_desc` varchar(512) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `config_id_uindex` (`id`), + UNIQUE KEY `config_module_item_uindex` (`module`,`item`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `payments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `unlocked_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `paid_time` timestamp NOT NULL DEFAULT '1970-01-01 00:00:01', + `pool_type` varchar(64) DEFAULT NULL, + `payment_address` varchar(125) DEFAULT NULL, + `transaction_id` int(11) DEFAULT NULL COMMENT 'Transaction ID in the transactions table', + `bitcoin` tinyint(1) DEFAULT '0', + `amount` bigint(20) DEFAULT NULL, + `block_id` int(11) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `transfer_fee` bigint(20) DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `payments_id_uindex` (`id`), + KEY `payments_transactions_id_fk` (`transaction_id`), + KEY `payments_payment_address_payment_id_index` (`payment_address`,`payment_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `pools` ( + `id` int(11) NOT NULL, + `ip` varchar(72) NOT NULL, + `last_checkin` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `active` tinyint(1) NOT NULL, + `blockID` int(11) DEFAULT NULL, + `blockIDTime` timestamp NULL DEFAULT NULL, + `hostname` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pools_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `port_config` ( + `poolPort` int(11) NOT NULL, + `difficulty` int(11) DEFAULT '1000', + `portDesc` varchar(128) DEFAULT NULL, + `portType` varchar(16) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ssl` tinyint(1) DEFAULT '0', + PRIMARY KEY (`poolPort`), + UNIQUE KEY `port_config_poolPort_uindex` (`poolPort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `ports` ( + `pool_id` int(11) DEFAULT NULL, + `network_port` int(11) DEFAULT NULL, + `starting_diff` int(11) DEFAULT NULL, + `port_type` varchar(64) DEFAULT NULL, + `description` varchar(256) DEFAULT NULL, + `hidden` tinyint(1) DEFAULT '0', + `ip_address` varchar(256) DEFAULT NULL, + `lastSeen` timestamp NULL DEFAULT NULL, + `miners` int(11) DEFAULT NULL, + `ssl_port` tinyint(1) DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `shapeshiftTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `shapeshiftTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `bitcoin` tinyint(1) DEFAULT NULL, + `address` varchar(128) DEFAULT NULL, + `payment_id` varchar(128) DEFAULT NULL, + `xmr_amt` bigint(26) DEFAULT NULL, + `btc_amt` bigint(26) DEFAULT NULL, + `transaction_hash` varchar(128) DEFAULT NULL, + `submitted_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `mixin` int(11) DEFAULT NULL, + `fees` bigint(26) DEFAULT NULL, + `payees` int(11) DEFAULT NULL, + `exchange_rate` bigint(26) DEFAULT NULL, + `confirmed` tinyint(1) DEFAULT NULL, + `confirmed_time` timestamp NULL DEFAULT NULL, + `exchange_name` varchar(64) DEFAULT NULL, + `exchange_txn_id` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `transactions_id_uindex` (`id`), + KEY `transactions_shapeshiftTxn_id_fk` (`exchange_txn_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(256) NOT NULL, + `pass` varchar(64) DEFAULT NULL, + `email` varchar(256) DEFAULT NULL, + `admin` tinyint(1) DEFAULT '0', + `payout_threshold` bigint(16) DEFAULT '0', + `enable_email` tinyint(1) DEFAULT '1', + PRIMARY KEY (`id`), + UNIQUE KEY `users_id_uindex` (`id`), + UNIQUE KEY `users_username_uindex` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `xmrtoTxn` ( + `id` varchar(64) NOT NULL, + `address` varchar(128) DEFAULT NULL, + `paymentID` varchar(128) DEFAULT NULL, + `depositType` varchar(16) DEFAULT NULL, + `withdrawl` varchar(128) DEFAULT NULL, + `withdrawlType` varchar(16) DEFAULT NULL, + `returnAddress` varchar(128) DEFAULT NULL, + `returnAddressType` varchar(16) DEFAULT NULL, + `txnStatus` varchar(64) DEFAULT NULL, + `amountDeposited` bigint(26) DEFAULT NULL, + `amountSent` float DEFAULT NULL, + `transactionHash` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `xmrtoTxn_id_uindex` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +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', 'XMV', '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', 'http://127.0.0.1:8000/leafApi', 'string', 'Host that receives share information'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingBody', 'Your worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerNotHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker stops hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingBody', 'Your worker: %(worker)s has started submitting hashes at: %(timestamp)s UTC\n', 'string', 'Email sent to the miner when their worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('email', 'workerStartHashingSubject', 'Status of your worker(s)', 'string', 'Subject of email sent to miner when worker starts hashing'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('general', 'emailSig', 'NodeJS-Pool Administration Team', 'string', 'Signature line for the emails.'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timer', '120', 'int', 'Number of minutes between main payment daemon cycles'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'timerRetry', '25', 'int', 'Number of minutes between payment daemon retrying due to not enough funds'); +INSERT INTO pool.config (module, item, item_value, item_type, Item_desc) VALUES ('payout', 'priority', '0', 'int', 'Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)'); +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 (8888, 400000, 'Nicehash', 'pplns', 0, 0); +INSERT INTO pool.port_config (poolPort, difficulty, portDesc, portType, hidden, `ssl`) VALUES (9000, 20000, 'Claymore SSL', 'pplns', 0, 1); diff --git a/deployment/caddyfile b/deployment/caddyfile new file mode 100644 index 0000000..9352fcf --- /dev/null +++ b/deployment/caddyfile @@ -0,0 +1,18 @@ +# Catch-all vhost +:80 { + root /var/www + proxy /leafApi 127.0.0.1:8000 + proxy /api 127.0.0.1:8001 { + without /api + } + proxy /socket.io 127.0.0.1:8001 { + header_upstream Connection {>Connection} + header_upstream Upgrade {>Upgrade} + } + header / { + Access-Control-Allow-Methods "GET, POST, OPTIONS" + Access-Control-Allow-Headers "Content-Type, x-access-token" + } + gzip + mime .manifest text/cache-manifest +} diff --git a/deployment/deploy_aeon.bash b/deployment/deploy_aeon.bash new file mode 100644 index 0000000..785ce88 --- /dev/null +++ b/deployment/deploy_aeon.bash @@ -0,0 +1,89 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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 -b aeon-rebase-new https://github.com/stoffu/monero.git aeon +cd aeon +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/aeon.service /lib/systemd/system/ +sudo useradd -m aeondaemon -d /home/aeondaemon +sudo systemctl daemon-reload +sudo systemctl enable aeon +sudo systemctl start aeon +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/ArqTras/pooluiaeon.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/baseaeon.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_alloy.bash b/deployment/deploy_alloy.bash new file mode 100644 index 0000000..140ba33 --- /dev/null +++ b/deployment/deploy_alloy.bash @@ -0,0 +1,100 @@ +#!/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) +WALLET_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) +CURUSER=$(whoami) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/alloyproject/alloy-pool.git nodejs-pool # 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/alloyproject/alloy.git +cd alloy +sudo mkdir build +cd build +sudo cmake .. +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/alloy.service /lib/systemd/system/ +sudo useradd -m alloydaemon -d /home/alloydaemon +echo exit | sudo /usr/local/src/alloy/build/src/simplewallet --generate-new-wallet /home/alloydaemon/pool.wallet --password $WALLET_PASS # generate pool wallet +echo exit | sudo /usr/local/src/alloy/build/src/simplewallet --generate-new-wallet /home/alloydaemon/fee.wallet --password $WALLET_PASS #generate fee wallet +sudo sed -i "s/password=/password=$WALLET_PASS/g" ~/nodejs-pool/deployment/walletd.service +sudo cp ~/nodejs-pool/deployment/walletd.service /lib/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable alloy +sudo systemctl enable walletd +sudo systemctl start alloy + +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/alexmateescu/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +sudo mysql -u root --password=$ROOT_SQL_PASS < deployment/basexao.sql +sudo mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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.')" +sudo mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +echo "Your wallet passord is $WALLET_PASS. It will work for both pool and fee wallets. You can find the wallet addreses in simplewallet.log" +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!" diff --git a/deployment/deploy_electroneum.bash b/deployment/deploy_electroneum.bash new file mode 100644 index 0000000..e57f676 --- /dev/null +++ b/deployment/deploy_electroneum.bash @@ -0,0 +1,95 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +sudo git clone https://github.com/ArqTras/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/electroneum/electroneum.git +cd electroneum +sudo git checkout +sudo curl https://raw.githubusercontent.com/arqtras/nodejs-pool/master/deployment/electroneum_daemon.patch | sudo git apply -v +sudo cmake . +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/electroneum.service /lib/systemd/system/ +sudo useradd -m electroneumdaemon -d /home/electroneumdaemon +sudo systemctl daemon-reload +sudo systemctl enable electroneum +sudo systemctl start electroneum +sudo curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +cd ~/nodejs-pool +sudo chown -R $USER ~/nodejs-pool +npm install +npm install -g pm2 +sudo 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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +sudo git clone https://github.com/arqtras/poolui.git +cd poolui +sudo chown -R $USER ~/poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +sudo rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/baseetn.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +cd ~ +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_grf.bash b/deployment/deploy_grf.bash new file mode 100644 index 0000000..5312138 --- /dev/null +++ b/deployment/deploy_grf.bash @@ -0,0 +1,90 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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/graft-project/GraftNetwork.git +cd GraftNetwork +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/graft.service /lib/systemd/system/ +sudo useradd -m graftdaemon -d /home/graftdaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u graftdaemon mktemp -d) +sudo systemctl daemon-reload +sudo systemctl enable graft +sudo systemctl start graft +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/miziel/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/basegrf.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_ipbc.bash b/deployment/deploy_ipbc.bash new file mode 100644 index 0000000..a71dd58 --- /dev/null +++ b/deployment/deploy_ipbc.bash @@ -0,0 +1,107 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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/forknote/forknote.git +cd forknote +#sudo git checkout v0.11.1.0 +#curl https://raw.githubusercontent.com/Snipa22/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo cmake . +sudo make -j$(nproc) +cd src +sudo git clone https://github.com/forknote/configs.git +cd /usr/local/src +sudo git clone https://github.com/ipbc-dev/ipbc.git +cd ipbc +sudo git checkout v0.11.1.0 +#curl https://raw.githubusercontent.com/Snipa22/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo mkdir build +cd build +sudo cmake .. +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/ipbc.service /lib/systemd/system/ +sudo useradd -m ipbc -d /home/ipbc +#BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u monerodaemon mktemp -d) +#sudo -u monerodaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://downloads.getmonero.org/blockchain.raw +#sudo -u monerodaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/monerodaemon/.bitmonero +#sudo -u monerodaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +sudo systemctl daemon-reload +sudo systemctl enable ipbc +sudo systemctl start ipbc +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/miziel/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/baseipbc.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_itns.bash b/deployment/deploy_itns.bash new file mode 100644 index 0000000..938c087 --- /dev/null +++ b/deployment/deploy_itns.bash @@ -0,0 +1,94 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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/valiant1x/intensecoin.git +cd intensecoin +sudo git checkout xmr +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/intense.service /lib/systemd/system/ +sudo useradd -m intensedaemon -d /home/intensedaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u intensedaemon mktemp -d) +sudo -u intensedaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://github.com/valiant1x/intensecoin/releases/download/1.4.3/blockchain.raw +sudo -u intensedaemon /usr/local/src/intensecoin/build/release/bin/intense-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/intensedaemon/.intensecoin +sudo -u intensedaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +sudo systemctl daemon-reload +sudo systemctl enable intense +sudo systemctl start intense +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/ArqTras/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/baseitns.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_stellite.bash b/deployment/deploy_stellite.bash new file mode 100644 index 0000000..9b3f718 --- /dev/null +++ b/deployment/deploy_stellite.bash @@ -0,0 +1,95 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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/stellitecoin/Stellite.git stellite +cd stellite +#sudo git checkout v0.11.1.0 +curl https://raw.githubusercontent.com/vtnplus/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/stellite.service /lib/systemd/system/ +sudo useradd -m pooldaemon -d /home/pooldaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u pooldaemon mktemp -d) +#sudo -u pooldaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://downloads.getmonero.org/blockchain.raw +#sudo -u pooldaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/pooldaemon/.bitmonero +#sudo -u pooldaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +sudo systemctl daemon-reload +sudo systemctl enable stellite +sudo systemctl start stellite +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/miziel/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/basestl.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_sumo.bash b/deployment/deploy_sumo.bash new file mode 100644 index 0000000..9412410 --- /dev/null +++ b/deployment/deploy_sumo.bash @@ -0,0 +1,95 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev doxygen graphviz +cd ~ +git clone https://github.com/ArqTras/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/sumoprojects/sumokoin.git sumokoin +cd sumokoin +#sudo git checkout v0.11.1.0 +#curl https://raw.githubusercontent.com/vtnplus/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/sumo.service /lib/systemd/system/ +sudo useradd -m pooldaemon -d /home/pooldaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u pooldaemon mktemp -d) +#sudo -u pooldaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://downloads.getmonero.org/blockchain.raw +#sudo -u pooldaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/pooldaemon/.bitmonero +#sudo -u pooldaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +sudo systemctl daemon-reload +sudo systemctl enable sumo.service +sudo systemctl start sumo.service +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/miziel/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/basesumo.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/deploy_xmv.bash b/deployment/deploy_xmv.bash new file mode 100644 index 0000000..ecccc92 --- /dev/null +++ b/deployment/deploy_xmv.bash @@ -0,0 +1,95 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/ArqTras/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 v0.11.1.0 +#curl https://raw.githubusercontent.com/Snipa22/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/monero.service /lib/systemd/system/ +sudo useradd -m monerodaemon -d /home/monerodaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u monerodaemon mktemp -d) +#sudo -u monerodaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://downloads.getmonero.org/blockchain.raw +#sudo -u monerodaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/monerodaemon/.bitmonero +#sudo -u monerodaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +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 v8.9.3 +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/ +sed -r "s/(\"db_storage_path\": ).*/\1\"\/home\/$CURUSER\/pool_db\/\",/" config_example.json > config.json +cd ~ +git clone https://github.com/miziel/poolui.git +cd poolui +npm install +./node_modules/bower/bin/bower update +./node_modules/gulp/bin/gulp.js build +cd build +sudo ln -s `pwd` /var/www +CADDY_DOWNLOAD_DIR=$(mktemp -d) +cd $CADDY_DOWNLOAD_DIR +curl -sL "https://snipanet.com/caddy.tar.gz" | tar -xz caddy init/linux-systemd/caddy.service +sudo mv 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 sh -c "sed 's/ProtectHome=true/ProtectHome=false/' init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service" +sudo chown root:root /etc/systemd/system/caddy.service +sudo chmod 644 /etc/systemd/system/caddy.service +sudo systemctl daemon-reload +sudo systemctl enable caddy.service +sudo systemctl start caddy.service +rm -rf $CADDY_DOWNLOAD_DIR +cd ~ +sudo env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +cd ~/nodejs-pool +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +mysql -u root --password=$ROOT_SQL_PASS < deployment/basexmv.sql +mysql -u root --password=$ROOT_SQL_PASS pool -e "INSERT INTO pool.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 pool.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 --log-date-format="YYYY-MM-DD HH:mm Z" -- --module=api +bash ~/nodejs-pool/deployment/install_lmdb_tools.sh +cd ~/nodejs-pool/sql_sync/ +env PATH=$PATH:`pwd`/.nvm/versions/node/v8.9.3/bin node sql_sync.js +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!" diff --git a/deployment/electroneum.service b/deployment/electroneum.service new file mode 100644 index 0000000..25ebced --- /dev/null +++ b/deployment/electroneum.service @@ -0,0 +1,14 @@ +[Unit] +Description=Electroneum Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/electroneum/bin/electroneumd --rpc-bind-ip 127.0.0.1 --rpc-bind-port 26968 --detach +ExecStop=/usr/local/src/electroneum/bin/electroneumd exit +Restart=always +User=electroneumdaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/electroneum_daemon.patch b/deployment/electroneum_daemon.patch new file mode 100644 index 0000000..5f681f7 --- /dev/null +++ b/deployment/electroneum_daemon.patch @@ -0,0 +1,13 @@ +diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp +index 4b3fa787..a2340139 100644 +--- a/src/cryptonote_core/tx_pool.cpp ++++ b/src/cryptonote_core/tx_pool.cpp +@@ -863,7 +863,7 @@ namespace cryptonote + LockedTXN lock(m_blockchain); + + 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) + { + txpool_tx_meta_t meta = m_blockchain.get_txpool_tx_meta(sorted_it->second); + LOG_PRINT_L2("Considering " << sorted_it->second << ", size " << meta.blob_size << ", current block size " << total_size << "/" << max_total_size << ", current coinbase " << print_money(best_coinbase)); diff --git a/deployment/graft.service b/deployment/graft.service new file mode 100644 index 0000000..db3fee2 --- /dev/null +++ b/deployment/graft.service @@ -0,0 +1,13 @@ +[Unit] +Description=Graft Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/GraftNetwork/build/release/bin/graftnoded --rpc-bind-ip 127.0.0.1 --detach +Restart=always +User=graftdaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/install_lmdb_tools.sh b/deployment/install_lmdb_tools.sh new file mode 100644 index 0000000..80b95c5 --- /dev/null +++ b/deployment/install_lmdb_tools.sh @@ -0,0 +1,12 @@ +#!/bin/bash +cd ~ +git clone https://github.com/LMDB/lmdb +cd lmdb +git checkout 4d2154397afd90ca519bfa102b2aad515159bd50 +cd libraries/liblmdb/ +make -j `nproc` +mkdir ~/.bin +echo ' ' >> ~/.bashrc +echo 'export PATH=~/.bin:$PATH' >> ~/.bashrc +for i in mdb_copy mdb_dump mdb_load mdb_stat; do cp $i ~/.bin/; done +echo "Please run source ~/.bashrc to initialize the new LMDB tools. Thanks for flying Snipa22 Patch Services." \ No newline at end of file diff --git a/deployment/intense.service b/deployment/intense.service new file mode 100644 index 0000000..f061501 --- /dev/null +++ b/deployment/intense.service @@ -0,0 +1,13 @@ +[Unit] +Description=Intense Coin Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/intensecoin/build/release/bin/intensecoind --rpc-bind-ip 127.0.0.1 --detach --restricted-rpc +Restart=always +User=intensedaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/ipbc.service b/deployment/ipbc.service new file mode 100644 index 0000000..fa8082a --- /dev/null +++ b/deployment/ipbc.service @@ -0,0 +1,14 @@ +[Unit] +Description=Interstellar Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/ipbc/build/src/ipbcd --rpc-bind-ip 127.0.0.1 --rpc-bind-port 24182 +ExecStop=/usr/local/src/ipbc/build/src/ipbcd exit +Restart=always +User=ipbc + +[Install] +WantedBy=multi-user.target diff --git a/deployment/leaf.bash b/deployment/leaf.bash new file mode 100644 index 0000000..de9ceb6 --- /dev/null +++ b/deployment/leaf.bash @@ -0,0 +1,45 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/arqtras/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/electroneum/electroneum.git +cd electroneum +sudo git checkout +curl https://raw.githubusercontent.com/arqtras/nodejs-pool/master/deployment/electroneum_daemon.patch | sudo git apply -v +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/electroneum.service /lib/systemd/system/ +sudo useradd -m electroneumdaemon -d /home/electroneumdaemon +sudo systemctl daemon-reload +sudo systemctl enable electroneum +sudo systemctl start electroneum +curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash +source ~/.nvm/nvm.sh +nvm install v8.9.3 +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/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +echo "You're setup with a leaf node! Congrats" diff --git a/deployment/leaf_monero.bash b/deployment/leaf_monero.bash new file mode 100644 index 0000000..5e2326a --- /dev/null +++ b/deployment/leaf_monero.bash @@ -0,0 +1,49 @@ +#!/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) +sudo timedatectl set-timezone Etc/UTC +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 libzmq3-dev +cd ~ +git clone https://github.com/Venthos/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 v0.11.0.0 +curl https://raw.githubusercontent.com/Venthos/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo make -j$(nproc) +sudo cp ~/nodejs-pool/deployment/monero.service /lib/systemd/system/ +sudo useradd -m monerodaemon -d /home/monerodaemon +BLOCKCHAIN_DOWNLOAD_DIR=$(sudo -u monerodaemon mktemp -d) +sudo -u monerodaemon wget --limit-rate=50m -O $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw https://downloads.getmonero.org/blockchain.raw +sudo -u monerodaemon /usr/local/src/monero/build/release/bin/monero-blockchain-import --input-file $BLOCKCHAIN_DOWNLOAD_DIR/blockchain.raw --batch-size 20000 --database lmdb#fastest --verify off --data-dir /home/monerodaemon/.bitmonero +sudo -u monerodaemon rm -rf $BLOCKCHAIN_DOWNLOAD_DIR +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 v8.9.3 +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/v8.9.3/bin `pwd`/.nvm/versions/node/v8.9.3/lib/node_modules/pm2/bin/pm2 startup systemd -u $CURUSER --hp `pwd` +sudo chown -R $CURUSER. ~/.pm2 +echo "Installing pm2-logrotate in the background!" +pm2 install pm2-logrotate & +echo "You're setup with a leaf node! Congrats" diff --git a/deployment/monero_daemon.patch b/deployment/monero_daemon.patch new file mode 100644 index 0000000..5f681f7 --- /dev/null +++ b/deployment/monero_daemon.patch @@ -0,0 +1,13 @@ +diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp +index 4b3fa787..a2340139 100644 +--- a/src/cryptonote_core/tx_pool.cpp ++++ b/src/cryptonote_core/tx_pool.cpp +@@ -863,7 +863,7 @@ namespace cryptonote + LockedTXN lock(m_blockchain); + + 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) + { + txpool_tx_meta_t meta = m_blockchain.get_txpool_tx_meta(sorted_it->second); + LOG_PRINT_L2("Considering " << sorted_it->second << ", size " << meta.blob_size << ", current block size " << total_size << "/" << max_total_size << ", current coinbase " << print_money(best_coinbase)); diff --git a/deployment/stellite.service b/deployment/stellite.service new file mode 100644 index 0000000..8566a43 --- /dev/null +++ b/deployment/stellite.service @@ -0,0 +1,13 @@ +[Unit] +Description=Stellite Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/stellite/build/release/bin/stellited --rpc-bind-ip 127.0.0.1 --detach +Restart=always +User=pooldaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/sumo.service b/deployment/sumo.service new file mode 100644 index 0000000..6a9850f --- /dev/null +++ b/deployment/sumo.service @@ -0,0 +1,13 @@ +[Unit] +Description=Sumo Daemon +After=network.target + +[Service] +Type=forking +GuessMainPID=no +ExecStart=/usr/local/src/sumokoin/build/release/bin/sumokoind --rpc-bind-ip 127.0.0.1 --detach +Restart=always +User=pooldaemon + +[Install] +WantedBy=multi-user.target diff --git a/deployment/upgrade_electroneum.bash b/deployment/upgrade_electroneum.bash new file mode 100644 index 0000000..d9bbbad --- /dev/null +++ b/deployment/upgrade_electroneum.bash @@ -0,0 +1,13 @@ +#!/bin/bash +echo "This assumes that you have 3DITGuy ETN nodejs-pool install, and will patch and update it to the latest stable builds of Electroneum." +sleep 15 +echo "Continuing install, this will prompt you for your password if you didn't enable passwordless sudo. Please do not run me as root!" +cd /usr/local/src/electroneum +sudo git checkout . +sudo git checkout master +sudo git pull +sudo git checkout origin/release-v0.11.0.0 +curl -L https://raw.githubusercontent.com/3ditguy/nodejs-pool/master/deployment/electroneum_daemon.patch | sudo git apply -v +sudo rm -rf build +sudo make -j$(nproc) +echo "Done building the new electroneum daemon! Please go ahead and reboot electroneum with: sudo systemctl restart electroneum as soon as the pool source is updated!" diff --git a/deployment/upgrade_monero.bash b/deployment/upgrade_monero.bash new file mode 100644 index 0000000..f599176 --- /dev/null +++ b/deployment/upgrade_monero.bash @@ -0,0 +1,13 @@ +#!/bin/bash +echo "This assumes that you have a standard nodejs-pool install, and will patch and update it to the latest stable builds of Monero." +sleep 15 +echo "Continuing install, this will prompt you for your password if you didn't enable passwordless sudo. Please do not run me as root!" +cd /usr/local/src/monero +sudo git checkout . +sudo git checkout master +sudo git pull +sudo git checkout origin/release-v0.11.0.0 +curl -L https://raw.githubusercontent.com/Venthos22/nodejs-pool/master/deployment/monero_daemon.patch | sudo git apply -v +sudo rm -rf build +sudo make -j$(nproc) +echo "Done building the new Monero daemon! Please go ahead and reboot monero with: sudo systemctl restart monero as soon as the pool source is updated!" diff --git a/deployment/walletd.service b/deployment/walletd.service new file mode 100644 index 0000000..37aaf7f --- /dev/null +++ b/deployment/walletd.service @@ -0,0 +1,13 @@ +[Unit] +Description=Wallet Daemon +After=network.target + +[Service] +Type=simple +GuessMainPID=no +ExecStart=/usr/local/src/alloy/build/src/simplewallet --wallet-file /home/alloydaemon/pool.wallet --password= --rpc-bind-ip=127.0.0.1 --rpc-bind-port 8070 --daemon-port=1811 --set_log 4 +Restart=always +User=root + +[Install] +WantedBy=multi-user.target diff --git a/init.js b/init.js new file mode 100644 index 0000000..1a2f257 --- /dev/null +++ b/init.js @@ -0,0 +1,113 @@ +"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'); +let path = require('path'); + +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')); +global.argv = argv; +let comms; +let coinInc; + +// Config Table Layout +// . + +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); + if (argv.hasOwnProperty('tool') && fs.existsSync('./tools/'+argv.tool+'.js')) { + require('./tools/'+argv.tool+'.js'); + } else if (argv.hasOwnProperty('module')){ + 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); + } + } else { + console.error("Invalid module/tool provided. Please provide a valid module/tool"); + console.error("Valid Modules: pool, blockManager, payments, api, remoteShare, worker, longRunner"); + let valid_tools = "Valid Tools: "; + fs.readdirSync('./tools/').forEach(function(line){ + valid_tools += path.parse(line).name + ", "; + }); + valid_tools = valid_tools.slice(0, -2); + console.error(valid_tools); + process.exit(1); + } +}); \ No newline at end of file diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..cb9737b --- /dev/null +++ b/lib/api.js @@ -0,0 +1,851 @@ +"use strict"; +const express = require('express'); // call express +const app = express(); // define our app using express +const server = require('http').createServer(app); +const cluster = require('cluster'); +const async = require("async"); +const debug = require("debug")("api"); +const btcValidator = require('wallet-address-validator'); +const cnUtil = require('cryptonote-util'); +const bodyParser = require('body-parser'); +const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens +const crypto = require('crypto'); +const cors = require('cors'); + +let addressBase58Prefix = cnUtil.address_decode(new Buffer(global.config.pool.address)); +let threadName = ""; + +if (cluster.isMaster) { + threadName = "(Master) "; +} else { + threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") "; +} + +let pool_list = []; +if(global.config.pplns.enable === true){ + pool_list.push('pplns'); +} +if(global.config.pps.enable === true){ + pool_list.push('pps'); +} +if(global.config.solo.enable === true){ + pool_list.push('solo'); +} + +app.use(cors({origin: true})); +app.use(bodyParser.urlencoded({extended: false})); +app.use(bodyParser.json()); + +// Support Functions that are reused now +function getAllWorkerHashCharts(address, callback){ + let identifiers = global.database.getCache(address + '_identifiers'); + let returnData = {global: global.database.getCache(address)['hashHistory']}; + if (identifiers !== false){ + identifiers.sort(); + } else { + return returnData; + } + let intCounter = 0; + identifiers.forEach(function(identifier){ + returnData[identifier] = global.database.getCache(address+"_"+identifier)['hashHistory']; + intCounter += 1; + if (intCounter === identifiers.length){ + return callback(null, returnData); + } + }); +} + +function getAllWorkerStats(address, callback){ + let identifiers = global.database.getCache(address + '_identifiers'); + 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; + if (identifiers === false){ + return callback(null, returnData); + } + identifiers.sort().forEach(function(identifier){ + let cachedData = global.database.getCache(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 callback(null, returnData); + } + }); +} + +function getAddressStats(address, extCallback){ + let address_parts = address.split('.'); + let address_pt = address_parts[0]; + let payment_id = address_parts[1]; + let cachedData = global.database.getCache(address); + let paidQuery = "SELECT SUM(amount) as amt FROM payments WHERE payment_address = ? AND payment_id = ?"; + let txnQuery = "SELECT count(id) 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 ?"; + txnQuery = "SELECT count(id) 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: cachedData.hash, identifier: 'global', lastHash: Math.floor(cachedData.lastHash / 1000), + totalHashes: cachedData.totalHashes, validShares: Number(cachedData.goodShares), invalidShares: Number(cachedData.badShares)}); + }, + 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(null, returnData); + }); + }, + function (returnData, callback) { + debug(threadName + "Checking MySQL total amount unpaid for /miner/address/stats"); + global.mysql.query(txnQuery, [address_pt, payment_id]).then(function (rows) { + if (typeof(rows[0]) === 'undefined') { + returnData.txnCount = 0; + } else { + returnData.txnCount = rows[0].amt; + if (returnData.txnCount === null) { + returnData.txnCount = 0; + } + } + return callback(true, returnData); + }); + } + ], function (err, result) { + debug(threadName + "Result information for " + address + ": " + JSON.stringify(result)); + if (err === true) { + return extCallback(null, result); + } + if (err) { + console.error(threadName + "Error within the miner stats identifier func"); + return extCallback(err.toString()); + } + }); +} + +// 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, + pool_dev_donation: global.config.payout.poolDevDonation, + maturity_depth: global.config.payout.blocksRequired, + min_denom: global.config.payout.denom * global.config.general.sigDivisor, + coin_code: global.config.general.coinCode + }); +}); + +// 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); + let lastPayment = global.database.getCache('lastPaymentCycle'); + res.json({pool_list: pool_list, pool_statistics: localCache, last_payment: !lastPayment ? 0 : lastPayment}); +}); + +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) { + let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25; + let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0; + res.json(global.database.getBlockList(req.params.pool_type).slice(page*limit, (page + 1) * limit)); +}); + +app.get('/pool/blocks', function (req, res) { + let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25; + let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0; + res.json(global.database.getBlockList().slice(page*limit, (page + 1) * limit)); +}); + +app.get('/pool/payments/:pool_type', function (req, res) { + let pool_type = req.params.pool_type; + let limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 10; + let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0; + 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 DESC LIMIT ? OFFSET ?"; + let response = []; + global.mysql.query(query, [pool_type, limit, page * limit]).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 limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 10; + let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0; + let query = "SELECT * FROM transactions ORDER BY id DESC LIMIT ? OFFSET ?"; + global.mysql.query(query, [limit, page * limit]).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 pool 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 limit = typeof(req.query.limit) !== 'undefined' ? Number(req.query.limit) : 25; + let page = typeof(req.query.page) !== 'undefined' ? Number(req.query.page) : 0; + let address_parts = req.params.address.split('.'); + let address = address_parts[0]; + let payment_id = address_parts[1]; + let query = "SELECT amount as amt, pool_type, transaction_id, UNIX_TIMESTAMP(paid_time) as ts FROM " + + "payments WHERE payment_address = ? AND payment_id = ? ORDER BY paid_time DESC LIMIT ? OFFSET ?"; + 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 payment_id IS ? ORDER BY paid_time DESC LIMIT ? OFFSET ?"; + } + let response = []; + global.mysql.query(query, [address, payment_id, limit, page * limit]).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.sort(global.support.tsCompare)); + } + }); + }); + }); + }).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) { + getAllWorkerStats(req.params.address, function(err, data){ + return res.json(data); + }); +}); + +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, + validShares: Number(cachedData.goodShares), + invalidShares: Number(cachedData.badShares) + }); +}); + +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) { + getAllWorkerHashCharts(req.params.address, function(err, data){ + return res.json(data); + }); +}); + +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) { + getAddressStats(req.params.address, function(err, data){ + return res.json(data); + }); +}); + +// Authentication +app.post('/authenticate', function (req, res) { + let hmac; + try{ + hmac = crypto.createHmac('sha256', global.config.api.secKey).update(req.body.password).digest('hex'); + } catch (e) { + return res.status(401).send({'success': false, msg: 'Invalid username/password'}); + } + 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.status(401).send({'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, enable_email FROM users WHERE id = ?", [req.decoded.id]).then(function(row){ + return res.json({msg: {payout_threshold: row[0].payout_threshold, email_enabled: row[0].enable_email}}); + }); +}); + +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('/toggleEmail', function (req, res) { + global.mysql.query("UPDATE users SET enable_email = NOT enable_email WHERE id = ?", [req.decoded.id]).then(function () { + return res.json({'msg': 'Email toggled'}); + }); +}); + +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.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 () { + 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.body.port]).then(function (rows) { + if (rows.length !== 0) { + return "Port already exists with that port number."; + } + if (req.body.diff > global.config.pool.maxDifficulty || req.body.diff < global.config.pool.minDifficulty) { + return "Invalid difficulty."; + } + if (["pplns", "solo", "pps"].indexOf(req.body.portType) === -1) { + return "Invalid port type"; + } + global.mysql.query("INSERT INTO port_config (poolPort, difficulty, portDesc, portType, hidden, ssl) VALUES (?, ?, ?, ?, ?, ?)", + [req.body.port, req.body.diff, req.body.desc, req.body.portType, req.body.hidden === 1, req.body.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.body.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.body.diff > global.config.pool.maxDifficulty || req.body.diff < global.config.pool.minDifficulty) { + return "Invalid difficulty."; + } + if (["pplns", "solo", "pps"].indexOf(req.body.portType) === -1) { + return "Invalid port type"; + } + global.mysql.query("UPDATE port_config SET difficulty=?, portDesc=?, portType=?, hidden=?, ssl=? WHERE poolPort = ?", + [req.body.diff, req.body.desc, req.body.portType, req.body.hidden === 1, req.body.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.body.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.body.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.body.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 + +let workerList = []; + +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'); + }); +} diff --git a/lib/blockManager.js b/lib/blockManager.js new file mode 100644 index 0000000..93bc0c3 --- /dev/null +++ b/lib/blockManager.js @@ -0,0 +1,535 @@ +"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 blockScannerTask; +let blockQueue = async.queue(function (task, callback) { + // Todo: Implement within the coins/.js file. + 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; + if (typeof(blockScannerTask) === 'undefined'){ + blockScannerTask = setInterval(blockScanner, 1000); + } +}; + +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) { + if (global.config.pps.enable === true) { + 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; + if (global.config.pps.enable === true) { + 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; + }); + if (global.config.pps.enable === true) { + 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 blockDiff = blockHeader.difficulty; + let windowPPLNS = blockDiff * global.config.pplns.shareMulti; + let blockCheckHeight = blockHeader.height; + let totalPaid = 0; + let totalShares = 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 + }; + + function addPayment(keyAdd, valueAdd) { + if (valueAdd === 0) return; + if (totalPaid >= rewardTotal) return; + totalShares += valueAdd; + paymentData[keyAdd].amount += valueAdd; + let totalPaid2 = totalShares / windowPPLNS * rewardTotal; + if (totalPaid2 + 1 < rewardTotal) { // totalPaid can not overflow rewardTotal now + totalPaid = totalPaid2; + } else { // we need recalculate totalPaid precisely now + totalPaid = 0; + Object.keys(paymentData).forEach(function (key) { + totalPaid += Math.floor(paymentData[key].amount / windowPPLNS * rewardTotal); + }); + console.log("Aproximate totalPaid " + totalPaid2 + " was reset to precise value " + totalPaid); + if (totalPaid >= rewardTotal) { + console.log("Precise value totalPaid " + totalPaid + " reached max " + rewardTotal); + let extra = (totalPaid - rewardTotal) / rewardTotal * windowPPLNS; + console.log("Rewarded " + (valueAdd - extra) + " instead of " + valueAdd + " hashes for " + keyAdd); + paymentData[keyAdd].amount -= extra; + totalPaid = rewardTotal; + } + } + }; + + 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 = shareData.shares; + let feesToPay = amountToPay * (global.config.payout.pplnsFee / 100) + + (shareData.bitcoin === true ? amountToPay * (global.config.payout.btcFee / 100) : 0); + let devDonation = feesToPay * (global.config.payout.devDonation / 100); + let poolDevDonation = feesToPay * (global.config.payout.poolDevDonation / 100); + + addPayment(userIdentifier, amountToPay - feesToPay); + addPayment(global.config.payout.feeAddress, feesToPay - devDonation - poolDevDonation); + addPayment(global.coinFuncs.poolDevAddress, poolDevDonation); + addPayment(global.coinFuncs.coinDevAddress, devDonation); + } + }); + } + cursor.close(); + txn.abort(); + setImmediate(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) { + paymentData[key].amount = Math.floor((paymentData[key].amount / (blockDiff*global.config.pplns.shareMulti)) * rewardTotal); + balanceQueue.push(paymentData[key], function () {}); + //console.log("[PAYMENT] " + key + ": " + global.support.coinToDecimal(paymentData[key].amount)); + 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 + "% (precisely " + totalPayments + " / " + blockHeader.reward + ")"); + }); +}; + +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 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(); + // Todo: Implement within the coins/.js file. + global.support.rpcDaemon('getlastblockheader', [], function (body) { + let blockHeight = body.result.block_header.height; + blockList.forEach(function (row) { + // Todo: Implement within the coins/.js file. + global.support.rpcDaemon('getblockheaderbyheight', {"height": row.height}, function (body) { + if (body.result.block_header.hash !== row.hash) { + global.database.invalidateBlock(row.height); + global.mysql.query("UPDATE block_log SET orphan = true WHERE hex = ?", [row.hash]); + blockIDCache.splice(blockIDCache.indexOf(body.result.block_header.height)); + console.log("Invalidating block " + body.result.block_header.height + " due to being an orphan block"); + } else { + 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. + global.database.unlockBlock(block.hash); + break; + case global.protos.POOLTYPE.PPLNS: + global.coinFuncs.getBlockHeaderByHash(block.hash, function (err, header) { + if (err === null){ + calculatePPLNSPayments(header); + global.database.unlockBlock(block.hash); + } + }); + break; + case global.protos.POOLTYPE.SOLO: + global.coinFuncs.getBlockHeaderByHash(block.hash, function (err, header) { + if (err === null){ + calculateSoloPayments(header); + global.database.unlockBlock(block.hash); + } + }); + break; + default: + console.log("Unknown payment type. FREAKOUT"); + global.database.unlockBlock(block.hash); + break; + } +} + +function blockScanner() { + let inc_check = 0; + if (scanInProgress) { + debug("Skipping scan as there's one in progress."); + return; + } + scanInProgress = true; + global.coinFuncs.getLastBlockHeader(function (err, blockHeader) { + if (err === null){ + 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 - Math.floor(global.config.payout.blocksRequired/2))).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; + blockScannerTask = setInterval(blockScanner, 1000); + } + } else { + console.error(`Upstream error from the block daemon. Resetting scanner due to: ${JSON.stringify(blockHeader)}`); + scanInProgress = false; + blockScannerTask = setInterval(blockScanner, 1000); + } + }); +} + +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. + blockScanner(); + // Scan every 120 seconds for invalidated blocks + setInterval(blockUnlocker, 120000); + 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(); diff --git a/lib/coins/aeon.js b/lib/coins/aeon.js new file mode 100644 index 0000000..d8d6b1d --- /dev/null +++ b/lib/coins/aeon.js @@ -0,0 +1,192 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "WmsSWgtT1JPg5e3cK41hKXSHVpKW7e47bjgiKmWZkYrhSS5LhRemNyqayaSBtAQ6517eo5PtH9wxHVmM78JDZSUu2W8PqRiNs"; // Developer Address + this.poolDevAddress = "WmtusoThH5t7kWACr3nhQGBmEPA1bxKPQD9RVEGyNB6k8E5f9WG1qvteFhvnzW7WA67HBGQfN824LRn2vYgSNq5Q1Jm6nmeV7"; // ArqTras Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 178; + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHeight = function(blockHeight, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockHeight}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + wallet_address: walletAddress + }, function(body){ + return callback(body); + }); + }; + + this.submitBlock = function(blockBlobData, callback){ + global.support.rpcDaemon('submitblock', [blockBlobData], function(body){ + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.status); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBalance = function(callback){ + global.support.rpcWallet('getbalance', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getHeight = function(callback){ + global.support.rpcWallet('getheight', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.height); + } else { + console.error(JSON.stringify(body)); + return callback(true, 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 false; + }; + + this.isIntegratedAddress = function(address) { + return false; + }; + + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight_light; + +} + +module.exports = Coin; diff --git a/lib/coins/etn.js b/lib/coins/etn.js new file mode 100644 index 0000000..43c448d --- /dev/null +++ b/lib/coins/etn.js @@ -0,0 +1,161 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "etnkLgWfr5uE8MZSZpsabb6HjG8Mig9qaS4wQ6Hu2VVKTiJT9Ucdrzz9CqGF9tycaWbntrSRr1CwVJDqGYPtumL72GhXwtoPvs"; // ArqTras Address + this.poolDevAddress = "etnkQMp3Hmsay2p7uxokuHRKANrMDNASwQjDUgFb5L2sDM3jqUkYQPKBkooQFHVWBzEaZVzfzrXoETX6RbMEvg4R4csxfRHLo1"; // MoneroOcean PoolDev Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 18018; + this.intPrefix = 18019; + + if (global.config.general.testnet === true){ + this.prefix = 53; + this.intPrefix = 54; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/grf.js b/lib/coins/grf.js new file mode 100644 index 0000000..ce63835 --- /dev/null +++ b/lib/coins/grf.js @@ -0,0 +1,161 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "GAQsx9YDqkN5WMiGgZ8AqS9S5Wg7HwNFLdPDqjmvwxs8ekKy6L6ph69TFU5uxF1MzySWBwxKPKtDxPLJ7fD6j1Xu3MWhYyS"; // ArqTras + this.poolDevAddress = "GAQsx9YDqkN5WMiGgZ8AqS9S5Wg7HwNFLdPDqjmvwxs8ekKy6L6ph69TFU5uxF1MzySWBwxKPKtDxPLJ7fD6j1Xu3MWhYyS"; // ArqTras + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 90; + this.intPrefix = 91; + + if (global.config.general.testnet === true){ + this.prefix = 84; + this.intPrefix = 85; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 405000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/ipbc.js b/lib/coins/ipbc.js new file mode 100644 index 0000000..d0613bc --- /dev/null +++ b/lib/coins/ipbc.js @@ -0,0 +1,157 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('forknote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + + this.coinDevAddress = "bxcxrfBRTimUsEcDLmAfd4JRdgrw1eRTfTnzpqEk3ANs4zgiwL3m9gyQXpjYzLXPXdH9KQjM9c9JGgBG3q12vii71ndDRpzXc"; // Developer Address + this.poolDevAddress = "bxcxrfBRTimUsEcDLmAfd4JRdgrw1eRTfTnzpqEk3ANs4zgiwL3m9gyQXpjYzLXPXdH9KQjM9c9JGgBG3q12vii71ndDRpzXc"; // ArqTras PoolDev Address + + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 209; + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/itns.js b/lib/coins/itns.js new file mode 100644 index 0000000..21a559e --- /dev/null +++ b/lib/coins/itns.js @@ -0,0 +1,200 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('intensecoin-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "iz5w5LGYQY2SseEd9BTaF8SRqFmZLTEVDBEGidvzYnZBcc9RMEHXs2rXBZfAvXQPPc85NR2JeZcQUj7jjBcgw26b1Rk6m4H2z"; // Developer Address + this.poolDevAddress = "iz5imhe9C7vWnjZtZBFtT8MwNxVuJuryUUHXSAtnWUo93CJzNdZBizHQExPRCHUBi36tk2BcigPAFRDA4cnddGXF1R6j69n3w"; // Venthos Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress + ]; + + this.exchangeAddresses = [ + "iz4pcDLxmo7KqbFmYjE5aGDv68U9Sgm1ePFjWUY24vzyPeGMcoG894MAFjrtHbaMv1TygTcvJWzGN3zNR6PeEYuc1w8V2tiMW" // stocks.exchange + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 251; + this.intPrefix = 129; + + if (global.config.general.testnet === true) { + this.prefix = 25247; + this.intPrefix = 3745; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHeight = function(blockHeight, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockHeight}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + wallet_address: walletAddress + }, function(body){ + return callback(body); + }); + }; + + this.submitBlock = function(blockBlobData, callback){ + global.support.rpcDaemon('submitblock', [blockBlobData], function(body){ + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.status); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBalance = function(callback){ + global.support.rpcWallet('getbalance', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getHeight = function(callback){ + global.support.rpcWallet('getheight', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.height); + } else { + console.error(JSON.stringify(body)); + return callback(true, 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.isIntegratedAddress = function(address) { + address = new Buffer(address); + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/krb.js b/lib/coins/krb.js new file mode 100644 index 0000000..ae37b82 --- /dev/null +++ b/lib/coins/krb.js @@ -0,0 +1,156 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + console.log("Generated instanceId: " + instanceId.toString('hex')); + this.coinDevAddress = "Kdev1L9V5ow3cdKNqDpLcFFxZCqu5W2GE9xMKewsB2pUXWxcXvJaUWHcSrHuZw91eYfQFzRtGfTemReSSMN4kE445i6Etb3"; // Developer Address + this.poolDevAddress = "KgseWakG2bMXHGJSsAUfzL1HykCyvD4m8gd9qgcuyZ1ufy8PqUCKRxEfAv3nahfdTrCjZByiWoCiRiohxq4u2rf2RgQ1pcJ"; // Snipa Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 111; + + this.supportsAutoExchange = false; + + this.niceHashDiff = 200000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 16 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 4); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/msr.js b/lib/coins/msr.js new file mode 100644 index 0000000..39f9507 --- /dev/null +++ b/lib/coins/msr.js @@ -0,0 +1,161 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "cashBsVQ5Cv9uUHfaMDbgSQ8yryBcAuGCMRjHTuc8e5dYBMcA4hoonFLqLFGZGtaMhX7q6mLJDRiW6zQxYDxdwgD7z6gWqKy1W"; // Developer Address + this.poolDevAddress = "cashPUyMJXpgUaoRA5nSKaMdtvQ1kXwMAXfQbCQiJRUwM54BYLkHKeagCugY7C9CPRfDChH5vHyVk3cfsy2bpm9q4kDyrLD5UR"; // Snipa Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 28; + this.intPrefix = 29; + + if (global.config.general.testnet === true){ + this.prefix = 33; + this.intPrefix = 34; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 200000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/omb.js b/lib/coins/omb.js new file mode 100644 index 0000000..bd3a944 --- /dev/null +++ b/lib/coins/omb.js @@ -0,0 +1,161 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "cashBsVQ5Cv9uUHfaMDbgSQ8yryBcAuGCMRjHTuc8e5dYBMcA4hoonFLqLFGZGtaMhX7q6mLJDRiW6zQxYDxdwgD7z6gWqKy1W"; // Developer Address + this.poolDevAddress = "cashPUyMJXpgUaoRA5nSKaMdtvQ1kXwMAXfQbCQiJRUwM54BYLkHKeagCugY7C9CPRfDChH5vHyVk3cfsy2bpm9q4kDyrLD5UR"; // Snipa Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 0xe1f54; + this.intPrefix = 0xe9f54; + + if (global.config.general.testnet === true){ + this.prefix = 0xe1f54; + this.intPrefix = 0xe9f54; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 200000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/stl.js b/lib/coins/stl.js new file mode 100644 index 0000000..c99d276 --- /dev/null +++ b/lib/coins/stl.js @@ -0,0 +1,162 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "Se2qr4ovTU2SiAbE1KyMLR4kQYKFWhB3V8mxijFfwdYTJVnw4ZdsAHw294cHoypTYyc1sbUv2hTqr7q8GVBmSB9Q1a2L26BTn"; // Arq Address + this.poolDevAddress = "Se2qr4ovTU2SiAbE1KyMLR4kQYKFWhB3V8mxijFfwdYTJVnw4ZdsAHw294cHoypTYyc1sbUv2hTqr7q8GVBmSB9Q1a2L26BTn"; // Arq Address + + this.blockedAddresses = [ + + ]; + + this.exchangeAddresses = [ + + ]; // These are addresses that MUST have a paymentID to perform logins with. + + //this.prefix = cnUtil.address_decode(new Buffer(config.poolServer.poolAddress)) + this.prefix = 9241; + this.intPrefix = 28822; //cnUtil.address_decode_integrated(new Buffer(config.poolServer.poolAddress)); + + if (global.config.general.testnet === true){ + this.prefix = 53; + this.intPrefix = 54; + } + + this.supportsAutoExchange = true; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/sumo.js b/lib/coins/sumo.js new file mode 100644 index 0000000..4416240 --- /dev/null +++ b/lib/coins/sumo.js @@ -0,0 +1,162 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "Sumoo3sC2PkGS85rzrjKJa1XeLBoX9oqiWEhb7Sm7BzCb69xfVqCh7adNQjbTqaUux5zXaZV39gdSDhtoCHMwUNrSP47xZFiUQR"; // ArqTras + this.poolDevAddress = "Sumoo1DGS7c9LEKZNipsiDEqRzaUB3ws7YHfUiiZpx9SQDhdYGEEbZjRET26ewuYEWAZ8uKrz6vpUZkEVY7mDCZyGnQhkLpxKmy"; // MoneroOcean + + this.blockedAddresses = [ + + ]; + + this.exchangeAddresses = [ + + ]; // These are addresses that MUST have a paymentID to perform logins with. + + //this.prefix = cnUtil.address_decode(new Buffer(config.poolServer.poolAddress)) + this.prefix = 2864026; + this.intPrefix = 2864027; //cnUtil.address_decode_integrated(new Buffer(config.poolServer.poolAddress)); + + if (global.config.general.testnet === true){ + this.prefix = 53; + this.intPrefix = 54; + } + + this.supportsAutoExchange = true; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/trtl.js b/lib/coins/trtl.js new file mode 100644 index 0000000..529e708 --- /dev/null +++ b/lib/coins/trtl.js @@ -0,0 +1,155 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('forknote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = ""; // Developer Address + this.poolDevAddress = ""; // Snipa Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + ]; + + this.exchangeAddresses = [ + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 3914525; + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/xao.js b/lib/coins/xao.js new file mode 100644 index 0000000..9907423 --- /dev/null +++ b/lib/coins/xao.js @@ -0,0 +1,200 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "AANbbVywNWAX2wnJbjiZuHSBL26GZBzQFBQLzg8PaaJVjex71hNF8VUMPvVLAA7TAXfopfHLBRbHGMXHkNUmfwC4RpHWrSG"; // Alex's XAO address + this.poolDevAddress = "AANbbVywNWAX2wnJbjiZuHSBL26GZBzQFBQLzg8PaaJVjex71hNF8VUMPvVLAA7TAXfopfHLBRbHGMXHkNUmfwC4RpHWrSG"; // Alex's XAO address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress + ]; + + this.exchangeAddresses = [ + "" // stocks.exchange + ]; // These are addresses that MUST have a paymentID to perform logins with. + + this.prefix = 54; + this.intPrefix = 55; + + if (global.config.general.testnet === true) { + this.prefix = 25247; + this.intPrefix = 3745; + } + + this.supportsAutoExchange = false; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHeight = function(blockHeight, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockHeight}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + wallet_address: walletAddress + }, function(body){ + return callback(body); + }); + }; + + this.submitBlock = function(blockBlobData, callback){ + global.support.rpcDaemon('submitblock', [blockBlobData], function(body){ + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.status); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBalance = function(callback){ + global.support.rpcWallet('getbalance', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getHeight = function(callback){ + global.support.rpcWallet('get_height', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.height); + } else { + console.error(JSON.stringify(body)); + return callback(true, 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.isIntegratedAddress = function(address) { + address = new Buffer(address); + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/xmr.js b/lib/coins/xmr.js new file mode 100644 index 0000000..0771444 --- /dev/null +++ b/lib/coins/xmr.js @@ -0,0 +1,169 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + this.data = data; + let instanceId = crypto.randomBytes(4); + this.coinDevAddress = "4AYuDc4cEqxfxVTUFwVqPd4JdmKLjm9dNhTjuT6Ud5gQa564wp6cxMBWbwaVe4vUMveKAzAiA4j8xgUi29TpKXpm3zc3jmn"; // ArqTras Address + this.poolDevAddress = "499fS1Phq64hGeqV8p2AfXbf6Ax7gP6FybcMJq6Wbvg8Hw6xms8tCmdYpPsTLSaTNuLEtW4kF2DDiWCFcw4u7wSvFD8wFWE"; // MoneroOcean Address + + this.blockedAddresses = [ + this.coinDevAddress, + this.poolDevAddress, + "43SLUTpyTgXCNXsL43uD8FWZ5wLAdX7Ak67BgGp7dxnGhLmrffDTXoeGm2GBRm8JjigN9PTg2gnShQn5gkgE1JGWJr4gsEU", // Wolf0's address + "42QWoLF7pdwMcTXDviJvNkWEHJ4TXnMBh2Cx6HNkVAW57E48Zfw6wLwDUYFDYJAqY7PLJUTz9cHWB5C4wUA7UJPu5wPf4sZ", // Wolf0's address + "46gq64YYgCk88LxAadXbKLeQtCJtsLSD63NiEc3XHLz8NyPAyobACP161JbgyH2SgTau3aPUsFAYyK2RX4dHQoaN1ats6iT", // Claymore's Fee Address. + "47mr7jYTroxQMwdKoPQuJoc9Vs9S9qCUAL6Ek4qyNFWJdqgBZRn4RYY2QjQfqEMJZVWPscupSgaqmUn1dpdUTC4fQsu3yjN" // Claymore's _other_ 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.supportsAutoExchange = true; + + this.niceHashDiff = 200000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/coins/xmv.js b/lib/coins/xmv.js new file mode 100644 index 0000000..a452111 --- /dev/null +++ b/lib/coins/xmv.js @@ -0,0 +1,169 @@ +"use strict"; +const bignum = require('bignum'); +const cnUtil = require('cryptonote-util'); +const multiHashing = require('multi-hashing'); +const crypto = require('crypto'); +const debug = require('debug')('coinFuncs'); + +let hexChars = new RegExp("[0-9a-f]+"); + +function Coin(data){ + this.bestExchange = global.config.payout.bestExchange; + 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. + "47mr7jYTroxQMwdKoPQuJoc9Vs9S9qCUAL6Ek4qyNFWJdqgBZRn4RYY2QjQfqEMJZVWPscupSgaqmUn1dpdUTC4fQsu3yjN" // Claymore's _other_ 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.supportsAutoExchange = true; + + this.niceHashDiff = 400000; + + this.getBlockHeaderByID = function(blockId, callback){ + global.support.rpcDaemon('getblockheaderbyheight', {"height": blockId}, function (body) { + if (body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockHeaderByHash = function(blockHash, callback){ + global.support.rpcDaemon('getblockheaderbyhash', {"hash": blockHash}, function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getLastBlockHeader = function(callback){ + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof(body) !== 'undefined' && body.hasOwnProperty('result')){ + return callback(null, body.result.block_header); + } else { + console.error(JSON.stringify(body)); + return callback(true, body); + } + }); + }; + + this.getBlockTemplate = function(walletAddress, callback){ + global.support.rpcDaemon('getblocktemplate', { + reserve_size: 17, + 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) { + /* + Generating a block template is a simple thing. Ask for a boatload of information, and go from there. + Important things to consider. + The reserved space is 13 bytes long now in the following format: + Assuming that the extraNonce starts at byte 130: + |130-133|134-137|138-141|142-145| + |minerNonce/extraNonce - 4 bytes|instanceId - 4 bytes|clientPoolNonce - 4 bytes|clientNonce - 4 bytes| + This is designed to allow a single block template to be used on up to 4 billion poolSlaves (clientPoolNonce) + Each with 4 billion clients. (clientNonce) + While being unique to this particular pool thread (instanceId) + With up to 4 billion clients (minerNonce/extraNonce) + Overkill? Sure. But that's what we do here. Overkill. + */ + + // Set this.blob equal to the BT blob that we get from upstream. + this.blob = template.blocktemplate_blob; + this.idHash = crypto.createHash('md5').update(template.blocktemplate_blob).digest('hex'); + // Set this.diff equal to the known diff for this block. + this.difficulty = template.difficulty; + // Set this.height equal to the known height for this block. + this.height = template.height; + // Set this.reserveOffset to the byte location of the reserved offset. + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = new Buffer(this.blob, 'hex'); + // Copy the Instance ID to the reserve offset + 4 bytes deeper. Copy in 4 bytes. + instanceId.copy(this.buffer, this.reserveOffset + 4, 0, 3); + // Generate a clean, shiny new buffer. + this.previous_hash = new Buffer(32); + // Copy in bytes 7 through 39 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 7, 39); + // Reset the Nonce. - This is the per-miner/pool nonce + this.extraNonce = 0; + // The clientNonceLocation is the location at which the client pools should set the nonces for each of their clients. + this.clientNonceLocation = this.reserveOffset + 12; + // The clientPoolLocation is for multi-thread/multi-server pools to handle the nonce for each of their tiers. + this.clientPoolLocation = this.reserveOffset + 8; + this.nextBlob = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Convert the blob into something hashable. + return global.coinFuncs.convertBlob(this.buffer).toString('hex'); + }; + // Make it so you can get the raw block blob out. + this.nextBlobWithChildNonce = function () { + // Write a 32 bit integer, big-endian style to the 0 byte of the reserve offset. + this.buffer.writeUInt32BE(++this.extraNonce, this.reserveOffset); + // Don't convert the blob to something hashable. You bad. + return this.buffer.toString('hex'); + }; + }; + + this.cryptoNight = multiHashing.cryptonight; + +} + +module.exports = Coin; diff --git a/lib/data.proto b/lib/data.proto new file mode 100644 index 0000000..0db1421 --- /dev/null +++ b/lib/data.proto @@ -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; +} \ No newline at end of file diff --git a/lib/local_comms.js b/lib/local_comms.js new file mode 100644 index 0000000..e507dae --- /dev/null +++ b/lib/local_comms.js @@ -0,0 +1,722 @@ +"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: false, + mapAsync: false, + useWritemap: false, + 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) || intDict.value === false){ + cached[intDict.location] = 0; + } else { + cached[intDict.location] += intDict.value; + } + }); + txn.putString(this.cacheDB, key, JSON.stringify(cached)); + txn.commit(); + } else { + 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_stats2', [{location: 'totalHashes', value: share.shares}, {location: 'roundHashes', value: share.shares}]); + switch (share.poolType) { + case global.protos.POOLTYPE.PPLNS: + this.incrementCacheData('pplns_stats2', [{location: 'totalHashes', value: share.shares}, {location: 'roundHashes', value: share.shares}]); + break; + case global.protos.POOLTYPE.PPS: + this.incrementCacheData('pps_stats2', [{location: 'totalHashes', value: share.shares}, {location: 'roundHashes', value: share.shares}]); + break; + case global.protos.POOLTYPE.SOLO: + this.incrementCacheData('solo_stats2', [{location: 'totalHashes', value: share.shares}, {location: 'roundHashes', 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.storeBulkShares = function(shareObject) { + let cachedData = { + global_stats2: {totalHashes: 0, roundHashes: 0}, + pplns_stats2: {totalHashes: 0, roundHashes: 0}, + pps_stats2: {totalHashes: 0, roundHashes: 0}, + solo_stats2: {totalHashes: 0, roundHashes: 0} + }; + let shares = {}; // Shares keyed by blockID + let shareCount = 0; + debug(shareObject.length + ' shares to process'); + shareObject.forEach(function(share){ + //Data is the share object at this point. + shareCount += 1; + if (typeof(share.shares) === "number") { + if (!shares.hasOwnProperty(share.blockHeight)) { + shares[share.blockHeight] = []; + } + shares[share.blockHeight].push(share); + let minerID = share.paymentAddress; + if (typeof(share.paymentID) !== 'undefined' && share.paymentID.length > 10) { + minerID = minerID + '.' + share.paymentID; + } + let minerIDWithIdentifier = minerID + "_" + share.identifier; + cachedData.global_stats2.totalHashes += share.shares; + cachedData.global_stats2.roundHashes += share.shares; + let stats_type = 'pplns_stats2'; + switch (share.poolType) { + case global.protos.POOLTYPE.PPLNS: + stats_type = 'pplns_stats2'; + break; + case global.protos.POOLTYPE.PPS: + stats_type = 'pps_stats2'; + break; + case global.protos.POOLTYPE.SOLO: + stats_type = 'solo_stats2'; + break; + } + cachedData[stats_type].totalHashes += share.shares; + cachedData[stats_type].roundHashes += share.shares; + if (!cachedData.hasOwnProperty(minerID)) { + cachedData[minerID] = {totalHashes: 0, goodShares: 0}; + } + if (!cachedData.hasOwnProperty(minerIDWithIdentifier)) { + cachedData[minerIDWithIdentifier] = {totalHashes: 0, goodShares: 0}; + } + cachedData[minerIDWithIdentifier].totalHashes += share.shares; + cachedData[minerID].totalHashes += share.shares; + cachedData[minerIDWithIdentifier].goodShares += 1; + cachedData[minerID].goodShares += 1; + } else { + console.error("Error in share parser: " + JSON.stringify(share)); + } + if(shareObject.length === shareCount){ + // Perform insert and return + let inCache = 'notNull'; + while (inCache !== null){ + let txn_ro = global.database.env.beginTxn({readOnly: true}); + inCache = txn_ro.getString(global.database.cacheDB, 'cacheUpdate'); + txn_ro.abort(); + } + + let txn = global.database.env.beginTxn(); + for (let key in cachedData){ + if (cachedData.hasOwnProperty(key) && cachedData[key].totalHashes !== 0){ + let cacheStore = txn.getString(global.database.cacheDB, key); + if (cacheStore === null){ + txn.putString(global.database.cacheDB, key, JSON.stringify(cachedData[key])); + } else { + let json_cache = JSON.parse(cacheStore); + if (json_cache.hasOwnProperty('totalHashes')) { // cachedData.totalHashes is always there for global and miners stats + json_cache.totalHashes += cachedData[key].totalHashes; + } else { + json_cache.totalHashes = cachedData[key].totalHashes; + } + if (cachedData[key].hasOwnProperty('goodShares')) { + if (json_cache.hasOwnProperty('goodShares')) { + json_cache.goodShares += cachedData[key].goodShares; + } else { + json_cache.goodShares = cachedData[key].goodShares; + } + } else if (cachedData[key].hasOwnProperty('roundHashes')) { + if (json_cache.hasOwnProperty('roundHashes')) { + json_cache.roundHashes += cachedData[key].roundHashes; + } else { + json_cache.roundHashes = cachedData[key].roundHashes; + } + } + txn.putString(global.database.cacheDB, key, JSON.stringify(json_cache)); + } + } + } + let blocksSeen = 0; + for (let key in shares){ + if (shares.hasOwnProperty(key)){ + blocksSeen += 1; + let sharesSeen = 0; + shares[key].forEach(function(final_share){ // jshint ignore:line + sharesSeen += 1; + try { + txn.putBinary(global.database.shareDB, parseInt(key), global.protos.Share.encode(final_share)); + } catch (e) { + debug(final_share); + } + if (Object.keys(shares).length === blocksSeen && sharesSeen === shares[key].length){ + debug('Made it to where I can do the insert'); + txn.commit(); + return true; + } + }); + } + } + } + }); + return false; + }; + + 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.getBlockByID = function(blockID){ + this.refreshEnv(); + debug("Getting the data for blockID: " + blockID); + let txn = this.env.beginTxn({readOnly: true}); + let data = txn.getBinary(this.blockDB, blockID); + if (data === null){ + debug("Unable to get block at height: "+ blockID); + return false; + } + let blockData = global.protos.Block.decode(data); + txn.commit(); + debug("Done getting the last block for: "+ blockData.poolType + " height of: "+ blockID); + return blockData; + }; + + 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); + }; + + // hash -> time + let orphanBlocks = {}; + + this.storeBlock = function(blockId, blockData, callback){ + this.refreshEnv(); + try{ + let blockDataDecoded = global.protos.Block.decode(blockData); + global.coinFuncs.getBlockHeaderByHash(blockDataDecoded.hash, function(err, header){ + // after hour of submit attempts finally cosider this block as orphan + if (err && header && header.error && typeof(header.error.message) === 'string' && header.error.message.indexOf("can't get block by hash") > -1) { + let time_now = Date.now(); + if (blockDataDecoded.hash in orphanBlocks) { + if (time_now - orphanBlocks[blockDataDecoded.hash] > 5*60*1000) { + console.log("Stopped attempts to get block reward for " + blockDataDecoded.hash); + err = false; + header = {}; + header.reward = 0; + } + } else { + console.log("Started attempts to store possibly orphan block " + blockDataDecoded.hash); + orphanBlocks[blockDataDecoded.hash] = time_now; + } + } + if (err || typeof(header) === 'undefined' || !header){ + setTimeout(function () { return callback(false) }, 1000); + return; + } + 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(); + global.database.incrementCacheData('global_stats2', [{location: 'roundHashes', value: false}]); + switch (blockDataDecoded.poolType) { + case global.protos.POOLTYPE.PPLNS: + global.database.incrementCacheData('pplns_stats2', [{location: 'roundHashes', value: false}]); + break; + case global.protos.POOLTYPE.PPS: + global.database.incrementCacheData('pps_stats2', [{location: 'roundHashes', value: false}]); + break; + case global.protos.POOLTYPE.SOLO: + global.database.incrementCacheData('solo_stats2', [{location: 'roundHashes', value: false}]); + break; + } + 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(err, header){ + if (err === null) { + 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; + blockData.unlocked = true; + 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.lockBlock = function(blockId){ + let txn = this.env.beginTxn(); + let blockProto = txn.getBinary(this.blockDB, blockId); + if (blockProto !== null){ + let blockData = global.protos.Block.decode(blockProto); + blockData.unlocked = false; + txn.putBinary(this.blockDB, blockId, global.protos.Block.encode(blockData)); + } + 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.bulkSetCache = function(cacheUpdates){ + let txn = this.env.beginTxn(); + txn.putString(this.cacheDB, 'cacheUpdate', 'cacheUpdate'); + txn.commit(); + txn = this.env.beginTxn(); + for (let key in cacheUpdates){ + if (cacheUpdates.hasOwnProperty(key)){ + txn.putString(this.cacheDB, key, JSON.stringify(cacheUpdates[key])); + } + } + txn.del(this.cacheDB, 'cacheUpdate'); + txn.commit(); + }; + + this.getOldestLockedBlock = function(){ + /* + 6-29-2017 - Snipa - + This function returns a decompressed block proto for the first locked block in the system as part of the + share depth functions. DO NOT BLINDLY REPLACE getLastBlock WITH THIS FUNCTION. + */ + this.refreshEnv(); + debug("Getting the oldest locked block in the system"); + let txn = this.env.beginTxn({readOnly: true}); + let cursor = new this.lmdb.Cursor(txn, this.blockDB); + let highestBlockKey = null; + let highestBlock = null; + 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.unlocked === false){ + if (highestBlock === null || highestBlockKey >= key) { + if (highestBlock !== null) { + console.error("Found unordered blocks with " + highestBlockKey.toString() + " and " + key.toString() + " heights"); + } + highestBlock = blockData; + highestBlockKey = key; + } + } + }); + } + cursor.close(); + txn.commit(); + if (highestBlock !== null) { + debug("Got the oldest locked block in the system at height: " + JSON.stringify(highestBlock)); + } else { + debug("There are no unlocked blocks in the system. Woohoo!"); + } + return highestBlock; + }; + + 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 * shareMulti * 2 + 6/29/2017 - Fixed bug with the way the system got blocks. getLastBlock gets the most recent block. + getOldestLockedBlock gets the oldest block in the system that's locked. This ensures that shares for that block + can't be destroyed, and that there's enough depth past that point to ensure the system will have the ability + to make payouts based on the shareMultiLog. Thanks suhz for finding this. Sorry it hit your aeon pool hard. + :( -- Snipa + If current_height - global.payout.blocksRequired > lastLockedBlock, then set the scan start to + current_height - global.payout.blocksRequired - 1 so that we have the full block in case of PPS. + Otherwise, use the lastPPLNSBlock as the scan start. There we go. Stupid logic! + Math check! + cur_height = 100, blocksRequired=20, lastPPLNSLockedBlock.height=90 + In this case, the functional depth required for SOLO is 80 - 1, giving us 79 as our start + cur_height = 100, blocksRequired=20, lastPPLNSLockedBlock.height=70 + In this case, the PPLNS locked block is older than the current height - the required amount, so start is 70. + PPS height no longer matters! Yay! + Solo really doesn't matter, as block finder gets everything. + If there is no valid locked block to start from, aka all blocks are unlocked, then scan from the current height + of the chain, as there's no way for the system to have older blocks. We only need to save extra in the case + where there's unlocked blocks. A find on the current block will have enough depth as long as the saves are + correct. This will cause the system to clean up shares massively when there are no unlocked blocks. + */ + let oldestLockedBlock = this.getOldestLockedBlock(); + async.waterfall([ + function(callback){ + if (oldestLockedBlock === null) { + callback(null, oldestLockedBlock) + } else { + global.coinFuncs.getBlockHeaderByHash(oldestLockedBlock.hash, (err, result) => { + oldestLockedBlock.height = result.height; + console.log("Got the oldest block"); + callback(null, oldestLockedBlock); + }); + } + }, + function(oldestLockedBlock, callback){ + global.coinFuncs.getLastBlockHeader(function(err, body){ + if (err !== null) { + console.error("Last block header request failed!"); + return; + } + if (oldestLockedBlock === null){ + /* + If there's no locked blocks, then allow the system to scan from the PPS depth downwards if PPS + is enabled. + Save enough shares so that the diff * share multi * 30% for buffer. + */ + if (global.config.pps.enable){ + // If PPS is enabled, we scan for new blocks at cur height - blocksRequired/2. + // We need to save shares back that far at the least. + callback(null, body.height - Math.floor(global.config.payout.blocksRequired/2), Math.floor(body.difficulty * global.config.pplns.shareMulti * 5)); + } else { + // Otherwise, we can just start from the current height. Woo! + callback(null, body.height, Math.floor(body.difficulty * global.config.pplns.shareMulti * 5)); + } + + } else { + /* + Otherwise, start the scan from the oldest locked block downwards. + This protects against the blockManager being messed up and not unlocking blocks. + This will ensure that enough shares are in place to unlock all blocks. + If the block is Solo, PPLNS or PPS, it doesn't matter. + */ + if (global.config.pps.enable && oldestLockedBlock.height > body.height - Math.floor(global.config.payout.blocksRequired/2)) { + // If PPS is enabled, and the oldestLockedBlock.height > the PPS minimum, start from the PPS minimum. + callback(null, body.height - Math.floor(global.config.payout.blocksRequired/2), Math.floor(body.difficulty * global.config.pplns.shareMulti * 5)); + } else { + // If PPS isn't enabled, or the oldestLockedBlock.height < the PPS minimum, then start from there. + callback(null, oldestLockedBlock.height, Math.floor(oldestLockedBlock.difficulty * global.config.pplns.shareMulti * 5)); + } + } + }); + }, + function (lastBlock, difficulty, callback) { + let shareCount = 0; + let pplnsFound = false; + let blockList = []; + console.log("Scanning from: "+lastBlock + " for more than: " + difficulty + " shares"); + range.range(lastBlock-1, 0, -1).forEach(function (blockID) { + debug("Scanning block: " + blockID); + let txn = global.database.env.beginTxn({readOnly: true}); + let cursor = new global.database.lmdb.Cursor(txn, global.database.shareDB); + let pplnsCurrFound = false; + for (let found = (cursor.goToRange(parseInt(blockID)) === blockID); found; found = cursor.goToNextDup()) { + if (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 + 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){ + pplnsCurrFound = true; + } + } + } + cursor.close(); + txn.abort(); + if (pplnsCurrFound) pplnsFound = true; + }); + callback(null, blockList); + } + ], function(err, data){ + if (global.config.general.blockCleaner === true){ + if(data.length > 0){ + global.database.refreshEnv(); + let totalDeleted = 0; + data.forEach(function(block){ + 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 have removed: " + JSON.stringify(data)); + } + }); + }; + + this.refreshEnv = function(){}; + + setInterval(function(){ + global.database.dirtyenv = true; + }, 900000); // Set DB env reload for every 15 minutes. +} + +module.exports = Database; diff --git a/lib/longRunner.js b/lib/longRunner.js new file mode 100644 index 0000000..4013efb --- /dev/null +++ b/lib/longRunner.js @@ -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); \ No newline at end of file diff --git a/lib/payment_systems/aeon.js b/lib/payment_systems/aeon.js new file mode 100644 index 0000000..3b4dc3d --- /dev/null +++ b/lib/payment_systems/aeon.js @@ -0,0 +1,298 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + + // Protect our balance by making sure we have sufficient unlocked funds before trying to transact + global.support.rpcWallet('getbalance', [], function (body) { + if (body.result) { + var amountToPay = paymentDetails.destinations.reduce(function (a, b) { return a + b; }, 0); + if (body.result.unlocked_balance < amountToPay) { + console.error("Wallet only has " + body.result.unlocked_balance + " unlocked balance, can't pay " + amountToPay + " worth of AEON. Retrying in " + timerRetry + " minutes!"); + if (!extraPaymentRound) { + setTimeout(function () { + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } + } else { + console.error("Issue checking pool wallet balance before 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 check wallet balance", + "Hello,\r\nThe payment daemon has hit an issue checking the pool's wallet balance: " + JSON.stringify(body.error) + + ". Please investigate and restart the payment daemon as appropriate"); + return; + } + }); + + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + startNormalPaymentTimer(); + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.fee, 1]).then(function (result) { + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " AEON to 1 miner"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + this.makePaymentAsIntegrated = function () { + let paymentDetails = { + destinations: [ + { + amount: this.amount - this.fee, + address: this.address + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.fee, 1]).then(function (result) { + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " AEON to 1 miner"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + 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 && global.coinFuncs.isIntegratedAddress(payee.address) === false) { + 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.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(); + } + } + 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, + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.fee, paymentDetails.destinations.length]).then(function (result) { + paymentDetails.destinations.forEach(function (payeeItem) { + payee = payeeObjects[payeeItem.address]; + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(totalAmount) + " AEON to " + paymentDetails.destinations.length + " miners"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + } + } + }); + }); + }); +} + +function startNormalPaymentTimer() { + if (global.config.payout.timer > 35791) { + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes for its next normal run."); + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } +} + +function init() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("(Payout Timer Configuration) Normal Timer: " + global.config.payout.timer + " minutes, Out of Money Retry Timer: " + global.config.payout.timerRetry + " minutes"); + startNormalPaymentTimer(); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/etn.js b/lib/payment_systems/etn.js new file mode 100644 index 0000000..e3f19f5 --- /dev/null +++ b/lib/payment_systems/etn.js @@ -0,0 +1,696 @@ +"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 extraPaymentRound = false; +let paymentTimer = null; + +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 + } + ], + priority: global.config.payout.priority, + 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 > 0.0000001) { + 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(true); + } 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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 + } + ], + priority: global.config.payout.priority, + 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 > 0.0000001) { + 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 + */ + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 > 0.0000001) { + 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 + } + ], + priority: global.config.payout.priority, + 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 > 0.0000001) { + 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, fee: this.fee}; + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (body.fee && body.fee > 0.0000001) { + 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() { + global.support.rpcWallet("store", [], function () { + }); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/grf.js b/lib/payment_systems/grf.js new file mode 100644 index 0000000..101ab39 --- /dev/null +++ b/lib/payment_systems/grf.js @@ -0,0 +1,254 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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) + " GRF with a " + global.support.coinToDecimal(body.result.fee) + " GRF 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) + " GRF"); + } + let intCount = 0; + paymentDetails.destinations.forEach(function (details) { + console.log("Payment made to: " + details.address + " For: " + global.support.coinToDecimal(details.amount) + " GRF"); + intCount += 1; + if (intCount === paymentDetails.destinations.length) { + return callback(body.result); + } + }); + } + }); +}, 1); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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.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.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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/ipbc.js b/lib/payment_systems/ipbc.js new file mode 100644 index 0000000..2ebc6e7 --- /dev/null +++ b/lib/payment_systems/ipbc.js @@ -0,0 +1,257 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.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 + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.fee, 1]).then(function (result) { + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + 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.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(); + } + } + 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, + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/itns.js b/lib/payment_systems/itns.js new file mode 100644 index 0000000..6fbe212 --- /dev/null +++ b/lib/payment_systems/itns.js @@ -0,0 +1,302 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null) { + clearInterval(paymentTimer); + paymentTimer = null; + } + + // Protect our balance by making sure we have sufficient unlocked funds before trying to transact + global.support.rpcWallet('getbalance', [], function (body) { + if (body.result) { + var amountToPay = paymentDetails.destinations.reduce(function (a, b) { return a + b; }, 0); + if (body.result.unlocked_balance < amountToPay) { + console.error("Wallet only has " + body.result.unlocked_balance + " unlocked balance, can't pay " + amountToPay + " worth of ITNS. Retrying in " + timerRetry + " minutes!"); + if (!extraPaymentRound) { + setTimeout(function () { + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } + } else { + console.error("Issue checking pool wallet balance before 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 check wallet balance", + "Hello,\r\nThe payment daemon has hit an issue checking the pool's wallet balance: " + JSON.stringify(body.error) + + ". Please investigate and restart the payment daemon as appropriate"); + return; + } + }); + + 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')) { + if (body.error.message === "not enough money") { + // In theory the earlier check should prevent this from occurring. + console.error("Issue making payments, not enough money, will try later"); + if (!extraPaymentRound) { + setTimeout(function () { + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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) + " ITNS with a " + global.support.coinToDecimal(body.result.fee) + " ITNS 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) + " ITNS"); + } + let intCount = 0; + paymentDetails.destinations.forEach(function (details) { + console.log("Payment made to: " + details.address + " For: " + global.support.coinToDecimal(details.amount) + " ITNS"); + intCount += 1; + if (intCount === paymentDetails.destinations.length) { + return callback(body.result); + } + }); + } + }); +}, 1); + +paymentQueue.drain = function () { + extraPaymentRound = false; + startNormalPaymentTimer(); + global.database.setCache('lastPaymentCycle', Math.floor(Date.now() / 1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " ITNS to 1 miner"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + this.makePaymentAsIntegrated = function () { + let paymentDetails = { + destinations: [ + { + amount: this.amount - this.fee, + address: this.address + } + ], + priority: global.config.payout.priority, + 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(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " ITNS to 1 miner"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + 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 && global.coinFuncs.isIntegratedAddress(payee.address) === false) { + 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 && global.coinFuncs.isIntegratedAddress(payee.address) === true && (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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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(); + }); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(totalAmount) + " ITNS to " + paymentDetails.destinations.length + " miners"); + } + } else { + console.error("Unknown error from the wallet."); + } + }); + } + } + }); + }); + }); +} + +function startNormalPaymentTimer() { + if (global.config.payout.timer > 35791) { + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes for its next normal run."); + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } +} + +function init() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("(Payout Timer Configuration) Normal Timer: " + global.config.payout.timer + " minutes, Out of Money Retry Timer: " + global.config.payout.timerRetry + " minutes"); + startNormalPaymentTimer(); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/krb.js b/lib/payment_systems/krb.js new file mode 100644 index 0000000..bc907c1 --- /dev/null +++ b/lib/payment_systems/krb.js @@ -0,0 +1,254 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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.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.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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); \ No newline at end of file diff --git a/lib/payment_systems/msr.js b/lib/payment_systems/msr.js new file mode 100644 index 0000000..3a98c55 --- /dev/null +++ b/lib/payment_systems/msr.js @@ -0,0 +1,254 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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.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.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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/omb.js b/lib/payment_systems/omb.js new file mode 100644 index 0000000..3a98c55 --- /dev/null +++ b/lib/payment_systems/omb.js @@ -0,0 +1,254 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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.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.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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/stl.js b/lib/payment_systems/stl.js new file mode 100644 index 0000000..0508ca1 --- /dev/null +++ b/lib/payment_systems/stl.js @@ -0,0 +1,696 @@ +"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 extraPaymentRound = false; +let paymentTimer = null; + +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 + } + ], + priority: global.config.payout.priority, + 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 > 0.00000001) { + 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(true); + } 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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 + } + ], + priority: global.config.payout.priority, + 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 > 0.00000001) { + 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 + */ + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 > 0.00000001) { + 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 + } + ], + priority: global.config.payout.priority, + 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 > 0.00000001) { + 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, fee: this.fee}; + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (body.fee && body.fee > 0.00000001) { + 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() { + global.support.rpcWallet("store", [], function () { + }); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/sumo.js b/lib/payment_systems/sumo.js new file mode 100644 index 0000000..9b3fb3b --- /dev/null +++ b/lib/payment_systems/sumo.js @@ -0,0 +1,696 @@ +"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 extraPaymentRound = false; +let paymentTimer = null; + +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 + } + ], + priority: global.config.payout.priority, + 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(true); + } 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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_long, '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 + } + ], + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn, + payment_id: orderStatus.xmr_required_payment_id_long + }; + 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 + */ + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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, fee: this.fee}; + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/trtl.js b/lib/payment_systems/trtl.js new file mode 100644 index 0000000..2ebc6e7 --- /dev/null +++ b/lib/payment_systems/trtl.js @@ -0,0 +1,257 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.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 + } + ], + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time, + 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 (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.fee, 1]).then(function (result) { + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + } else { + console.error("Unknown error from the wallet."); + } + }); + }; + + 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.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(); + } + } + 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, + fee: global.config.payout.fee, + unlock_time: global.config.payout.unlock_time + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (typeof body.tx_hash !== 'undefined') { + 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, global.config.payout.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() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/xao.js b/lib/payment_systems/xao.js new file mode 100644 index 0000000..bf04847 --- /dev/null +++ b/lib/payment_systems/xao.js @@ -0,0 +1,311 @@ +"use strict"; +const async = require("async"); +const debug = require("debug")("payments"); + +let hexChars = new RegExp("[0-9a-f]+"); +let extraPaymentRound = false; +let paymentTimer = null; + +let paymentQueue = async.queue(function (paymentDetails, callback) { + if (paymentTimer !== null) { + clearInterval(paymentTimer); + paymentTimer = null; + } + + // Protect our balance by making sure we have sufficient unlocked funds before trying to transact + global.support.rpcWallet('getbalance', [], function (body) { + console.log(body.result) //we log the balance + if (body.result) { + var amountToPay = paymentDetails.destinations.reduce(function (a, b) { return a + b; }, 0); + if (body.result.unlocked_balance < amountToPay) { + console.error("Wallet only has " + body.result.unlocked_balance + " unlocked balance, can't pay " + amountToPay + " worth of XAO. Retrying in " + timerRetry + " minutes!"); + if (!extraPaymentRound) { + setTimeout(function () { + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } + } else { + console.log(body.error) //we log errors + console.error("Issue checking pool wallet balance before 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 check wallet balance", + "Hello,\r\nThe payment daemon has hit an issue checking the pool's wallet balance: " + JSON.stringify(body.error) + + ". Please investigate and restart the payment daemon as appropriate"); + return; + } + }); + + 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')) { + if (body.error.message === "Not enough funds or problem with amount") { + // In theory the earlier check should prevent this from occurring. + console.error("Issue making payments, not enough money, will try later"); + if (!extraPaymentRound) { + setTimeout(function () { + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + console.log(body) + 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) + " XAO with a " + global.support.coinToDecimal(body.result.fee) + " XAO 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) + " XAO"); + } + let intCount = 0; + paymentDetails.destinations.forEach(function (details) { + console.log("Payment made to: " + details.address + " For: " + global.support.coinToDecimal(details.amount) + " XAO"); + intCount += 1; + if (intCount === paymentDetails.destinations.length) { + return callback(body.result); + } + }); + } + }); +}, 1); + +paymentQueue.drain = function () { + extraPaymentRound = false; + startNormalPaymentTimer(); + global.database.setCache('lastPaymentCycle', Math.floor(Date.now() / 1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " XAO to 1 miner"); + } + } else { + console.error("1 Unknown error from the wallet."); + } + }); + }; + + this.makePaymentAsIntegrated = function () { + let paymentDetails = { + destinations: [ + { + amount: this.amount - this.fee, + address: this.address + } + ], + priority: global.config.payout.priority, + 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) { + console.log(body.fee) + 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(); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(amount) + " XAO to 1 miner"); + } + } else { + console.error("2 Unknown error from the wallet."); + } + }); + }; + + 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 && global.coinFuncs.isIntegratedAddress(payee.address) === false) { + 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 && global.coinFuncs.isIntegratedAddress(payee.address) === true && (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(); + } + } + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log(global.config.payout.maxPaymentTxns) + console.log(global.config.payout.mixIn) + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + console.log("got so far") //just logging for debug purposes + console.log(body) //more logging + + 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, global.config.payout.feeSlewAmount, paymentDetails.destinations.length]).then(function (result) { + console.log(body.tx_hash.match(hexChars)[0]) //we need some logs to see various variables returned from transactions + console.log(global.config.payout.mixIn) //more logging + console.log(0, null, null, totalAmount, body.tx_hash.match(hexChars)[0], global.config.payout.mixIn, 0.1, paymentDetails.destinations.length) + paymentDetails.destinations.forEach(function (payeeItem) { + payee = payeeObjects[payeeItem.address]; + payee.transactionID = result.insertId; + payee.trackPayment(); + }); + }); + if (global.config.general.discordPayoutAnnounce === true) { + global.support.poolBotMsg("Pool paid out: " + global.support.coinToDecimal(totalAmount) + " XAO to " + paymentDetails.destinations.length + " miners"); + } + + }); + } + } + }); + }); + }); +} + +function startNormalPaymentTimer() { + if (global.config.payout.timer > 35791) { + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes for its next normal run."); + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } +} + +function init() { + global.support.rpcWallet("store", [], function () { + }); + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("(Payout Timer Configuration) Normal Timer: " + global.config.payout.timer + " minutes, Out of Money Retry Timer: " + global.config.payout.timerRetry + " minutes"); + startNormalPaymentTimer(); + makePayments(); +} + +init(); diff --git a/lib/payment_systems/xmr.js b/lib/payment_systems/xmr.js new file mode 100644 index 0000000..f098eee --- /dev/null +++ b/lib/payment_systems/xmr.js @@ -0,0 +1,824 @@ +"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'); +const sprintf = require("sprintf-js").sprintf; + +let hexChars = new RegExp("[0-9a-f]+"); +let bestExchange = global.config.payout.bestExchange; +let xmrAPIClient = request.createClient('https://xmr.to/api/v1/xmr2btc/'); + +let is_full_stop = false; + +function full_stop(err) { + is_full_stop = true; + console.error("Issue making payments: " + JSON.stringify(err)); + 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(err) + + ". Please investigate and restart the payment daemon as appropriate"); +} + +let shapeshiftQueue = async.queue(function (task, callback) { + if (is_full_stop) { + debug("Dropping all pending shapeshift payments"); + return; + } + + // 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 + } + ], + priority: global.config.payout.priority, + 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(true); + } 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) { + if (is_full_stop) { + debug("Dropping all pending xmr.to payments"); + return; + } + + // 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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_long, '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 + } + ], + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn, + payment_id: orderStatus.xmr_required_payment_id_long + }; + 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) { + if (is_full_stop) { + debug("Dropping all pending payments"); + return; + } + + /* + 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("Trying to make payment based on: " + JSON.stringify(paymentDetails)); + + function getbalance() { + global.support.rpcWallet("getbalance", paymentDetails, function (body) { + if (body.hasOwnProperty('error') || !body.hasOwnProperty('result')) { + console.error("Can't getbalance: " + JSON.stringify(body.error)); + setTimeout(getbalance, 60*1000); + return; + } + if (body.result.unlocked_balance === 0) { + console.log("Waiting for balance to unlock after previous payment"); + setTimeout(getbalance, 5*60*1000); + return; + } + console.log("Current wallet balance is " + global.support.coinToDecimal(body.result.balance) + " with " + global.support.coinToDecimal(body.result.unlocked_balance) + " unlocked balance"); + + let transferFunc = 'transfer'; + paymentDetails.get_tx_key = true; + global.support.rpcWallet(transferFunc, paymentDetails, function (body) { + debug("Payment made: " + JSON.stringify(body)); + if (body.hasOwnProperty('error') || !body.hasOwnProperty('result')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + setTimeout(getbalance, 10*60*1000); + } else { + full_stop(body.error); + } + return; + } + callback(body.result); + }); + }); + }; + + getbalance(); + +}, 1); + +paymentQueue.drain = function(){ + console.log("Payment queue drained"); + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn, + payment_id: this.paymentID + }; + let identifier = this.id; + let amount = this.amount; + let fee = this.fee; + 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) { + console.log("[*] Successful payment to " + identifier + " of " + global.support.coinToDecimal(amount) + " XMR (fee " + global.support.coinToDecimal(fee) + " - " + global.support.coinToDecimal(body.fee) + " = " + global.support.coinToDecimal(fee - body.fee) + ") with tx_hash " + body.tx_hash.match(hexChars)[0] + " and tx_key " + body.tx_key); + 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) { + if (!result.hasOwnProperty("affectedRows") || result.affectedRows != 1) { + console.error("Can't do: 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)" + ); + payee.transactionID = 0; + payee.manualPaymentShow(); + full_stop(result); + return; + } + payee.transactionID = result.insertId; + payee.tx_hash = body.tx_hash.match(hexChars)[0]; + payee.tx_key = body.tx_key; + payee.trackPayment(); + }); + } else { + console.error("Unknown error from the wallet: " + JSON.stringify(body)); + } + }); + }; + + this.makePaymentAsIntegrated = function () { + let paymentDetails = { + destinations: [ + { + amount: this.amount - this.fee, + address: this.address + } + ], + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + let identifier = this.id; + let amount = this.amount; + let fee = this.fee; + let address = this.address; + let payee = this; + + debug("Payment Details: " + JSON.stringify(paymentDetails)); + paymentQueue.push(paymentDetails, function (body) { + if (body.fee && body.fee > 10) { + console.log("[*] Successful payment to " + identifier + " of " + global.support.coinToDecimal(amount) + " XMR (fee " + global.support.coinToDecimal(fee) + " - " + global.support.coinToDecimal(body.fee) + " = " + global.support.coinToDecimal(fee - body.fee) + ") with tx_hash " + body.tx_hash.match(hexChars)[0] + " and tx_key " + body.tx_key); + 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) { + if (!result.hasOwnProperty("affectedRows") || result.affectedRows != 1) { + console.error("Can't do: INSERT INTO transactions (bitcoin, address, payment_id, xmr_amt, transaction_hash, mixin, fees, payees) VALUES (0, '" + + address + "', " + amount + ", '" + body.tx_hash.match(hexChars)[0] + "', " + global.config.payout.mixIn + ", " + body.fee + ", 1)" + ); + payee.transactionID = 0; + payee.manualPaymentShow(); + full_stop(result); + return; + } + payee.transactionID = result.insertId; + payee.tx_hash = body.tx_hash.match(hexChars)[0]; + payee.tx_key = body.tx_key; + payee.trackPayment(); + }); + } else { + console.error("Unknown error from the wallet: " + JSON.stringify(body)); + } + }); + }; + + this.makeBitcoinPayment = function () { + let functionalData = {address: this.address, amount: this.amount, fee: this.fee}; + 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.manualPaymentShow = function () { + console.error("Manual payment update:"); + console.error(" UPDATE balance SET amount = amount - " + this.amount + " WHERE id = " + this.sqlID + ";"); + console.error(" 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 + ");" + ); + }; + + this.trackPayment = function () { + global.mysql.query("UPDATE balance SET amount = amount - ? WHERE id = ?", [this.amount, this.sqlID]).then(function (result) { + if (!result.hasOwnProperty("affectedRows") || result.affectedRows != 1) { + console.error("Can't do SQL balance update"); + this.manualPaymentShow(); + full_stop(result); + } + }); + 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]).then(function (result) { + if (!result.hasOwnProperty("affectedRows") || result.affectedRows != 1) { + console.error("Can't do SQL payments update"); + this.manualPaymentShow(); + full_stop(result); + } + }); + + let payee = this; + + global.mysql.query("SELECT email FROM users WHERE username = ? AND enable_email IS true limit 1", [payee.id]).then(function (rows) { + if (rows.length === 0) return; + // toAddress, subject, body + let emailData = { + address: payee.id, + payment_amount: global.support.coinToDecimal(payee.amount - payee.fee), + amount: global.support.coinToDecimal(payee.amount), + fee: global.support.coinToDecimal(payee.fee), + tx_hash: payee.tx_hash, + tx_key: payee.tx_key + }; + global.support.sendEmail(rows[0].email, + sprintf("Your %(payment_amount)s XMR payment was just performed", emailData), + sprintf( + "Your payment of %(payment_amount)s XMR (with tx fee %(fee)s XMR) to %(address)s wallet was just performed and total due was decreased by %(amount)s XMR.\n" + + (payee.tx_hash && payee.tx_key ? "Your payment tx_hash is %(tx_hash)s and tx_key is %(tx_key)s (can be used to verify payment)\n" : ""), + emailData + ), + payee.id + ); + }); + }; +} + +function makePayments() { + if (is_full_stop) { + debug("Dropping all new payment creation"); + return; + } + if (paymentQueue.idle() === false) { + debug("Payment queue is not empty so dropping all new payment creation"); + return; + } + if (shapeshiftQueue.idle() === false) { + debug("Shapeshift payment queue is not empty so dropping all new payment creation"); + return; + } + if (xmrToQueue.idle() === false) { + debug("xmr.to payment queue is not empty so dropping all new payment creation"); + return; + } + + debug("Starting 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 = global.support.decimalToCoin(0.3); + let custom_threshold = false; + if (userRow.length !== 0 && userRow[0].payout_threshold != 0) { + threshold = userRow[0].payout_threshold; + custom_threshold = true; + } + 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) { + console.log("[++] " + payee.id + " miner to bulk payment. Amount: " + 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 && custom_threshold))) { + // 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. + console.log("[+] " + payee.id + " as separate payment to integrated address. Amount: " + global.support.coinToDecimal(payee.amount)); + payee.makePaymentAsIntegrated(); + } else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && custom_threshold)) && payee.bitcoin === 0) { + console.log("[+] " + payee.id + " as separate payment to payment ID address. Amount: " + global.support.coinToDecimal(payee.amount)); + payee.makePaymentWithID(); + } else if ((payee.amount >= global.support.decimalToCoin(global.config.payout.exchangeMin) || (payee.amount > threshold && custom_threshold)) && payee.bitcoin === 1) { + console.log("[+] " + payee.id + " as separate payment to bitcoin. Amount: " + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Adding payment for " + paymentDetails.destinations.length + " miners"); + paymentQueue.unshift(paymentDetails, function (body) { //jshint ignore:line + // This is the only section that could potentially contain multiple txns. Lets do this safely eh? + if (body.fee && body.fee > 10) { + let totalAmount = 0; + let totalFee = 0; + paymentDetails.destinations.forEach(function (payeeItem) { + totalAmount += payeeObjects[payeeItem.address].amount; + totalFee += payeeObjects[payeeItem.address].fee; + console.log("[**] Successful payment to " + payeeItem.address + " for " + global.support.coinToDecimal(payeeObjects[payeeItem.address].amount) + " XMR (fee " + global.support.coinToDecimal(payeeObjects[payeeItem.address].fee) + ")"); + }); + console.log("[*] Successful payment to multiple miners of " + global.support.coinToDecimal(totalAmount) + " XMR (fee " + global.support.coinToDecimal(totalFee) + " - " + global.support.coinToDecimal(body.fee) + " = " + global.support.coinToDecimal(totalFee - body.fee) + ") with tx_hash " + body.tx_hash.match(hexChars)[0] + " and tx_key " + body.tx_key); + 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) { + if (!result.hasOwnProperty("affectedRows") || result.affectedRows != 1) { + console.error("Can't do: 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 + ")" + ); + paymentDetails.destinations.forEach(function (payeeItem) { + payee = payeeObjects[payeeItem.address]; + payee.transactionID = 0; + payee.manualPaymentShow(); + }); + full_stop(result); + return; + } + paymentDetails.destinations.forEach(function (payeeItem) { + payee = payeeObjects[payeeItem.address]; + payee.transactionID = result.insertId; + payee.tx_hash = body.tx_hash.match(hexChars)[0]; + payee.tx_key = body.tx_key; + payee.trackPayment(); + }); + }); + } else { + console.error("Unknown error from the wallet: " + JSON.stringify(body)); + } + }); + } + } + if (roundCount === rows.length) debug("Finished processing payments for now"); + }); + }); + }); + debug("Finished makePayments"); +} + +function init() { + global.support.rpcWallet("store", [], function () {}); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () {}); + }, 60*1000); + + setInterval(function () { + console.log("Payment queue lengths: payment (" + (paymentQueue.running() + paymentQueue.length()) + ") / shapeshift (" + (shapeshiftQueue.running() + shapeshiftQueue.length()) + ") / xmr.to (" + (xmrToQueue.running() + xmrToQueue.length()) + ")"); + }, 10*60*1000); + + makePayments(); + + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes"); + setInterval(makePayments, global.config.payout.timer * 60 * 1000); +} + +if (global.config.payout.timer > 35791) { + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); +} else { + init(); +} diff --git a/lib/payment_systems/xmrold.js b/lib/payment_systems/xmrold.js new file mode 100644 index 0000000..596397c --- /dev/null +++ b/lib/payment_systems/xmrold.js @@ -0,0 +1,696 @@ +"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 extraPaymentRound = false; +let paymentTimer = null; + +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 + } + ], + priority: global.config.payout.priority, + 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(true); + } 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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 + } + ], + priority: global.config.payout.priority, + 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 + */ + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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, fee: this.fee}; + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); \ No newline at end of file diff --git a/lib/payment_systems/xmv.js b/lib/payment_systems/xmv.js new file mode 100644 index 0000000..596397c --- /dev/null +++ b/lib/payment_systems/xmv.js @@ -0,0 +1,696 @@ +"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 extraPaymentRound = false; +let paymentTimer = null; + +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 + } + ], + priority: global.config.payout.priority, + 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(true); + } 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(5); + 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": , + "btc_dest_address": "", + "uuid": "" + } + 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 + } + ], + priority: global.config.payout.priority, + 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 + */ + if (paymentTimer !== null){ + clearInterval(paymentTimer); + paymentTimer = null; + } + 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')) { + if (body.error.message === "not enough money"){ + console.error("Issue making payments, not enough money, will try later"); + if(!extraPaymentRound){ + setTimeout(function(){ + makePayments(); + }, global.config.payout.timerRetry * 60 * 1000); + } + extraPaymentRound = true; + return callback(false); + } else { + 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); + +paymentQueue.drain = function(){ + extraPaymentRound = false; + if (global.config.payout.timer > 35791){ + console.error("Payout timer is too high. Please use a value under 35791 to avoid overflows."); + } else { + paymentTimer = setInterval(makePayments, global.config.payout.timer * 60 * 1000); + } + global.database.setCache('lastPaymentCycle', Math.floor(Date.now()/1000)); +}; + +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 + } + ], + priority: global.config.payout.priority, + 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 + } + ], + priority: global.config.payout.priority, + 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, fee: this.fee}; + 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), + priority: global.config.payout.priority, + mixin: global.config.payout.mixIn + }; + console.log("Paying out: " + paymentDetails.destinations.length + " people"); + paymentQueue.push(paymentDetails, function (body) { //jshint ignore:line + // 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() { + global.support.rpcWallet("store", [], function () { + }); + if (global.config.allowBitcoin) { + determineBestExchange(); + setInterval(updateXMRToCompletion, 90000); + setInterval(updateShapeshiftCompletion, 90000); + setInterval(determineBestExchange, 60000); + } + setInterval(function () { + global.support.rpcWallet("store", [], function () { + }); + }, 60000); + console.log("Setting the payment timer to: " + global.config.payout.timer + " minutes with a: " + global.config.payout.timerRetry + " minute delay if the wallet is out of money"); + makePayments(); +} + +init(); \ No newline at end of file diff --git a/lib/payments.js b/lib/payments.js new file mode 100644 index 0000000..3113c54 --- /dev/null +++ b/lib/payments.js @@ -0,0 +1,3 @@ +"use strict"; + +require(global.config.coin.paymentFile); \ No newline at end of file diff --git a/lib/pool.js b/lib/pool.js new file mode 100644 index 0000000..c0c9ec0 --- /dev/null +++ b/lib/pool.js @@ -0,0 +1,1284 @@ +"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 lastBlockHash; +let lastBlockHashUpdateTime; +let lastBlockTemplateUpdateTime; +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; +let hexMatch = new RegExp("^[0-9a-f]+$"); +let totalShares = 0, trustedShares = 0, normalShares = 0, invalidShares = 0, outdatedShares = 0; + +Buffer.prototype.toByteArray = function () { + return Array.prototype.slice.call(this, 0); +}; + + +if (cluster.isMaster) { + threadName = "(Master) "; + setInterval(function () { + let trustedSharesPercent = (trustedShares / totalShares * 100).toFixed(2); + let normalSharesPercent = (normalShares / totalShares * 100).toFixed(2); + let invalidSharesPercent = (invalidShares / totalShares * 100).toFixed(2); + let outdatedSharesPercent = (outdatedShares / totalShares * 100).toFixed(2); + console.log(`>>> Processed Trusted ${trustedShares} (${trustedSharesPercent}%) / Validated ${normalShares} (${normalSharesPercent}%) / Invalid ${invalidShares} (${invalidSharesPercent}%) / Outdated ${outdatedShares} (${outdatedSharesPercent}%) / Total shares ${totalShares} in the last 30 seconds`); + totalShares = 0; + trustedShares = 0; + normalShares = 0; + invalidShares = 0; + outdatedShares = 0; + }, 30000); +} else { + threadName = "(Worker " + cluster.worker.id + " - " + process.pid + ") "; +} + +global.database.thread_id = threadName; + +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; + case 'sendRemote': + if (cluster.isMaster) { + global.database.sendQueue.push({body: Buffer.from(message.body, 'hex')}); + } + break; + case 'trustedShare': + trustedShares += 1; + totalShares += 1; + break; + case 'normalShare': + normalShares += 1; + totalShares += 1; + break; + case 'invalidShare': + invalidShares += 1; + totalShares += 1; + break; + case 'outdatedShare': + outdatedShares += 1; + } +} + +process.on('message', messageHandler); + +function sendToWorkers(data) { + workerList.forEach(function (worker) { + worker.send(data); + }); +} + +function retargetMiners() { + debug(threadName + "Performing difficulty check on miners"); + console.log('Performing difficulty update on miners'); + for (let minerId in activeMiners) { + if (activeMiners.hasOwnProperty(minerId)) { + let miner = activeMiners[minerId]; + if (!miner.fixed_diff || (miner.hashes > 0 && miner.difficulty * 10 < miner.calcNewDiff())) { + miner.updateDifficulty(); + } + } + } +} + +// wallet " " proxy miner name -> { connectTime, count, hashes } +// this is needed to set cummulative based diff for workers provided by Atreides proxy and xmrig-proxy +let proxyMiners = {}; + +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]; + let proxyMinerName = miner.payout + ":" + miner.identifier; + if (proxyMinerName in proxyMiners && --proxyMiners[proxyMinerName]["count"] <= 0) delete proxyMiners[proxyMinerName]; + } + } + } +} + +function templateUpdateReal() { + global.coinFuncs.getBlockTemplate(global.config.pool.address, function (rpcResponse) { + if (rpcResponse && typeof rpcResponse.result !== 'undefined') { + rpcResponse = rpcResponse.result; + debug(threadName + "New block template found at " + rpcResponse.height + " height"); + if (cluster.isMaster) { + sendToWorkers({type: 'newBlockTemplate', data: rpcResponse}); + newBlockTemplate(rpcResponse); + } else { + process.send({type: 'newBlockTemplate', data: rpcResponse}); + newBlockTemplate(rpcResponse); + } + } else { + console.error("Block template request failed!"); + setTimeout(templateUpdateReal, 3000); + } + }); +} + +function templateUpdate(repeating) { + if (global.config.general.allowStuckPoolKill && cluster.isMaster && lastBlockHashUpdateTime && Date.now() - lastBlockHashUpdateTime > 30*60*1000) { + console.error("Block height was not updated for half an hour. Check your monerod. Exiting..."); + fs.closeSync(fs.openSync("block_template_is_stuck", 'w')); + process.exit(); + } + global.coinFuncs.getLastBlockHeader(function (err, body) { + if (err === null) { + let time_now = Date.now(); + let is_block_hash_updated = !lastBlockHash || body.hash !== lastBlockHash; + if (is_block_hash_updated) { + lastBlockHashUpdateTime = time_now; + lastBlockHash = body.hash; + templateUpdateReal(); + } + if (repeating === true) setTimeout(templateUpdate, 50, repeating); + } else { + console.error("Last block header request failed!"); + setTimeout(templateUpdate, 1000, repeating); + } + }); +} + +function newBlockTemplate(template) { + if (activeBlockTemplate) { + if (activeBlockTemplate.previous_hash.toString('hex') === template.prev_hash) { + console.log(threadName + 'Ignoring duplicate block template update at height: ' + template.height + '. Difficulty: ' + template.difficulty); + return; + } + pastBlockTemplates.enq(activeBlockTemplate); + } + console.log(threadName + 'New block to mine at height: ' + template.height + '. Difficulty: ' + template.difficulty); + 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.sendNewJob(); + } + } + lastBlockTemplateUpdateTime = Date.now(); +} + +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 + }; +})(); + +// here we keep verified share number of a specific wallet (miner.payout) +// it will reset to 0 after invalid share is found +// if walletTrust exceeds certain threshold (global.config.pool.trustThreshold * 100) then low diff (<=16000) new workers for this wallet are started with high trust +// this is needed to avoid CPU overload after constant miner reconnections that happen during mining botnet swarms +let walletTrust = {}; +// wallet last seen time (all wallets that are not detected for more than 1 day are removed) +let walletLastSeeTime = {}; + +var reEmail = /^\S+@\S+\.\S+$/; +// wallet password last check time +let walletLastCheckTime = {}; + +function Miner(id, login, pass, ipAddress, startingDiff, messageSender, protoVersion, portType, port, agent) { + // Username Layout -
. + // Password Layout - .. + // 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(":"); + // Workaround for a common mistake to put email without : before it + // and also security measure to hide emails used as worker names + if (pass_split.length === 1 && reEmail.test(pass_split[0])) { + pass_split.push(pass_split[0]); + pass_split[0] = "email"; + } + this.error = ""; + this.identifier = pass_split[0]; + this.proxy = false; + if (agent && agent.includes('MinerGate')) { + this.identifier = "MinerGate"; + } + if (agent && agent.includes('xmr-node-proxy')) { + this.proxy = true; + } + this.paymentID = null; + this.valid_miner = true; + this.port = port; + this.portType = portType; + this.incremented = false; + 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 (agent && agent.includes('NiceHash')) { + this.fixed_diff = true; + this.difficulty = global.coinFuncs.niceHashDiff; + } + 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 === 64 && hexMatch.test(addressSplit[1])) { + this.paymentID = addressSplit[1]; + this.payout = this.address + "." + this.paymentID; + } else if (typeof(addressSplit[1]) !== 'undefined') { + this.identifier = pass_split[0] === 'x' ? addressSplit[1] : pass_split[0]; + } + if (typeof(addressSplit[2]) !== 'undefined') { + this.identifier = pass_split[0] === 'x' ? addressSplit[2] : pass_split[0]; + } + + this.identifier = this.identifier.substring(0, 64); + + 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 && global.coinFuncs.supportsAutoExchange) { + 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; + } + if (global.coinFuncs.isIntegratedAddress(this.address) === true && this.paymentID) { ++ this.error = "Integrated addresses cannot also have a Payment ID"; ++ this.valid_miner = false; ++ } + if (!activeBlockTemplate) { + this.error = "No active block template"; + this.valid_miner = false; + } + + if (this.valid_miner && 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; + let time_now = Date.now(); + if (!(payoutAddress in walletLastCheckTime) || time_now - walletLastCheckTime[payoutAddress] > 60*1000) { + 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]]); + }); + walletLastCheckTime[payoutAddress] = time_now; + } + } + + 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 = Math.floor(Date.now() / 1000); + + this.validShares = 0; + this.invalidShares = 0; + this.hashes = 0; + this.logString = this.address + " ID: " + this.identifier + " IP: " + this.ipAddress; + + if (global.config.pool.trustedMiners) { + if (!(this.payout in walletTrust)) { + walletTrust[this.payout] = 0; + walletLastSeeTime[this.payout] = Date.now(); + } + let is_trusted_wallet = this.difficulty <= 16001 && this.payout in walletTrust && walletTrust[this.payout] > global.config.pool.trustThreshold * 20; + this.trust = { + threshold: is_trusted_wallet ? 1 : global.config.pool.trustThreshold, + probability: is_trusted_wallet ? global.config.pool.trustMin : 256, + penalty: 0 + }; + } + + this.validJobs = global.support.circularBuffer(4); + this.sentJobs = global.support.circularBuffer(8); + + this.cachedJob = null; + + 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.calcNewDiff = function () { + let proxyMinerName = this.payout + ":" + this.identifier; + if (proxyMinerName in proxyMiners) { + let target = 5; + return Math.floor((proxyMiners[proxyMinerName]["hashes"] / (Math.floor((Date.now() - proxyMiners[proxyMinerName]["connectTime"]) / 1000))) * target); + } else { + let target = this.proxy ? 5 : global.config.pool.targetTime; + return Math.floor((this.hashes / (Math.floor((Date.now() - this.connectTime) / 1000))) * target); + } + }; + + this.updateDifficulty = function () { + if (this.hashes > 0) { + this.setNewDiff(this.calcNewDiff()); + } 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.proxy) { + 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; + } + debug(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); + } + if (this.fixed_diff) { + console.log("Dropped low fixed diff " + this.difficulty + " for " + this.payout + ":" + this.identifier + " miner to " + this.newDiff + " dynamic diff"); + this.fixed_diff = false; + } + this.sendNewJob(); + }; + + 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]; + let proxyMinerName = this.payout + ":" + this.identifier; + if (proxyMinerName in proxyMiners && --proxyMiners[proxyMinerName]["count"] <= 0) delete proxyMiners[proxyMinerName]; + 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 && activeBlockTemplate.idHash === this.validJobs.get(0).blockHash && !this.newDiff && this.cachedJob !== null) { + return this.cachedJob; + } + + if (!this.proxy) { + let blob = activeBlockTemplate.nextBlob(); + let target = this.getTargetHex(); + this.lastBlockHeight = activeBlockTemplate.height; + + + let newJob = { + id: crypto.pseudoRandomBytes(21).toString('base64'), + extraNonce: activeBlockTemplate.extraNonce, + height: activeBlockTemplate.height, + difficulty: this.difficulty, + diffHex: this.diffHex, + submissions: [], + blockHash: activeBlockTemplate.idHash + }; + + this.validJobs.enq(newJob); + this.cachedJob = { + blob: blob, + job_id: newJob.id, + target: target, + id: this.id + }; + } else { + let blob = activeBlockTemplate.nextBlobWithChildNonce(); + if (this.newDiff) { + this.difficulty = this.newDiff; + this.newDiff = null; + } + this.lastBlockHeight = activeBlockTemplate.height; + + let newJob = { + id: crypto.pseudoRandomBytes(21).toString('base64'), + extraNonce: activeBlockTemplate.extraNonce, + height: activeBlockTemplate.height, + difficulty: this.difficulty, + diffHex: this.diffHex, + clientPoolLocation: activeBlockTemplate.clientPoolLocation, + clientNonceLocation: activeBlockTemplate.clientNonceLocation, + submissions: [] + }; + this.validJobs.enq(newJob); + this.cachedJob = { + blocktemplate_blob: blob, + difficulty: activeBlockTemplate.difficulty, + height: activeBlockTemplate.height, + reserved_offset: activeBlockTemplate.reserveOffset, + client_nonce_offset: activeBlockTemplate.clientNonceLocation, + client_pool_offset: activeBlockTemplate.clientPoolLocation, + target_diff: this.difficulty, + target_diff_hex: this.diffHex, + job_id: newJob.id, + id: this.id + }; + } + return this.cachedJob; + }; + + this.sendNewJob = function() { + let job = this.getJob(); + let tempJob = this.sentJobs.toarray().filter(function (intJob) { + return intJob.id === job.job_id; + })[0]; + + if (tempJob) { + console.error(`Tried sending a duped job to: ${this.address}, stopped by Snipa!`); + return; + } + this.sentJobs.enq(job); + return this.messageSender('job', job); + }; + } +} + +// store wallet_key (address, paymentID, bitcoin, poolTypeEnum) -> worker_name -> shareType -> (height, difficulty, time, acc) +let walletAcc = {}; +// number of worker_name for wallet_key (so we do not count them by iteration) +let walletWorkerCount = {}; +// is share finalizer function for dead worker_name is active +let is_walletAccFinalizer = {}; + +function walletAccFinalizer(wallet_key, miner_address, miner_paymentID, miner_bitcoin, miner_poolTypeEnum) { + //console.log("!!! " + wallet_key + ": scanning for old worker names"); + let wallet = walletAcc[wallet_key]; + let is_something_left = false; + let time_now = Date.now(); + for (let worker_name in wallet) { + let worker = wallet[worker_name]; + if (time_now - worker["time"] > 60*1000) { + let acc = worker["acc"]; + if (acc != 0) { + let height = worker["height"]; + //console.log("!!! " + wallet_key + " / " + worker_name + ": storing old worker share " + height + " " + worker["difficulty"] + " " + time_now + " " + acc); + global.database.storeShare(height, global.protos.Share.encode({ + shares: acc, + paymentAddress: miner_address, + paymentID: miner_paymentID, + foundBlock: false, + trustedShare: true, + poolType: miner_poolTypeEnum, + poolID: global.config.pool_id, + blockDiff: worker["difficulty"], + bitcoin: miner_bitcoin, + blockHeight: height, + timestamp: time_now, + identifier: worker_name + })); + } + //console.log("!!! " + wallet_key + ": removing old worker " + worker_name); + if (worker_name !== "all_other_workers") -- walletWorkerCount[wallet_key]; + delete wallet[worker_name]; + } else { + is_something_left = true; + } + } + + if (is_something_left) { + setTimeout(walletAccFinalizer, 60*1000, wallet_key, miner_address, miner_paymentID, miner_bitcoin, miner_poolTypeEnum); + } else { + is_walletAccFinalizer[wallet_key] = false; + } +} + +function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate) { + miner.hashes += job.difficulty; + let proxyMinerName = miner.payout + ":" + miner.identifier; + if (proxyMinerName in proxyMiners) proxyMiners[proxyMinerName]["hashes"] += job.difficulty; + + let time_now = Date.now(); + let wallet_key = miner.address + " " + miner.paymentID + " " + miner.bitcoin + " " + miner.poolTypeEnum; + + if (!(wallet_key in walletAcc)) { + walletAcc[wallet_key] = {}; + walletWorkerCount[wallet_key] = 0; + is_walletAccFinalizer[wallet_key] = false; + } + + if (job.difficulty >= 1000000 || blockCandidate) { + + global.database.storeShare(job.height, global.protos.Share.encode({ + shares: job.rewarded_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: time_now, + identifier: miner.identifier + })); + + } else { + + let wallet = walletAcc[wallet_key]; + + let worker_name = miner.identifier in wallet || walletWorkerCount[wallet_key] < 50 ? miner.identifier : "all_other_workers"; + + if (!(worker_name in wallet)) { + if (worker_name !== "all_other_workers") ++ walletWorkerCount[wallet_key]; + //console.log("!!! " + wallet_key + ": adding new worker " + worker_name + " (num " + walletWorkerCount[wallet_key] + ")"); + wallet[worker_name] = {}; + let worker = wallet[worker_name]; + worker["height"] = job.height; + worker["difficulty"] = activeBlockTemplate.difficulty; + worker["time"] = time_now; + worker["acc"] = 0; + } + + let worker = wallet[worker_name]; + + let height = worker["height"]; + let difficulty = worker["difficulty"]; + let acc = worker["acc"]; + + if (height !== job.height || difficulty !== activeBlockTemplate.difficulty || time_now - worker["time"] > 60*1000 || acc >= 1000000) { + if (acc != 0) { + //console.log("!!! " + wallet_key + " / " + worker_name + ": storing share " + height + " " + difficulty + " " + time_now + " " + acc); + global.database.storeShare(height, global.protos.Share.encode({ + shares: acc, + paymentAddress: miner.address, + paymentID: miner.paymentID, + foundBlock: false, + trustedShare: shareType, + poolType: miner.poolTypeEnum, + poolID: global.config.pool_id, + blockDiff: difficulty, + bitcoin: miner.bitcoin, + blockHeight: height, + timestamp: time_now, + identifier: worker_name + })); + } + + worker["height"] = job.height; + worker["difficulty"] = activeBlockTemplate.difficulty; + worker["time"] = time_now; + worker["acc"] = job.rewarded_difficulty; + + } else { + worker["acc"] += job.rewarded_difficulty; + } + + //console.log("!!! " + wallet_key + " / " + worker_name + ": accumulating share " + job.height + " " + activeBlockTemplate.difficulty + " " + worker["time"] + " " + worker["acc"] + " (+" + job.rewarded_difficulty + ")"); + } + + if (is_walletAccFinalizer[wallet_key] === false) { + is_walletAccFinalizer[wallet_key] = true; + setTimeout(walletAccFinalizer, 60*1000, wallet_key, miner.address, miner.paymentID, miner.bitcoin, miner.poolTypeEnum); + } + + 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) { + process.send({type: 'trustedShare'}); + debug(threadName + "Accepted trusted share at difficulty: " + job.difficulty + "/" + job.rewarded_difficulty + "/" + shareDiff + " from: " + miner.logString); + } else { + process.send({type: 'normalShare'}); + debug(threadName + "Accepted valid share at difficulty: " + job.difficulty + "/" + job.rewarded_difficulty + "/" + shareDiff + " from: " + miner.logString); + } + if (activeBlockTemplate && activeBlockTemplate.height != job.height) { + process.send({type: 'outdatedShare'}); + } + +} + +function processShare(miner, job, blockTemplate, params) { + let nonce = params.nonce; + let resultHash = params.result; + let template = new Buffer(blockTemplate.buffer.length); + if (!miner.proxy) { + blockTemplate.buffer.copy(template); + template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset); + } else { + blockTemplate.buffer.copy(template); + template.writeUInt32BE(job.extraNonce, blockTemplate.reserveOffset); + template.writeUInt32BE(params.poolNonce, job.clientPoolLocation); + template.writeUInt32BE(params.workerNonce, job.clientNonceLocation); + } + let shareBuffer = global.coinFuncs.constructNewBlob(template, new Buffer(nonce, 'hex')); + + let convertedBlob; + let hash; + let shareType; + + if (global.config.pool.trustedMiners && miner.difficulty < 400000 && 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; + ++ walletTrust[miner.payout]; + walletLastSeeTime[miner.payout] = Date.now(); + } + if (hash.toString('hex') !== resultHash) { + console.error(threadName + "Bad share from miner (diff " + job.difficulty + ") " + miner.logString); + process.send({type: 'invalidShare'}); + if (miner.incremented === false) { + miner.newDiff = miner.difficulty + 1; + miner.incremented = true; + } else { + miner.newDiff = miner.difficulty - 1; + miner.incremented = false; + } + miner.sendNewJob(); + walletTrust[miner.payout] = 0; + walletLastSeeTime[miner.payout] = Date.now(); + 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. + // Todo: Implement within the coins/.js file. + 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)) { + process.send({type: 'invalidShare'}); + 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; +} + +let lastMessageTime; + +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 && params.agent && !params.agent.includes('MinerGate'))) { + 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, params.agent); + if (!miner.valid_miner) { + let time_now = Date.now(); + if (!lastMessageTime || time_now - lastMessageTime > 30*1000) { + console.log("Invalid miner, disconnecting due to: " + miner.error); + lastMessageTime = time_now; + } + sendReply(miner.error); + return; + } + process.send({type: 'newMiner', data: miner.port}); + activeMiners[minerId] = miner; + if (!miner.proxy && params.agent && params.agent.includes('proxy')) { + let proxyMinerName = miner.payout + ":" + miner.identifier; + if (!(proxyMinerName in proxyMiners)) { + proxyMiners[proxyMinerName] = {}; + proxyMiners[proxyMinerName]["connectTime"] = Date.now(); + proxyMiners[proxyMinerName]["count"] = 0; + proxyMiners[proxyMinerName]["hashes"] = 0; + console.log("Starting to calculate high diff for " + proxyMinerName + " proxy"); + } + ++ proxyMiners[proxyMinerName]["count"]; + } + sendReply(null, { + id: minerId, + job: miner.getJob(), + status: 'OK' + }); + break; + case 'getjob': + if (!miner) { + sendReply('Unauthenticated'); + return; + } + miner.heartbeat(); + miner.sendNewJob(); + 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 = (typeof params.nonce === 'string' ? 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 (!miner.proxy) { + 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); + } else { + if (!Number.isInteger(params.poolNonce) || !Number.isInteger(params.workerNonce)) { + console.warn(threadName + 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + miner.logString); + miner.checkBan(false); + sendReply('Duplicate share'); + global.database.storeInvalidShare(miner.invalidShareProto); + return; + } + let nonce_test = `${params.nonce}_${params.poolNonce}_${params.workerNonce}`; + if (job.submissions.indexOf(nonce_test) !== -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(nonce_test); + } + + let blockTemplate = activeBlockTemplate.height === job.height ? activeBlockTemplate : pastBlockTemplates.toarray().filter(function (t) { + return t.height === job.height; + })[0]; + + let is_outdated = false; + + job.rewarded_difficulty = job.difficulty; + + if (activeBlockTemplate.height != job.height && lastBlockTemplateUpdateTime) { + let late_time = Date.now() - lastBlockTemplateUpdateTime; + let max_late_time = global.config.pool.targetTime*1000; + if (late_time < max_late_time) { + let factor = (max_late_time - late_time) / max_late_time; + job.rewarded_difficulty = Math.floor(job.difficulty * Math.pow(factor, 6)); + if (job.rewarded_difficulty === 0) job.rewarded_difficulty = 1; + } else { + is_outdated = true; + } + } + + if (!blockTemplate || is_outdated) { + let err_str = is_outdated ? "Block outdated" : "Block expired"; + console.warn(threadName + err_str + ', Height: ' + job.height + ' (diff ' + job.difficulty + ') from ' + miner.logString); + if (miner.incremented === false) { + miner.newDiff = miner.difficulty + 1; + miner.incremented = true; + } else { + miner.newDiff = miner.difficulty - 1; + miner.incremented = false; + } + miner.sendNewJob(); + sendReply(err_str); + global.database.storeInvalidShare(miner.invalidShareProto); + return; + } + + let shareAccepted = processShare(miner, job, blockTemplate, params); + miner.checkBan(shareAccepted); + + if (global.config.pool.trustedMiners) { + if (shareAccepted) { + miner.trust.probability -= global.config.pool.trustChange; + 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; + case 'keepalived': + if (!miner) { + sendReply('Unauthenticated'); + return; + } + sendReply(null, { + status: 'KEEPALIVED' + }); + break; + } +} + +if (global.config.general.allowStuckPoolKill && fs.existsSync("block_template_is_stuck")) { + console.error("Stuck block template was detected on previous run. Please fix monerod and remove block_template_is_stuck file after that. Exiting..."); + process.exit(); +} + +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]); + if (activeBlockTemplate) { + 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); + }); + templateUpdate(); + setTimeout(templateUpdate, 50, true); + 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 = []; + //templateUpdate(); + }, 60000); + + // load merged wallet trust from files + let numWorkers = require('os').cpus().length; + for (let i = 1; i <= numWorkers; ++ i) { + let fn = "wallet_trust_" + i.toString(); + let rs = fs.createReadStream(fn); + rs.on('error', function() { console.error("Can't open " + fn + " file"); }); + let lineReader = require('readline').createInterface({ input: rs }); + lineReader.on('line', function (line) { + let parts = line.split(/\t/); + if (parts.length != 3) { + console.error("Error line " + line + " ignored from " + fn + " file"); + return; + } + let wallet = parts[0]; + let trust = parseInt(parts[1], 10); + let time = parseInt(parts[2], 10); + if (Date.now() - time < 24*60*60*1000 && (!(wallet in walletTrust) || trust < walletTrust[wallet])) { + console.log("Adding " + trust.toString() + " trust for " + wallet + " wallet"); + walletTrust[wallet] = trust; + walletLastSeeTime[wallet] = time; + } + }); + } + + // dump wallet trust to file + setInterval(function () { + let str = ""; + for (let wallet in walletTrust) { + let time = walletLastSeeTime[wallet]; + if (Date.now() - time < 24*60*60*1000) { + str += wallet + "\t" + walletTrust[wallet].toString() + "\t" + time.toString() + "\n"; + } else { + delete walletTrust[wallet]; + delete walletLastSeeTime[wallet]; + } + } + let fn = "wallet_trust_" + cluster.worker.id.toString(); + fs.writeFile(fn, str, function(err) { if (err) console.error("Error saving " + fn + " file"); }); + }, 10*60*1000); + + async.each(global.config.ports, function (portData) { + if (global.config[portData.portType].enable !== true) { + return; + } + 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') > 102400) { //100KB + 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); + }); + } + }); +} diff --git a/lib/remoteShare.js b/lib/remoteShare.js new file mode 100644 index 0000000..0c564ae --- /dev/null +++ b/lib/remoteShare.js @@ -0,0 +1,108 @@ +"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 + ") "; +} +let shareData = []; + +// Websocket Stuffs. +app.use(function(req, res, next){ + req.pipe(concat(function(data){ + req.body = data; + next(); + })); +}); + +// Master/Slave communication Handling +function messageHandler(message) { + if (typeof message.shares === "number"){ + shareData.push(message); + } +} + +process.on('message', messageHandler); + +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: + try { + process.send(global.protos.Share.decode(msgData.msg)); + } catch (e){ + } + return res.json({'success': true}); + 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(); + } +}); + +function storeShares(){ + if (Object.keys(shareData).length > 0){ + global.database.storeBulkShares(shareData); + shareData = []; + } + setTimeout(storeShares, 1000); +} + +if (cluster.isMaster) { + let numWorkers = require('os').cpus().length; + console.log('Master cluster setting up ' + numWorkers + ' workers...'); + storeShares(); + + 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); + }); +} else { + app.listen(8000, function () { + console.log('Process ' + process.pid + ' is listening to all incoming requests'); + }); +} diff --git a/lib/remote_comms.js b/lib/remote_comms.js new file mode 100644 index 0000000..d296b28 --- /dev/null +++ b/lib/remote_comms.js @@ -0,0 +1,69 @@ +"use strict"; +const request = require('request'); +const async = require('async'); + +function Database() { + + let thread_id=''; + + this.sendQueue = async.queue(function (task, callback) { + async.doUntil( + function (intCallback) { + request.post({url: global.config.general.shareHost, body: task.body, forever: true}, function (error, response, body) { + if (!error) { + return intCallback(null, response.statusCode); + } + return intCallback(null, 0); + }); + }, + function (data) { + return data === 200; + }, + function () { + callback(); + }); + }, require('os').cpus().length*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 + }); + process.send({type: 'sendRemote', body: wsData.toString('hex')}); + }; + + 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 + }); + process.send({type: 'sendRemote', body: wsData.toString('hex')}); + }; + + this.storeInvalidShare = function (minerData) { + let wsData = global.protos.WSData.encode({ + msgType: global.protos.MESSAGETYPE.INVALIDSHARE, + key: global.config.api.authKey, + msg: minerData, + exInt: 1 + }); + process.send({type: 'sendRemote', body: wsData.toString('hex')}); + }; + + setInterval(function(queue_obj){ + if (global.database.thread_id === '(Master) '){ + console.log(global.database.thread_id + "Queue debug state: " + queue_obj.length() + " items in the queue " + queue_obj.running() + " items being processed"); + } + }, 30*1000, this.sendQueue); + + + this.initEnv = function(){ + this.data = null; + }; +} + +module.exports = Database; \ No newline at end of file diff --git a/lib/support.js b/lib/support.js new file mode 100644 index 0000000..0af4f9f --- /dev/null +++ b/lib/support.js @@ -0,0 +1,233 @@ +"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'); +const sprintf = require("sprintf-js").sprintf; + +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; +} + +// accumulates email notifications up to one hour (email/subject -> body) +let emailAcc = {}; +// last send time of email (email/subject -> time) +let emailLastSendTime = {}; +let lastEmailSendTime; + +function sendEmailReal(toAddress, subject, email_body){ + if (lastEmailSendTime && Date.now() - lastEmailSendTime < 1000) { + setTimeout(sendEmailReal, 1000, toAddress, subject, email_body); + return; + } + lastEmailSendTime = Date.now(); + 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: email_body + }, + agentOptions: { + rejectUnauthorized: global.config.general.mailgunNoCert === true ? false : true + } + }, function(err, response, body){ + if (!err && response.statusCode === 200) { + debug(email_body); + console.log("Email to '" + toAddress + "' was sent successfully! Response: " + body); + } else { + console.error("Did not send e-mail to '" + toAddress + "' successfully! Response: " + body + " Response: "+JSON.stringify(response)); + } + }); +} + +function sendEmail(toAddress, subject, body, wallet){ + if (toAddress === global.config.general.adminEmail) { + sendEmailReal(toAddress, subject, body); + } else { + let reEmail = /^([a-zA-Z0-9_\.-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; + if (!reEmail.test(toAddress)) { + console.error("Avoid sending email to invalid address '" + toAddress + "'"); + return; + } + let key = toAddress + "\t" + subject; + if (!(key in emailAcc)) { + emailAcc[key] = body; + let time_now = Date.now(); + let is_fast_email = !(key in emailLastSendTime) || time_now - emailLastSendTime[key] > 6*60*60*1000; + emailLastSendTime[key] = time_now; + setTimeout(function(email_address, email_subject, wallet) { + let key2 = email_address + "\t" + email_subject; + let email_body = emailAcc[key2]; + delete emailAcc[key2]; + let emailData = { + wallet: wallet + }; + sendEmailReal(email_address, email_subject, "Hello,\n\n" + email_body + "\n\nThank you,\n" + sprintf(global.config.general.emailSig, emailData)); + }, (is_fast_email ? 5 : 30)*60*1000, toAddress, subject, wallet); + } else { + emailAcc[key] += body; + } + } +} + +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, {timeout: 300000}); + 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); + } + debug("JSON result: " + JSON.stringify(body)); + return callback(body); + }); + }); + } else { + client.post(path, data, function (err, res, body) { + if (err) { + return callback(err); + } + debug("JSON result: " + JSON.stringify(body)); + 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 Math.round(amount * global.config.coin.sigDigits); +} + +function bitcoinDecimalToCoin(amount) { + return Math.round(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 + }; +}; diff --git a/lib/worker.js b/lib/worker.js new file mode 100644 index 0000000..b356167 --- /dev/null +++ b/lib/worker.js @@ -0,0 +1,602 @@ +"use strict"; +const debug = require("debug")("worker"); +const async = require("async"); +const sprintf = require("sprintf-js").sprintf; + +let threadName = "Worker Server "; +let cycleCount = 0; +let lastBlockHash = null; +let hashrate_avg_min = 5; + +// cached email of specific address +let minerEmail = {}; +// time of last SQL check for specific address +let minerEmailTime = {}; + +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 = Date.now(); + //let activeAddresses = []; + async.waterfall([ + function (callback) { + global.coinFuncs.getLastBlockHeader(function (err, body) { + if (err !== null){ + return callback(err, "Invalid block header"); + } + callback(null, body.height + 1); + }); + }, + function (height, callback) { + let locTime = Date.now() - (hashrate_avg_min*60*1000); + let identifierTime = Date.now() - (2*hashrate_avg_min*60*1000); + 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 minerCount = 0; + 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); + ++ minerCount; + } else { + identifiers[minerID] = [share.identifier]; + ++ minerCount; + } + 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 (minerList.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 (minerList.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 || height < 0) { + return true; + } + return oldestTime <= identifierTime; + }, function (err) { + // todo: Need to finish parsing the cached data into caches for caching purproses. + let prevMinerList = global.database.getCache('minerList'); + if (prevMinerList === false) prevMinerList = minerList; + let cache_updates = {}; + // 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] / (hashrate_avg_min*60)) + 1; + cachedData.lastHash = localTimes[key]; + cachedData.minerCount = localMinerCount[key]; + if (!cachedData.hasOwnProperty("hashHistory")) { + cachedData.hashHistory = []; + cachedData.minerHistory = []; + } + if (cycleCount === 0) { + cachedData.hashHistory.unshift({ts: currentTime, 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: currentTime, 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] / (hashrate_avg_min*60)) + 1, + totalHashes: 0, + lastHash: localTimes[key], + minerCount: localMinerCount[key], + hashHistory: [{ts: currentTime, hs: cachedData.hash}], + minerHistory: [{ts: currentTime, cn: cachedData.hash}] + }; + } + cache_updates[key + "_stats"] = cachedData; + }); + minerList.forEach(function (miner) { + //if (miner.indexOf('_') === -1){ + // activeAddresses.push(miner); + //} + let cachedData = global.database.getCache(miner); + if (cachedData !== false) { + cachedData.hash = Math.floor(localStats.miners[miner] / (hashrate_avg_min*60)) + 1; + cachedData.lastHash = localTimes.miners[miner]; + if (!cachedData.hasOwnProperty("hashHistory")) { + cachedData.hashHistory = []; + } + if (cycleCount === 0){ + cachedData.hashHistory.unshift({ts: currentTime, 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] / (hashrate_avg_min*60)) + 1, + totalHashes: 0, + lastHash: localTimes.miners[miner], + hashHistory: [{ts: currentTime, hs: cachedData.hash}], + goodShares: 0, + badShares: 0 + }; + } + cache_updates[miner] = cachedData; + }); + + // remove old workers + prevMinerList.forEach(function (miner) { + if (minerList.indexOf(miner) !== -1) return; // we still have this miner in current list + debug("Removing: " + miner + " as an active miner from the cache."); + let minerStats = global.database.getCache(miner); + minerStats.hash = 0; + cache_updates[miner] = minerStats; + if (miner.indexOf('_') <= -1) return; + + // This is a worker case. + let address_parts = miner.split(/_(.+)/); + let address = address_parts[0]; + let worker = address_parts[1]; + // toAddress, subject, body + let emailData = { + worker: worker, + timestamp: global.support.formatDate(Date.now()), + poolEmailSig: global.config.general.emailSig + }; + if (!(address in minerEmailTime) || currentTime - minerEmailTime[address] > 10*60*1000) { + minerEmailTime[address] = currentTime; + global.mysql.query("SELECT email FROM users WHERE username = ? AND enable_email IS true limit 1", [address]).then(function (rows) { + if (rows.length === 0) { + delete minerEmail[address]; + return; + } else { + minerEmail[address] = rows[0].email; + } + global.support.sendEmail(minerEmail[address], + sprintf(global.config.email.workerNotHashingSubject, emailData), + sprintf(global.config.email.workerNotHashingBody, emailData), + address + ); + }); + } else if (address in minerEmail) { + global.support.sendEmail(minerEmail[address], + sprintf(global.config.email.workerNotHashingSubject, emailData), + sprintf(global.config.email.workerNotHashingBody, emailData), + address + ); + } + }); + + // find new workers + minerList.forEach(function (miner) { + if (prevMinerList.indexOf(miner) !== -1) return; // we still have this miner in previous list + debug("Adding: " + miner + " as an active miner to the cache."); + if (miner.indexOf('_') <= -1) return; + + // This is a worker case. + let address_parts = miner.split(/_(.+)/); + let address = address_parts[0]; + let worker = address_parts[1]; + // toAddress, subject, body + let emailData = { + worker: worker, + timestamp: global.support.formatDate(Date.now()), + poolEmailSig: global.config.general.emailSig + }; + if (!(address in minerEmailTime) || currentTime - minerEmailTime[address] > 10*60*1000) { + minerEmailTime[address] = currentTime; + global.mysql.query("SELECT email FROM users WHERE username = ? AND enable_email IS true limit 1", [address]).then(function (rows) { + if (rows.length === 0) { + delete minerEmail[address]; + return; + } else { + minerEmail[address] = rows[0].email; + } + global.support.sendEmail(minerEmail[address], + sprintf(global.config.email.workerStartHashingSubject, emailData), + sprintf(global.config.email.workerStartHashingBody, emailData), + address + ); + }); + } else if (address in minerEmail) { + global.support.sendEmail(minerEmail[address], + sprintf(global.config.email.workerStartHashingSubject, emailData), + sprintf(global.config.email.workerStartHashingBody, emailData), + address + ); + } + }); + + Object.keys(identifiers).forEach(function (key) { + cache_updates[key + '_identifiers'] = identifiers[key]; + }); + cache_updates.minerList = minerList; + global.database.bulkSetCache(cache_updates); + console.log("Processed " + minerCount + " workers for " + ((Date.now() - currentTime) / 1000) + " seconds"); + callback(null); + }); + } + ], function (err, result) { + cycleCount += 1; + if (cycleCount === 3){ + cycleCount = 0; + } + }); + setTimeout(updateShareStats, 10*1000); +} + +function updatePoolStats(poolType) { + let cache; + if (typeof(poolType) !== 'undefined') { + cache = global.database.getCache(poolType + "_stats"); + let cache2 = global.database.getCache(poolType + "_stats2"); + cache.totalHashes = cache2.totalHashes; + cache.roundHashes = cache2.roundHashes; + } else { + cache = global.database.getCache("global_stats"); + let cache2 = global.database.getCache("global_stats2"); + cache.totalHashes = cache2.totalHashes; + cache.roundHashes = cache2.roundHashes; + } + async.series([ + function (callback) { + debug(threadName + "Checking Influx for last 10min avg for pool stats"); + return callback(null, cache.hash || 0); + }, + function (callback) { + debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats"); + return callback(null, cache.minerCount || 0); + }, + function (callback) { + debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats"); + return callback(null, cache.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 payment_address, payment_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 payment_address, payment_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 (callback) { + debug(threadName + "Checking Influx for last 10min avg for miner count for pool stats"); + return callback(null, cache.roundHashes || 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, + roundHashes: result[8] || 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 pool_id < 1000 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 id < 1000 and 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() { + // Todo: Implement within the coins/.js file. + global.support.rpcDaemon('getlastblockheader', [], function (body) { + if (typeof body.error !== 'undefined'){ + return console.error(`Issue getting last block header: ${JSON.stringify(body)}`); + } + if (body.result && body.result.block_header.hash !== lastBlockHash) { + lastBlockHash = body.result.block_header.hash; + 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 if (body.result.block_header.hash === lastBlockHash) { + debug("Block headers identical to historical header. Ignoring"); + } else { + console.error("GetLastBlockHeader Error during block header update"); + } + }); +} + +function updateWalletStats() { + async.waterfall([ + function (callback) { + // Todo: Implement within the coins/.js file. + 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) { + // Todo: Implement within the coins/.js file. + global.support.rpcWallet('getheight', [], function (body) { + if (body.result) { + state.height = body.result.height; + return callback(null, state); + } else if (typeof body.error !== 'undefined' && body.error.message === 'Method not found') { + state.height = 0; + return callback(null, state); + } else { + return callback(true, "Unable to get current wallet height"); + } + }); + } + ], function (err, results) { + if (err) { + return console.error("Unable to get wallet stats: " + results); + } + 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, hostname, ip FROM pools WHERE last_checkin > date_sub(now(), interval 30 minute)").then(function (rows) { + global.coinFuncs.getLastBlockHeader(function (err, block) { + if (err !== null){ + console.error("Issue in getting block header. Skipping node monitor"); + return; + } + 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); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f74337 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "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", + "cors": "^2.8.1", + "crypto": "0.0.3", + "debug": "2.5.1", + "express": "4.14.0", + "jsonwebtoken": "^7.2.1", + "minimist": "1.2.0", + "moment": "2.17.1", + "mysql": "2.15.0", + "node-lmdb": "0.4.12", + "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", + "socketio": "^1.0.0", + "sprintf-js": "^1.0.3", + "sticky-cluster": "^0.3.1", + "uuid": "3.0.1", + "wallet-address-validator": "0.1.0", + "zmq": "^2.15.3" + }, + "optionalDependencies": { + "cryptonote-util": "git://github.com/Snipa22/node-cryptonote-util.git#xmr-Nan-2.0", ++ "intensecoin-util": "git://github.com/valiant1x/node-cryptonote-util.git#xmr-Nan-2.0", + "forknote-util": "git://github.com/wallet42/node-forknote-util.git", + "multi-hashing": "git+https://github.com/Snipa22/node-multi-hashing-aesni.git" + } +} diff --git a/package.old.json b/package.old.json new file mode 100644 index 0000000..52b4c14 --- /dev/null +++ b/package.old.json @@ -0,0 +1,49 @@ +{ + "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", + "cors": "^2.8.1", + "crypto": "0.0.3", + "debug": "2.5.1", + "express": "4.14.0", + "jsonwebtoken": "^7.2.1", + "minimist": "1.2.0", + "moment": "2.17.1", + "mysql": "2.15.0", + "node-lmdb": "0.4.12", + "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", + "socketio": "^1.0.0", + "sprintf-js": "^1.0.3", + "sticky-cluster": "^0.3.1", + "uuid": "3.0.1", + "wallet-address-validator": "0.1.0", + "zmq": "^2.15.3" + }, + "optionalDependencies": { + "cryptonote-util": "git://github.com/Snipa22/node-cryptonote-util.git#xmr-Nan-2.0", + "forknote-util": "git://github.com/ArqTras/node-forknote-util.git", + "multi-hashing": "git+https://github.com/Snipa22/node-multi-hashing-aesni.git" + } +} diff --git a/sample_config.sql b/sample_config.sql new file mode 100644 index 0000000..dac267e --- /dev/null +++ b/sample_config.sql @@ -0,0 +1,6 @@ +UPDATE pool.config SET item_value = '' WHERE module = 'pool' and item = 'address'; +UPDATE pool.config SET item_value = '' WHERE module = 'payout' 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'; diff --git a/sql_sync/config_entries.json b/sql_sync/config_entries.json new file mode 100644 index 0000000..8d320da --- /dev/null +++ b/sql_sync/config_entries.json @@ -0,0 +1,578 @@ +[ + { + "id": 1, + "module": "pool", + "item": "address", + "item_value": "", + "item_type": "string", + "Item_desc": "Address to mine to, this should be the wallet-rpc address." + }, + { + "id": 2, + "module": "pool", + "item": "minerTimeout", + "item_value": "900", + "item_type": "int", + "Item_desc": "Length of time before a miner is flagged inactive." + }, + { + "id": 3, + "module": "pool", + "item": "banEnabled", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Enables/disabled banning of \"bad\" miners." + }, + { + "id": 4, + "module": "pool", + "item": "banLength", + "item_value": "-15m", + "item_type": "string", + "Item_desc": "Ban duration except perma-bans" + }, + { + "id": 5, + "module": "pool", + "item": "targetTime", + "item_value": "30", + "item_type": "int", + "Item_desc": "Time in seconds between share finds" + }, + { + "id": 6, + "module": "pool", + "item": "trustThreshold", + "item_value": "30", + "item_type": "int", + "Item_desc": "Number of shares before miner trust can kick in." + }, + { + "id": 8, + "module": "pool", + "item": "banPercent", + "item_value": "25", + "item_type": "int", + "Item_desc": "Percentage of shares that need to be invalid to be banned." + }, + { + "id": 9, + "module": "pool", + "item": "banThreshold", + "item_value": "30", + "item_type": "int", + "Item_desc": "Number of shares before bans can begin" + }, + { + "id": 10, + "module": "pool", + "item": "trustedMiners", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Enable the miner trust system" + }, + { + "id": 11, + "module": "pool", + "item": "trustChange", + "item_value": "1", + "item_type": "int", + "Item_desc": "Change in the miner trust in percent" + }, + { + "id": 12, + "module": "pool", + "item": "trustMin", + "item_value": "20", + "item_type": "int", + "Item_desc": "Minimum level of miner trust" + }, + { + "id": 13, + "module": "pool", + "item": "trustPenalty", + "item_value": "30", + "item_type": "int", + "Item_desc": "Number of shares that must be successful to be trusted, reset to this value if trust share is broken" + }, + { + "id": 14, + "module": "pool", + "item": "retargetTime", + "item_value": "60", + "item_type": "int", + "Item_desc": "Time between difficulty retargets" + }, + { + "id": 15, + "module": "daemon", + "item": "address", + "item_value": "127.0.0.1", + "item_type": "string", + "Item_desc": "Monero Daemon RPC IP" + }, + { + "id": 16, + "module": "daemon", + "item": "port", + "item_value": "18081", + "item_type": "int", + "Item_desc": "Monero Daemon RPC Port" + }, + { + "id": 17, + "module": "wallet", + "item": "address", + "item_value": "127.0.0.1", + "item_type": "string", + "Item_desc": "Monero Daemon RPC Wallet IP" + }, + { + "id": 18, + "module": "wallet", + "item": "port", + "item_value": "37458", + "item_type": "int", + "Item_desc": "Monero Daemon RPC Wallet Port" + }, + { + "id": 21, + "module": "rpc", + "item": "https", + "item_value": "false", + "item_type": "bool", + "Item_desc": "Enable RPC over SSL" + }, + { + "id": 22, + "module": "pool", + "item": "maxDifficulty", + "item_value": "500000", + "item_type": "int", + "Item_desc": "Maximum difficulty for VarDiff" + }, + { + "id": 23, + "module": "pool", + "item": "minDifficulty", + "item_value": "100", + "item_type": "int", + "Item_desc": "Minimum difficulty for VarDiff" + }, + { + "id": 24, + "module": "pool", + "item": "varDiffVariance", + "item_value": "20", + "item_type": "int", + "Item_desc": "Percentage out of the target time that difficulty changes" + }, + { + "id": 25, + "module": "pool", + "item": "varDiffMaxChange", + "item_value": "30", + "item_type": "int", + "Item_desc": "Percentage amount that the difficulty may change" + }, + { + "id": 27, + "module": "payout", + "item": "btcFee", + "item_value": "1.5", + "item_type": "float", + "Item_desc": "Fee charged for auto withdrawl via BTC" + }, + { + "id": 28, + "module": "payout", + "item": "ppsFee", + "item_value": "6.5", + "item_type": "float", + "Item_desc": "Fee charged for usage of the PPS pool" + }, + { + "id": 29, + "module": "payout", + "item": "pplnsFee", + "item_value": ".6", + "item_type": "float", + "Item_desc": "Fee charged for the usage of the PPLNS pool" + }, + { + "id": 30, + "module": "payout", + "item": "propFee", + "item_value": ".7", + "item_type": "float", + "Item_desc": "Fee charged for the usage of the proportial pool" + }, + { + "id": 31, + "module": "payout", + "item": "soloFee", + "item_value": ".4", + "item_type": "float", + "Item_desc": "Fee charged for usage of the solo mining pool" + }, + { + "id": 32, + "module": "payout", + "item": "exchangeMin", + "item_value": "5", + "item_type": "float", + "Item_desc": "Minimum XMR balance for payout to exchange/payment ID" + }, + { + "id": 33, + "module": "payout", + "item": "walletMin", + "item_value": ".3", + "item_type": "float", + "Item_desc": "Minimum XMR balance for payout to personal wallet" + }, + { + "id": 34, + "module": "payout", + "item": "devDonation", + "item_value": "5", + "item_type": "float", + "Item_desc": "Donation to XMR core development" + }, + { + "id": 35, + "module": "payout", + "item": "poolDevDonation", + "item_value": "0", + "item_type": "float", + "Item_desc": "Donation to pool developer" + }, + { + "id": 36, + "module": "payout", + "item": "denom", + "item_value": ".000001", + "item_type": "float", + "Item_desc": "Minimum balance that will be paid out to." + }, + { + "id": 37, + "module": "payout", + "item": "blocksRequired", + "item_value": "60", + "item_type": "int", + "Item_desc": "Blocks required to validate a payout before it's performed." + }, + { + "id": 38, + "module": "general", + "item": "sigDivisor", + "item_value": "1000000000000", + "item_type": "int", + "Item_desc": "Divisor for turning coin into human readable amounts " + }, + { + "id": 39, + "module": "payout", + "item": "feeAddress", + "item_value": "", + "item_type": "string", + "Item_desc": "Address that pool fees are sent to." + }, + { + "id": 40, + "module": "payout", + "item": "feesForTXN", + "item_value": "10", + "item_type": "int", + "Item_desc": "Amount of XMR that is left from the fees to pay miner fees." + }, + { + "id": 41, + "module": "payout", + "item": "maxTxnValue", + "item_value": "250", + "item_type": "int", + "Item_desc": "Maximum amount of XMR to send in a single transaction" + }, + { + "id": 42, + "module": "payout", + "item": "shapeshiftPair", + "item_value": "xmr_btc", + "item_type": "string", + "Item_desc": "Pair to use in all shapeshift lookups for auto BTC payout" + }, + { + "id": 43, + "module": "general", + "item": "coinCode", + "item_value": "XMR", + "item_type": "string", + "Item_desc": "Coincode to be loaded up w/ the shapeshift getcoins argument." + }, + { + "id": 44, + "module": "general", + "item": "allowBitcoin", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Allow the pool to auto-payout to BTC via ShapeShift" + }, + { + "id": 45, + "module": "payout", + "item": "exchangeRate", + "item_value": "1217200", + "item_type": "float", + "Item_desc": "Current exchange rate" + }, + { + "id": 46, + "module": "payout", + "item": "bestExchange", + "item_value": "xmrto", + "item_type": "string", + "Item_desc": "Current best exchange" + }, + { + "id": 47, + "module": "payout", + "item": "mixIn", + "item_value": "4", + "item_type": "int", + "Item_desc": "Mixin count for coins that support such things." + }, + { + "id": 48, + "module": "pool", + "item": "geoDNS", + "item_value": "", + "item_type": "string", + "Item_desc": "geoDNS enabled address for the pool." + }, + { + "id": 49, + "module": "general", + "item": "statsBufferLength", + "item_value": "120", + "item_type": "int", + "Item_desc": "Number of items to be cached in the stats buffers." + }, + { + "id": 50, + "module": "api", + "item": "authKey", + "item_value": "", + "item_type": "string", + "Item_desc": "Auth key sent with all Websocket frames for validation" + }, + { + "id": 51, + "module": "general", + "item": "mailgunKey", + "item_value": "", + "item_type": "string", + "Item_desc": "MailGun API Key for notification" + }, + { + "id": 52, + "module": "general", + "item": "mailgunURL", + "item_value": "", + "item_type": "string", + "Item_desc": "MailGun URL for notifications" + }, + { + "id": 53, + "module": "general", + "item": "emailFrom", + "item_value": "", + "item_type": "string", + "Item_desc": "From address for the notification emails" + }, + { + "id": 54, + "module": "pps", + "item": "enable", + "item_value": "false", + "item_type": "bool", + "Item_desc": "Enable PPS or not" + }, + { + "id": 55, + "module": "pplns", + "item": "shareMulti", + "item_value": "2", + "item_type": "int", + "Item_desc": "Multiply this times difficulty to set the N in PPLNS" + }, + { + "id": 56, + "module": "pplns", + "item": "shareMultiLog", + "item_value": "3", + "item_type": "int", + "Item_desc": "How many times the difficulty of the current block do we keep in shares before clearing them out" + }, + { + "id": 57, + "module": "general", + "item": "blockCleaner", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Enable the deletion of blocks or not." + }, + { + "id": 58, + "module": "api", + "item": "secKey", + "item_value": "", + "item_type": "string", + "Item_desc": "HMAC key for Passwords. JWT Secret Key" + }, + { + "id": 59, + "module": "payout", + "item": "feeSlewAmount", + "item_value": ".011", + "item_type": "float", + "Item_desc": "Amount to charge for the txn fee" + }, + { + "id": 60, + "module": "payout", + "item": "feeSlewEnd", + "item_value": "4", + "item_type": "float", + "Item_desc": "Value at which txn fee amount drops to 0" + }, + { + "id": 61, + "module": "general", + "item": "testnet", + "item_value": "false", + "item_type": "bool", + "Item_desc": "Does this pool use testnet?" + }, + { + "id": 62, + "module": "pplns", + "item": "enable", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Enable PPLNS on the pool." + }, + { + "id": 63, + "module": "solo", + "item": "enable", + "item_value": "true", + "item_type": "bool", + "Item_desc": "Enable SOLO mining on the pool" + }, + { + "id": 64, + "module": "general", + "item": "adminEmail", + "item_value": "", + "item_type": "string", + "Item_desc": "Admin e-mail to send e-mails to when something isn't working right." + }, + { + "id": 65, + "module": "payout", + "item": "rpcPasswordEnabled", + "item_value": "false", + "item_type": "bool", + "Item_desc": "Does the wallet use a RPC password?" + }, + { + "id": 66, + "module": "payout", + "item": "rpcPasswordPath", + "item_value": "", + "item_type": "string", + "Item_desc": "Path and file for the RPC password file location" + }, + { + "id": 67, + "module": "payout", + "item": "maxPaymentTxns", + "item_value": "5", + "item_type": "int", + "Item_desc": "Maximum number of transactions in a single payment" + }, + { + "id": 68, + "module": "general", + "item": "shareHost", + "item_value": "", + "item_type": "string", + "Item_desc": "Host that receives share information" + }, + { + "id": 70, + "module": "email", + "item": "workerNotHashingBody", + "item_value": "Hello,\n\nYour worker: %(worker)s has stopped submitting hashes at: %(timestamp)s UTC\n\nThank you,\n%(poolEmailSig)s", + "item_type": "string", + "Item_desc": "Email sent to the miner when their worker stops hashing" + }, + { + "id": 71, + "module": "email", + "item": "workerNotHashingSubject", + "item_value": "Worker %(worker)s stopped hashing", + "item_type": "string", + "Item_desc": "Subject of email sent to miner when worker stops hashing" + }, + { + "id": 72, + "module": "general", + "item": "emailSig", + "item_value": "NodeJS-Pool Administration Team", + "item_type": "string", + "Item_desc": "Signature line for the emails." + }, + { + "id": 73, + "module": "payout", + "item": "timer", + "item_value": "120", + "item_type": "int", + "Item_desc": "Number of minutes between main payment daemon cycles" + }, + { + "id": 74, + "module": "payout", + "item": "timerRetry", + "item_value": "25", + "item_type": "int", + "Item_desc": "Number of minutes between payment daemon retrying due to not enough funds" + }, + { + "id": 75, + "module": "payout", + "item": "priority", + "item_value": "0", + "item_type": "int", + "Item_desc": "Payout priority setting. 0 = use default (4x fee); 1 = low prio (1x fee)" + }, + { + "id": 76, + "module": "payout", + "item": "fee", + "item_value": "10000000000", + "item_type": "int", + "Item_desc": "Atomic units of coin to use ass a fee" + }, + { + "id": 77, + "module": "payout", + "item": "unlock_time", + "item_value": "0", + "item_type": "int", + "Item_desc": "Number of blocks assumed before the payout unlocks." + } +] diff --git a/sql_sync/sql_sync.js b/sql_sync/sql_sync.js new file mode 100644 index 0000000..72644bb --- /dev/null +++ b/sql_sync/sql_sync.js @@ -0,0 +1,31 @@ +"use strict"; +let mysql = require("promise-mysql"); +let fs = require("fs"); +let config = fs.readFileSync("../config.json"); +let sql_schema = fs.readFileSync("config_entries.json"); +let async = require("async"); + +global.config = JSON.parse(config); +global.mysql = mysql.createPool(global.config.mysql); +global.schema = JSON.parse(sql_schema); + +// Config Table Layout +// . + +let loopCount = 0; +let updatedCount = 0; +async.eachSeries(global.schema, function(entry, callback){ + global.mysql.query("SELECT * FROM config WHERE module = ? AND item = ?", [entry.module, entry.item]).then(function(rows){ + loopCount += 1; + if (rows.length > 0){ + return callback(); + } + updatedCount += 1; + global.mysql.query("INSERT INTO config (module, item, item_value, item_type, Item_desc) VALUES (?, ?, ?, ?, ?)", [entry.module, entry.item, entry.item_value, entry.item_type, entry.Item_desc]).then(function(){ + return callback(); + }); + }); +}, function(){ + console.log("Updated SQL schema with "+updatedCount+" new rows! Exiting!"); + process.exit(); +}); \ No newline at end of file diff --git a/tools/blocks.js b/tools/blocks.js new file mode 100644 index 0000000..1de4149 --- /dev/null +++ b/tools/blocks.js @@ -0,0 +1,44 @@ +const valid_actions = ['finder', 'stats', 'list']; +let error = 0; + +if (!global.argv.hasOwnProperty('action') || valid_actions.indexOf(global.argv.action) === -1) { + console.error("No action provided to block module."); + console.error("Valid actions: " + valid_actions.join(', ')); +} + +switch (global.argv.action) { + case 'finder': + if (!global.argv.hasOwnProperty('value')) { + console.error('No block provided in value field. Please use --value=blockID'); + error = 1; + break; + } + let blockID = parseInt(global.argv.value); + let block_data = global.database.getBlockByID(blockID); + /* + 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; + */ + if (!block_data) { + console.error("Invalid blockID provided."); + error = 1; + break; + } + console.log("Data for block: " + blockID + '\n' + + 'Hash: ' + block_data.hash + '\n' + + 'Difficulty: ' + block_data.hash + '\n' + + 'Hashes Required: ' + block_data.hash + '\n' + + 'Find Time: ' + block_data.hash + '\n' + + 'Pool Type: ' + block_data.hash + '\n' + + 'Unlocked: ' + block_data.hash + '\n' + + 'Valid: ' + block_data.hash + '\n' + + 'Value: ' + block_data.hash + '\n'); +} + +process.exit(error); \ No newline at end of file