commit 29ba08020f192f2e216ad21fda94e5fe512c881c Author: muscleman Date: Sat Jan 15 11:31:38 2022 -0600 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ab76b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +package-lock.json +node_modules/ +build/ +.idea/ +logs/ +website/ +*.swp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3103459 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Why node:8 and not node:10? Because (a) v8 is LTS, so more likely to be stable, and (b) "npm update" on node:10 breaks on Docker on Linux (but not on OSX, oddly) +FROM node:8-slim + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs-legacy npm git libboost1.55-all libssl-dev \ + && rm -rf /var/lib/apt/lists/* && \ + chmod +x /wait-for-it.sh + +ADD . /pool/ +WORKDIR /pool/ + +RUN npm update + +RUN mkdir -p /config + +EXPOSE 8117 +EXPOSE 3333 +EXPOSE 5555 +EXPOSE 7777 + +VOLUME ["/config"] + +CMD node init.js -config=/config/config.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..22fbe5d --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb193de --- /dev/null +++ b/README.md @@ -0,0 +1,800 @@ +progpowz-nodejs-pool +====================== + +High performance Node.js (with native C addons) mining pool for CryptoNote based coins. Comes with lightweight example front-end script which uses the pool's AJAX API. Support for Cryptonight (Original, Monero v7, Stellite v7), Cryptonight Light (Original, Aeon v7, IPBC) and Cryptonight Heavy (Sumokoin) algorithms. + + +#### Table of Contents +* [Features](#features) +* [Usage](#usage) + * [Requirements](#requirements) + * [Downloading & Installing](#1-downloading--installing) + * [Configuration](#2-configuration) + * [Starting the Pool](#3-start-the-pool) + * [Host the front-end](#4-host-the-front-end) + * [Customizing your website](#5-customize-your-website) + * [SSL](#ssl) + * [Upgrading](#upgrading) +* [JSON-RPC Commands from CLI](#json-rpc-commands-from-cli) +* [Monitoring Your Pool](#monitoring-your-pool) +* [Donations](#donations) +* [Credits](#credits) +* [License](#license) + + +Features +=== + +#### Optimized pool server +* TCP (stratum-like) protocol for server-push based jobs + * Compared to old HTTP protocol, this has a higher hash rate, lower network/CPU server load, lower orphan + block percent, and less error prone +* Support for Cryptonight (Original, Monero v7, Stellite v7), Cryptonight Light (Original, Aeon v7, IPBC) and Cryptonight Heavy (Sumokoin) algorithms. +* IP banning to prevent low-diff share attacks +* Socket flooding detection +* Share trust algorithm to reduce share validation hashing CPU load +* Clustering for vertical scaling +* Ability to configure multiple ports - each with their own difficulty +* Miner login (wallet address) validation +* Workers identification (specify worker name as the password) +* Variable difficulty / share limiter +* Set fixed difficulty on miner client by passing "address" param with "+[difficulty]" postfix +* Modular components for horizontal scaling (pool server, database, stats/API, payment processing, front-end) +* SSL support for both pool and API servers +* RBPPS (PROP) payment system + +#### Live statistics API +* Currency network/block difficulty +* Current block height +* Network hashrate +* Pool hashrate +* Each miners' individual stats (hashrate, shares submitted, pending balance, total paid, payout estimate, etc) +* Blocks found (pending, confirmed, and orphaned) +* Historic charts of pool's hashrate, miners count and coin difficulty +* Historic charts of users's hashrate and payments + +#### Mined blocks explorer +* Mined blocks table with block status (pending, confirmed, and orphaned) +* Blocks luck (shares/difficulty) statistics +* Universal blocks and transactions explorer based on [chainradar.com](http://chainradar.com) + +#### Smart payment processing +* Splintered transactions to deal with max transaction size +* Minimum payment threshold before balance will be paid out +* Minimum denomination for truncating payment amount precision to reduce size/complexity of block transactions +* Prevent "transaction is too big" error with "payments.maxTransactionAmount" option +* Option to enable dynamic transfer fee based on number of payees per transaction and option to have miner pay transfer fee instead of pool owner (applied to dynamic fee only) +* Control transactions priority with config.payments.priority (default: 0). +* Set payment ID on miner client when using "[address].[paymentID]" login +* Integrated payment ID addresses support for Exchanges + +#### Admin panel +* Aggregated pool statistics +* Coin daemon & wallet RPC services stability monitoring +* Log files data access +* Users list with detailed statistics + +#### Pool stability monitoring +* Detailed logging in process console & log files +* Coin daemon & wallet RPC services stability monitoring +* See logs data from admin panel + +#### Extra features +* An easily extendable, responsive, light-weight front-end using API to display data +* Onishin's [keepalive function](https://github.com/perl5577/cpuminer-multi/commit/0c8aedb) +* Support for slush mining system (disabled by default) +* E-Mail Notifications on worker connected, disconnected (timeout) or banned (support MailGun, SMTP and Sendmail) +* Telegram channel notifications when a block is unlocked +* Top 10 miners report +* Multilingual user interface + + +Usage +=== + +#### Requirements +* Coin daemon(s) (find the coin's repo and build latest version from source) + * [List of Cryptonote coins](https://github.com/muscleman/progpow1-nodejs-pool/wiki/Cryptonote-Coins) +* [Node.js](http://nodejs.org/) v11.0+ + * For Ubuntu: + ``` + curl -sL https://deb.nodesource.com/setup_11.x | sudo -E bash + sudo apt-get install -y nodejs + ``` + * Or use NVM(https://github.com/creationix/nvm) for debian/ubuntu. + + +* [Redis](http://redis.io/) key-value store v2.6+ + * For Ubuntu: +``` +sudo add-apt-repository ppa:chris-lea/redis-server +sudo apt-get update +sudo apt-get install redis-server + ``` + Dont forget to tune redis-server: + ``` +echo never > /sys/kernel/mm/transparent_hugepage/enabled +echo 1024 > /proc/sys/net/core/somaxconn + ``` + Add this lines to your /etc/rc.local and make it executable + ``` + chmod +x /etc/rc.local + ``` + +* libssl required for the node-multi-hashing module + * For Ubuntu: `sudo apt-get install libssl-dev` + +* Boost is required for the cryptoforknote-util module + * For Ubuntu: `sudo apt-get install libboost-all-dev` + + +##### Seriously +Those are legitimate requirements. If you use old versions of Node.js or Redis that may come with your system package manager then you will have problems. Follow the linked instructions to get the last stable versions. + +[**Redis warning**](http://redis.io/topics/security): It'sa good idea to learn about and understand software that +you are using - a good place to start with redis is [data persistence](http://redis.io/topics/persistence). + +**Do not run the pool as root** : create a new user without ssh access to avoid security issues : +```bash +sudo adduser --disabled-password --disabled-login your-user +``` +To login with this user : +``` +sudo su - your-user +``` + +#### 1) Downloading & Installing + + +Clone the repository and run `npm update` for all the dependencies to be installed: + +```bash +git clone https://github.com/muscleman/cryptonote-nodejs-pool.git pool +cd pool + +npm update +``` + +#### 2) Configuration + +Copy the `config_examples/COIN.json` file of your choice to `config.json` then overview each options and change any to match your preferred setup. + +Explanation for each field: +```javascript +/* Pool host displayed in notifications and front-end */ +"poolHost": "your.pool.host", + +/* Used for storage in redis so multiple coins can share the same redis instance. */ +"coin": "graft", // Must match the parentCoin variable in config.js + +/* Used for front-end display */ +"symbol": "GRFT", + +/* Minimum units in a single coin, see COIN constant in DAEMON_CODE/src/cryptonote_config.h */ +"coinUnits": 10000000000, + +/* Number of coin decimals places for notifications and front-end */ +"coinDecimalPlaces": 4, + +/* Coin network time to mine one block, see DIFFICULTY_TARGET constant in DAEMON_CODE/src/cryptonote_config.h */ +"coinDifficultyTarget": 120, + +"blockchainExplorer": "http://blockexplorer.arqma.com/block/{id}", //used on blocks page to generate hyperlinks. +"transactionExplorer": "http://blockexplorer.arqma.com/tx/{id}", //used on the payments page to generate hyperlinks + +/* Set daemon type. Supported values: default, forknote (Fix block height + 1), bytecoin (ByteCoin Wallet RPC API) */ +"daemonType": "default", + +/* Set Cryptonight algorithm settings. + Supported algorithms: cryptonight (default). cryptonight_light and cryptonight_heavy + Supported variants for "cryptonight": 0 (Original), 1 (Monero v7), 3 (Stellite / XTL) + Supported variants for "cryptonight_light": 0 (Original), 1 (Aeon v7), 2 (IPBC) + Supported blob types: 0 (Cryptonote), 1 (Forknote v1), 2 (Forknote v2), 3 (Cryptonote v2 / Masari) */ +"cnAlgorithm": "cryptonight", +"cnVariant": 1, +"cnBlobType": 0, +"includeHeight":false, /*true to include block.height in job to miner*/ +"includeAlgo":"cn/wow", /*wownero specific change to include algo in job to miner*/ "includeAlgo":"cn/wow", /*wownero specific change to include algo in job to miner*/ +"isRandomX": true, +/* Logging */ +"logging": { + + "files": { + + /* Specifies the level of log output verbosity. This level and anything + more severe will be logged. Options are: info, warn, or error. */ + "level": "info", + + /* Directory where to write log files. */ + "directory": "logs", + + /* How often (in seconds) to append/flush data to the log files. */ + "flushInterval": 5 + }, + + "console": { + "level": "info", + /* Gives console output useful colors. If you direct that output to a log file + then disable this feature to avoid nasty characters in the file. */ + "colors": true + } +}, +"childPools":[ {"poolAddress":"your wallet", + "intAddressPrefix": null, + "coin": "MCN", //must match COIN name in the child pools config.json + "childDaemon": { + "host": "127.0.0.1", + "port": 26081 + }, + "pattern": "^Vdu", //regex to identify which childcoin the miner specified in password. eg) Vdu is first 3 chars of a MCN wallet address. + "blockchainExplorer": "https://explorer.mcn.green/?hash={id}#blockchain_block", + "transactionExplorer": "https://explorer.mcn.green/?hash={id}#blockchain_transaction", + "api": "https://multi-miner.smartcoinpool.net/apiMerged1", + "enabled": true + } +] +/* Modular Pool Server */ +"poolServer": { + "enabled": true, + "mergedMining":false, + /* Set to "auto" by default which will spawn one process/fork/worker for each CPU + core in your system. Each of these workers will run a separate instance of your + pool(s), and the kernel will load balance miners using these forks. Optionally, + the 'forks' field can be a number for how many forks will be spawned. */ + "clusterForks": "auto", + + /* Address where block rewards go, and miner payments come from. */ + "poolAddress": "your wallet", + + /* This is the integrated address prefix used for miner login validation. */ + "intAddressPrefix": 91, + + /* This is the Subaddress prefix used for miner login validation. */ + "subAddressPrefix": 252, + + /* Poll RPC daemons for new blocks every this many milliseconds. */ + "blockRefreshInterval": 1000, + + /* How many seconds until we consider a miner disconnected. */ + "minerTimeout": 900, + + "sslCert": "./cert.pem", // The SSL certificate + "sslKey": "./privkey.pem", // The SSL private key + "sslCA": "./chain.pem" // The SSL certificate authority chain + + "ports": [ + { + "port": 3333, // Port for mining apps to connect to + "difficulty": 2000, // Initial difficulty miners are set to + "desc": "Low end hardware" // Description of port + }, + { + "port": 4444, + "difficulty": 15000, + "desc": "Mid range hardware" + }, + { + "port": 5555, + "difficulty": 25000, + "desc": "High end hardware" + }, + { + "port": 7777, + "difficulty": 500000, + "desc": "Cloud-mining / NiceHash" + }, + { + "port": 8888, + "difficulty": 25000, + "desc": "Hidden port", + "hidden": true // Hide this port in the front-end + }, + { + "port": 9999, + "difficulty": 20000, + "desc": "SSL connection", + "ssl": true // Enable SSL + } + ], + + /* Variable difficulty is a feature that will automatically adjust difficulty for + individual miners based on their hashrate in order to lower networking and CPU + overhead. */ + "varDiff": { + "minDiff": 100, // Minimum difficulty + "maxDiff": 100000000, + "targetTime": 60, // Try to get 1 share per this many seconds + "retargetTime": 30, // Check to see if we should retarget every this many seconds + "variancePercent": 30, // Allow time to vary this % from target without retargeting + "maxJump": 100 // Limit diff percent increase/decrease in a single retargeting + }, + + /* Set difficulty on miner client side by passing
param with + postfix */ + "fixedDiff": { + "enabled": true, + "separator": "+", // Character separator between
and + }, + + /* Set payment ID on miner client side by passing
. */ + "paymentId": { + "addressSeparator": ".", // Character separator between
and + "validation": true // Refuse login if non alphanumeric characters in + "validations": ["1,16", "64"], //regex quantity. range 1-16 characters OR exactly 64 character + "ban": true // ban the miner for invalid paymentid + }, + + /* Feature to trust share difficulties from miners which can + significantly reduce CPU load. */ + "shareTrust": { + "enabled": true, + "min": 10, // Minimum percent probability for share hashing + "stepDown": 3, // Increase trust probability % this much with each valid share + "threshold": 10, // Amount of valid shares required before trusting begins + "penalty": 30 // Upon breaking trust require this many valid share before trusting + }, + + /* If under low-diff share attack we can ban their IP to reduce system/network load. */ + "banning": { + "enabled": true, + "time": 600, // How many seconds to ban worker for + "invalidPercent": 25, // What percent of invalid shares triggers ban + "checkThreshold": 30 // Perform check when this many shares have been submitted + }, + + /* Slush Mining is a reward calculation technique which disincentivizes pool hopping and rewards 'loyal' miners by valuing younger shares higher than older shares. Remember adjusting the weight! + More about it here: https://mining.bitcoin.cz/help/#!/manual/rewards */ + "slushMining": { + "enabled": false, // Enables slush mining. Recommended for pools catering to professional miners + "weight": 300 // Defines how fast the score assigned to a share declines in time. The value should roughly be equivalent to the average round duration in seconds divided by 8. When deviating by too much numbers may get too high for JS. + } +}, + +/* Module that sends payments to miners according to their submitted shares. */ +"payments": { + "enabled": true, + "interval": 300, // How often to run in seconds + "maxAddresses": 50, // Split up payments if sending to more than this many addresses + "mixin": 5, // Number of transactions yours is indistinguishable from + "priority": 0, // The transaction priority + "transferFee": 4000000000, // Fee to pay for each transaction + "dynamicTransferFee": true, // Enable dynamic transfer fee (fee is multiplied by number of miners) + "minerPayFee" : true, // Miner pays the transfer fee instead of pool owner when using dynamic transfer fee + "minPayment": 100000000000, // Miner balance required before sending payment + "maxPayment": null, // Maximum miner balance allowed in miner settings + "maxTransactionAmount": 0, // Split transactions by this amount (to prevent "too big transaction" error) + "denomination": 10000000000 // Truncate to this precision and store remainder +}, + +/* Module that monitors the submitted block maturities and manages rounds. Confirmed + blocks mark the end of a round where workers' balances are increased in proportion + to their shares. */ +"blockUnlocker": { + "enabled": true, + "interval": 30, // How often to check block statuses in seconds + + /* Block depth required for a block to unlocked/mature. Found in daemon source as + the variable CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW */ + "depth": 60, + "poolFee": 0.8, // 0.8% pool fee (1% total fee total including donations) + "devDonation": 0.2, // 0.2% donation to send to pool dev + "networkFee": 0.0, // Network/Governance fee (used by some coins like Loki) + + /* Some forknote coins have an issue with block height in RPC request, to fix you can enable this option. + See: https://github.com/forknote/forknote-pool/issues/48 */ + "fixBlockHeightRPC": false +}, + +/* AJAX API used for front-end website. */ +"api": { + "enabled": true, + "hashrateWindow": 600, // How many second worth of shares used to estimate hash rate + "updateInterval": 3, // Gather stats and broadcast every this many seconds + "bindIp": "0.0.0.0", // Bind API to a specific IP (set to 0.0.0.0 for all) + "port": 8117, // The API port + "blocks": 30, // Amount of blocks to send at a time + "payments": 30, // Amount of payments to send at a time + "password": "your_password", // Password required for admin stats + "ssl": false, // Enable SSL API + "sslPort": 8119, // The SSL port + "sslCert": "./cert.pem", // The SSL certificate + "sslKey": "./privkey.pem", // The SSL private key + "sslCA": "./chain.pem", // The SSL certificate authority chain + "trustProxyIP": false // Proxy X-Forwarded-For support +}, + +/* Coin daemon connection details (default port is 18981) */ +"daemon": { + "host": "127.0.0.1", + "port": 18981 +}, + +/* Wallet daemon connection details (default port is 18980) */ +"wallet": { + "host": "127.0.0.1", + "port": 18982, + "password": "--rpc-password" +}, + +/* Redis connection info (default port is 6379) */ +"redis": { + "host": "127.0.0.1", + "port": 6379, + "auth": null, // If set, client will run redis auth command on connect. Use for remote db + "db": 0, // Set the REDIS database to use (default to 0) + "cleanupInterval": 15 // Set the REDIS database cleanup interval (in days) +} + +/* Pool Notifications */ +"notifications": { + "emailTemplate": "email_templates/default.txt", + "emailSubject": { + "emailAdded": "Your email was registered", + "workerConnected": "Worker %WORKER_NAME% connected", + "workerTimeout": "Worker %WORKER_NAME% stopped hashing", + "workerBanned": "Worker %WORKER_NAME% banned", + "blockFound": "Block %HEIGHT% found !", + "blockUnlocked": "Block %HEIGHT% unlocked !", + "blockOrphaned": "Block %HEIGHT% orphaned !", + "payment": "We sent you a payment !" + }, + "emailMessage": { + "emailAdded": "Your email has been registered to receive pool notifications.", + "workerConnected": "Your worker %WORKER_NAME% for address %MINER% is now connected from ip %IP%.", + "workerTimeout": "Your worker %WORKER_NAME% for address %MINER% has stopped submitting hashes on %LAST_HASH%.", + "workerBanned": "Your worker %WORKER_NAME% for address %MINER% has been banned.", + "blockFound": "Block found at height %HEIGHT% by miner %MINER% on %TIME%. Waiting maturity.", + "blockUnlocked": "Block mined at height %HEIGHT% with %REWARD% and %EFFORT% effort on %TIME%.", + "blockOrphaned": "Block orphaned at height %HEIGHT% :(", + "payment": "A payment of %AMOUNT% has been sent to %ADDRESS% wallet." + }, + "telegramMessage": { + "workerConnected": "Your worker _%WORKER_NAME%_ for address _%MINER%_ is now connected from ip _%IP%_.", + "workerTimeout": "Your worker _%WORKER_NAME%_ for address _%MINER%_ has stopped submitting hashes on _%LAST_HASH%_.", + "workerBanned": "Your worker _%WORKER_NAME%_ for address _%MINER%_ has been banned.", + "blockFound": "*Block found at height* _%HEIGHT%_ *by miner* _%MINER%_*! Waiting maturity.*", + "blockUnlocked": "*Block mined at height* _%HEIGHT%_ *with* _%REWARD%_ *and* _%EFFORT%_ *effort on* _%TIME%_*.*", + "blockOrphaned": "*Block orphaned at height* _%HEIGHT%_ *:(*", + "payment": "A payment of _%AMOUNT%_ has been sent." + } +}, + +/* Email Notifications */ +"email": { + "enabled": false, + "fromAddress": "your@email.com", // Your sender email + "transport": "sendmail", // The transport mode (sendmail, smtp or mailgun) + + // Configuration for sendmail transport + // Documentation: http://nodemailer.com/transports/sendmail/ + "sendmail": { + "path": "/usr/sbin/sendmail" // The path to sendmail command + }, + + // Configuration for SMTP transport + // Documentation: http://nodemailer.com/smtp/ + "smtp": { + "host": "smtp.example.com", // SMTP server + "port": 587, // SMTP port (25, 587 or 465) + "secure": false, // TLS (if false will upgrade with STARTTLS) + "auth": { + "user": "username", // SMTP username + "pass": "password" // SMTP password + }, + "tls": { + "rejectUnauthorized": false // Reject unauthorized TLS/SSL certificate + } + }, + + // Configuration for MailGun transport + "mailgun": { + "key": "your-private-key", // Your MailGun Private API key + "domain": "mg.yourdomain" // Your MailGun domain + } +}, + +/* Telegram channel notifications. + See Telegram documentation to setup your bot: https://core.telegram.org/bots#3-how-do-i-create-a-bot */ +"telegram": { + "enabled": false, + "botName": "", // The bot user name. + "token": "", // The bot unique authorization token + "channel": "", // The telegram channel id (ex: BlockHashMining) + "channelStats": { + "enabled": false, // Enable periodical updater of pool statistics in telegram channel + "interval": 5 // Periodical update interval (in minutes) + }, + "botCommands": { // Set the telegram bot commands + "stats": "/stats", // Pool statistics + "enable": "/enable", // Enable telegram notifications + "disable": "/disable" // Disable telegram notifications + } +}, + +/* Monitoring RPC services. Statistics will be displayed in Admin panel */ +"monitoring": { + "daemon": { + "checkInterval": 60, // Interval of sending rpcMethod request + "rpcMethod": "getblockcount" // RPC method name + }, + "wallet": { + "checkInterval": 60, + "rpcMethod": "getbalance" + } +}, + +/* Prices settings for market and price charts */ +"prices": { + "source": "cryptonator", // Exchange (supported values: cryptonator, altex, crex24, cryptopia, stocks.exchange, tradeogre, maplechange) + "currency": "USD" // Default currency +}, + +/* Collect pool statistics to display in frontend charts */ +"charts": { + "pool": { + "hashrate": { + "enabled": true, // Enable data collection and chart displaying in frontend + "updateInterval": 60, // How often to get current value + "stepInterval": 1800, // Chart step interval calculated as average of all updated values + "maximumPeriod": 86400 // Chart maximum periods (chart points number = maximumPeriod / stepInterval = 48) + }, + "miners": { + "enabled": true, + "updateInterval": 60, + "stepInterval": 1800, + "maximumPeriod": 86400 + }, + "workers": { + "enabled": true, + "updateInterval": 60, + "stepInterval": 1800, + "maximumPeriod": 86400 + }, + "difficulty": { + "enabled": true, + "updateInterval": 1800, + "stepInterval": 10800, + "maximumPeriod": 604800 + }, + "price": { + "enabled": true, + "updateInterval": 1800, + "stepInterval": 10800, + "maximumPeriod": 604800 + }, + "profit": { + "enabled": true, + "updateInterval": 1800, + "stepInterval": 10800, + "maximumPeriod": 604800 + } + + }, + "user": { // Chart data displayed in user stats block + "hashrate": { + "enabled": true, + "updateInterval": 180, + "stepInterval": 1800, + "maximumPeriod": 86400 + }, + "worker_hashrate": { + "enabled": true, + "updateInterval": 60, + "stepInterval": 60, + "maximumPeriod": 86400 + }, + "payments": { // Payment chart uses all user payments data stored in DB + "enabled": true + } + }, + "blocks": { + "enabled": true, + "days": 30 // Number of days displayed in chart (if value is 1, display last 24 hours) + } +} +``` + +#### 3) Start the pool + +```bash +node init.js +``` + +The file `config.json` is used by default but a file can be specified using the `-config=file` command argument, for example: + +```bash +node init.js -config=config_backup.json +``` + +This software contains four distinct modules: +* `pool` - Which opens ports for miners to connect and processes shares +* `api` - Used by the website to display network, pool and miners' data +* `unlocker` - Processes block candidates and increases miners' balances when blocks are unlocked +* `payments` - Sends out payments to miners according to their balances stored in redis +* `chartsDataCollector` - Processes miners and workers hashrate stats and charts +* `telegramBot` - Processes telegram bot commands + + +By default, running the `init.js` script will start up all four modules. You can optionally have the script start +only start a specific module by using the `-module=name` command argument, for example: + +```bash +node init.js -module=api +``` + +[Example screenshot](http://i.imgur.com/SEgrI3b.png) of running the pool in single module mode with tmux. + +To keep your pool up, on operating system with systemd, you can create add your pool software as a service. +Use this [example](https://github.com/muscleman/cryptonote-nodejs-pool/blob/master/deployment/cryptonote-nodejs-pool.service) to create the systemd service `/lib/systemd/system/cryptonote-nodejs-pool.service` +Then enable and start the service with the following commands : + +``` +sudo systemctl enable cryptonote-nodejs-pool.service +sudo systemctl start cryptonote-nodejs-pool.service +``` + +#### 4) Host the front-end + +Simply host the contents of the `website_example` directory on file server capable of serving simple static files. + + +Edit the variables in the `website_example/config.js` file to use your pool's specific configuration. +Variable explanations: + +```javascript + +/* Must point to the API setup in your config.json file. */ +var api = "http://poolhost:8117"; + +/* Pool server host to instruct your miners to point to (override daemon setting if set) */ +var poolHost = "poolhost.com"; + +/* Number of coin decimals places (override daemon setting if set) */ +"coinDecimalPlaces": 4, + +/* Contact email address. */ +var email = "support@poolhost.com"; + +/* Pool Telegram URL. */ +var telegram = "https://t.me/YourPool"; + +/* Pool Discord URL */ +var discord = "https://discordapp.com/invite/YourPool"; + +/*Pool Facebook URL */ +var facebook = "https://www.facebook.com/ + + + + + + BlockHashMining.com + + + + + + + + + + +
+ + + + + diff --git a/email_templates/default.txt b/email_templates/default.txt new file mode 100644 index 0000000..9195dd1 --- /dev/null +++ b/email_templates/default.txt @@ -0,0 +1,6 @@ +Hello, + +%MESSAGE% + +Thank you, +Cheers, the allmighty BlockMaster at %POOL_HOST% \ No newline at end of file diff --git a/init.js b/init.js new file mode 100644 index 0000000..b2dbfe9 --- /dev/null +++ b/init.js @@ -0,0 +1,390 @@ + /** + * Cryptonite Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Pool initialization script + **/ + +// Load needed modules +var fs = require('fs'); +var cluster = require('cluster'); +var os = require('os'); + +// Load configuration +require('./lib/configReader.js'); + +// Load log system +require('./lib/logger.js'); + +// Initialize redis database client +var redis = require('redis'); + +var redisDB = (config.redis.db && config.redis.db > 0) ? config.redis.db : 0; +global.redisClient = redis.createClient(config.redis.port, config.redis.host, { db: redisDB, auth_pass: config.redis.auth }); + +if ((typeof config.poolServer.mergedMining !== 'undefined' && config.poolServer.mergedMining) && typeof config.childPools !== 'undefined') + config.childPools = config.childPools.filter(pool => pool.enabled); +else + config.childPools = []; + +// Load pool modules +if (cluster.isWorker){ + switch(process.env.workerType){ + case 'pool': + require('./lib/pool.js'); + break; + case 'daemon': + require('./lib/daemon.js') + break + case 'childDaemon': + require('./lib/childDaemon.js') + break + case 'blockUnlocker': + require('./lib/blockUnlocker.js'); + break; + case 'paymentProcessor': + require('./lib/paymentProcessor.js'); + break; + case 'api': + require('./lib/api.js'); + break; + case 'chartsDataCollector': + require('./lib/chartsDataCollector.js'); + break; + case 'telegramBot': + require('./lib/telegramBot.js'); + break; + } + return; +} + +// Initialize log system +var logSystem = 'master'; +require('./lib/exceptionWriter.js')(logSystem); + +// Pool informations +log('info', logSystem, 'Starting Cryptonote Node.JS pool version %s', [version]); + +// Developer donations +if (devFee < 0.2) + log('info', logSystem, 'Developer donation \(devDonation\) is set to %d\%, Please consider raising it to 0.2\% or higher !!!', [devFee]); + +// Run a single module ? +var singleModule = (function(){ + var validModules = ['pool', 'api', 'unlocker', 'payments', 'chartsDataCollector', 'telegramBot']; + + for (var i = 0; i < process.argv.length; i++){ + if (process.argv[i].indexOf('-module=') === 0){ + var moduleName = process.argv[i].split('=')[1]; + if (validModules.indexOf(moduleName) > -1) + return moduleName; + + log('error', logSystem, 'Invalid module "%s", valid modules: %s', [moduleName, validModules.join(', ')]); + process.exit(); + } + } +})(); + +/** + * Start modules + **/ +(function init(){ + checkRedisVersion(function(){ + if (singleModule){ + log('info', logSystem, 'Running in single module mode: %s', [singleModule]); + + switch(singleModule){ + case 'daemon': + spawnDaemon() + break + case 'pool': + spawnPoolWorkers(); + break; + case 'unlocker': + spawnBlockUnlocker(); + break; + case 'payments': + spawnPaymentProcessor(); + break; + case 'api': + spawnApi(); + break; + case 'chartsDataCollector': + spawnChartsDataCollector(); + break; + case 'telegramBot': + spawnTelegramBot(); + break; + } + } + else{ + spawnPoolWorkers(); + spawnDaemon(); + if (config.poolServer.mergedMining) + spawnChildDaemons(); + spawnBlockUnlocker(); + spawnPaymentProcessor(); + spawnApi(); + spawnChartsDataCollector(); + spawnTelegramBot(); + } + }); +})(); + +/** + * Check redis database version + **/ +function checkRedisVersion(callback){ + redisClient.info(function(error, response){ + if (error){ + log('error', logSystem, 'Redis version check failed'); + return; + } + var parts = response.split('\r\n'); + var version; + var versionString; + for (var i = 0; i < parts.length; i++){ + if (parts[i].indexOf(':') !== -1){ + var valParts = parts[i].split(':'); + if (valParts[0] === 'redis_version'){ + versionString = valParts[1]; + version = parseFloat(versionString); + break; + } + } + } + if (!version){ + log('error', logSystem, 'Could not detect redis version - must be super old or broken'); + return; + } + else if (version < 2.6){ + log('error', logSystem, "You're using redis version %s the minimum required version is 2.6. Follow the damn usage instructions...", [versionString]); + return; + } + callback(); + }); +} + +/** + * Spawn pool workers module + **/ +function spawnPoolWorkers(){ + if (!config.poolServer || !config.poolServer.enabled || !config.poolServer.ports || config.poolServer.ports.length === 0) return; + + if (config.poolServer.ports.length === 0){ + log('error', logSystem, 'Pool server enabled but no ports specified'); + return; + } + var numForks = (function(){ + if (!config.poolServer.clusterForks) + return 1; + if (config.poolServer.clusterForks === 'auto') + return os.cpus().length; + if (isNaN(config.poolServer.clusterForks)) + return 1; + return config.poolServer.clusterForks; + })(); + + var poolWorkers = {}; + + var createPoolWorker = function(forkId){ + var worker = cluster.fork({ + workerType: 'pool', + forkId: forkId + }); + worker.forkId = forkId; + worker.type = 'pool'; + poolWorkers[forkId] = worker; + worker.on('exit', function(code, signal){ + log('error', logSystem, 'Pool fork %s died, spawning replacement worker...', [forkId]); + setTimeout(function(){ + createPoolWorker(forkId); + }, 2000); + }).on('message', function(msg){ + switch(msg.type){ + case 'banIP': + Object.keys(cluster.workers).forEach(function(id) { + if (cluster.workers[id].type === 'pool'){ + cluster.workers[id].send({type: 'banIP', ip: msg.ip}); + } + }); + break; + } + }); + }; + + var i = 1; + var spawnInterval = setInterval(function(){ + createPoolWorker(i.toString()); + i++; + if (i - 1 === numForks){ + clearInterval(spawnInterval); + log('info', logSystem, 'Pool spawned on %d thread(s)', [numForks]); + } + }, 10); +} + +/** + * Spawn pool workers module + **/ +function spawnChildDaemons(){ + if (!config.poolServer || !config.poolServer.enabled || !config.poolServer.ports || config.poolServer.ports.length === 0) return; + + if (config.poolServer.ports.length === 0){ + log('error', logSystem, 'Pool server enabled but no ports specified'); + return; + } + + let numForks = config.childPools.length; + if (numForks === 0) return; + var daemonWorkers = {}; + + var createDaemonWorker = function(poolId){ + var worker = cluster.fork({ + workerType: 'childDaemon', + poolId: poolId + }); + worker.poolId = poolId; + worker.type = 'childDaemon'; + daemonWorkers[poolId] = worker; + worker.on('exit', function(code, signal){ + log('error', logSystem, 'Child Daemon fork %s died, spawning replacement worker...', [poolId]); + setTimeout(function(){ + createDaemonWorker(poolId); + }, 2000); + }).on('message', function(msg){ + switch(msg.type){ + case 'ChildBlockTemplate': + Object.keys(cluster.workers).forEach(function(id) { + if (cluster.workers[id].type === 'pool'){ + cluster.workers[id].send({type: 'ChildBlockTemplate', block: msg.block, poolIndex: msg.poolIndex}); + } + }); + break; + } + }); + }; + + var i = 0; + var spawnInterval = setInterval(function(){ + createDaemonWorker(i.toString()) + i++ + if (i === numForks){ + clearInterval(spawnInterval); + log('info', logSystem, 'Child Daemon spawned on %d thread(s)', [numForks]); + } + }, 10); +} + + +/** + * Spawn daemon module + **/ +function spawnDaemon(){ + if (!config.poolServer || !config.poolServer.enabled || !config.poolServer.ports || config.poolServer.ports.length === 0) return; + + var worker = cluster.fork({ + workerType: 'daemon' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'Daemon died, spawning replacement...'); + setTimeout(function(){ + spawnDaemon(); + }, 10); + }).on('message', function(msg){ + switch(msg.type){ + case 'BlockTemplate': + Object.keys(cluster.workers).forEach(function(id) { + if (cluster.workers[id].type === 'pool'){ + cluster.workers[id].send({type: 'BlockTemplate', block: msg.block}); + } + }); + break; + } + }); +} + +/** + * Spawn block unlocker module + **/ +function spawnBlockUnlocker(){ + if (!config.blockUnlocker || !config.blockUnlocker.enabled) return; + + var worker = cluster.fork({ + workerType: 'blockUnlocker' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'Block unlocker died, spawning replacement...'); + setTimeout(function(){ + spawnBlockUnlocker(); + }, 2000); + }); +} + +/** + * Spawn payment processor module + **/ +function spawnPaymentProcessor(){ + if (!config.payments || !config.payments.enabled) return; + + var worker = cluster.fork({ + workerType: 'paymentProcessor' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'Payment processor died, spawning replacement...'); + setTimeout(function(){ + spawnPaymentProcessor(); + }, 2000); + }); +} + +/** + * Spawn API module + **/ +function spawnApi(){ + if (!config.api || !config.api.enabled) return; + + var worker = cluster.fork({ + workerType: 'api' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'API died, spawning replacement...'); + setTimeout(function(){ + spawnApi(); + }, 2000); + }); +} + +/** + * Spawn charts data collector module + **/ +function spawnChartsDataCollector(){ + if (!config.charts) return; + + var worker = cluster.fork({ + workerType: 'chartsDataCollector' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'chartsDataCollector died, spawning replacement...'); + setTimeout(function(){ + spawnChartsDataCollector(); + }, 2000); + }); +} + +/** + * Spawn telegram bot module + **/ +function spawnTelegramBot(){ + if (!config.telegram || !config.telegram.enabled || !config.telegram.token) return; + + var worker = cluster.fork({ + workerType: 'telegramBot' + }); + worker.on('exit', function(code, signal){ + log('error', logSystem, 'telegramBot died, spawning replacement...'); + setTimeout(function(){ + spawnTelegramBot(); + }, 2000); + }); +} diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..307f4c5 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,2147 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Pool API + **/ + +// Load required modules +let fs = require('fs'); +let http = require('http'); +let https = require('https'); +let url = require("url"); +let async = require('async'); + +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet); +let authSid = Math.round(Math.random() * 10000000000) + '' + Math.round(Math.random() * 10000000000); + +let charts = require('./charts.js'); +let notifications = require('./notifications.js'); +let market = require('./market.js'); +let utils = require('./utils.js'); + +// Initialize log system +let logSystem = 'api'; +require('./exceptionWriter.js')(logSystem); + +// Data storage variables used for live statistics +let currentStats = {}; +let minerStats = {}; +let minersHashrate = {}; + +let liveConnections = {}; +let addressConnections = {}; + +/** + * Handle server requests + **/ +function handleServerRequest(request, response) { + let urlParts = url.parse(request.url, true); + + switch(urlParts.pathname){ + // Pool statistics + case '/stats': + handleStats(urlParts, request, response); + break; + case '/live_stats': + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Connection': 'keep-alive' + }); + + let address = urlParts.query.address ? urlParts.query.address : 'undefined'; + let uid = Math.random().toString(); + let key = address + ':' + uid; + + response.on("finish", function() { + delete liveConnections[key]; + }); + response.on("close", function() { + delete liveConnections[key]; + }); + + liveConnections[key] = response; + break; + + // Worker statistics + case '/stats_address': + handleMinerStats(urlParts, response); + break; + + // Payments + case '/get_payments': + handleGetPayments(urlParts, response); + break; + + // Blocks + case '/get_blocks': + handleGetBlocks(urlParts, response); + break; + + // Get market prices + case '/get_market': + handleGetMarket(urlParts, response); + break; + + // Top 10 miners + case '/get_top10miners': + handleTopMiners(response); + break; + + // Miner settings + case '/get_miner_payout_level': + handleGetMinerPayoutLevel(urlParts, response); + break; + case '/set_miner_payout_level': + handleSetMinerPayoutLevel(urlParts, response); + break; + case '/get_email_notifications': + handleGetMinerNotifications(urlParts, response); + break; + case '/set_email_notifications': + handleSetMinerNotifications(urlParts, response); + break; + case '/get_telegram_notifications': + handleGetTelegramNotifications(urlParts, response); + break; + case '/set_telegram_notifications': + handleSetTelegramNotifications(urlParts, response); + break; + case '/block_explorers': + handleBlockExplorers(response) + break + case '/get_apis': + handleGetApis(response) + break + // Miners/workers hashrate (used for charts) + case '/miners_hashrate': + if (!authorize(request, response)) { + return; + } + handleGetMinersHashrate(response); + break; + case '/workers_hashrate': + if (!authorize(request, response)) { + return; + } + handleGetWorkersHashrate(response); + break; + + // Pool Administration + case '/admin_stats': + if (!authorize(request, response)) + return; + handleAdminStats(response); + break; + case '/admin_monitoring': + if (!authorize(request, response)) { + return; + } + handleAdminMonitoring(response); + break; + case '/admin_log': + if (!authorize(request, response)) { + return; + } + handleAdminLog(urlParts, response); + break; + case '/admin_users': + if (!authorize(request, response)) { + return; + } + handleAdminUsers(request, response); + break; + case '/admin_ports': + if (!authorize(request, response)) { + return; + } + handleAdminPorts(request, response); + break; + + // Test notifications + case '/test_email_notification': + if (!authorize(request, response)) { + return; + } + handleTestEmailNotification(urlParts, response); + break; + case '/test_telegram_notification': + if (!authorize(request, response)) { + return; + } + handleTestTelegramNotification(urlParts, response); + break; + + // Default response + default: + response.writeHead(404, { + 'Access-Control-Allow-Origin': '*' + }); + response.end('Invalid API call'); + break; + } +} + +/** + * Collect statistics data + **/ +function collectStats(){ + let startTime = Date.now(); + let redisFinished; + let daemonFinished; + + let redisCommands = [ + ['zremrangebyscore', `${config.coin}:hashrate`, '-inf', ''], + ['zrange', `${config.coin}:hashrate`, 0, -1], + ['hgetall', `${config.coin}:stats`], + ['zrange', `${config.coin}:blocks:candidates`, 0, -1, 'WITHSCORES'], + ['zrevrange', `${config.coin}:blocks:matured`, 0, config.api.blocks - 1, 'WITHSCORES'], + ['hgetall', `${config.coin}:scores:prop:roundCurrent`], + ['hgetall', `${config.coin}:stats`], + // ['zcard', `${config.coin}:blocks:matured`], + ['zrevrange', `${config.coin}:payments:all`, 0, config.api.payments - 1, 'WITHSCORES'], + ['zcard', `${config.coin}:payments:all`], + ['keys', `${config.coin}:payments:*`], + ['hgetall', `${config.coin}:shares_actual:prop:roundCurrent`], + ['zrange', `${config.coin}:blocks:matured`, 0, -1, 'WITHSCORES'] + ]; + + let windowTime = (((Date.now() / 1000) - config.api.hashrateWindow) | 0).toString(); + redisCommands[0][3] = '(' + windowTime; + + async.parallel({ + health: function(callback) { + let keys = [] + let data = {} + keys.push(config.coin) + let healthCommands = [] + healthCommands.push(['hget', `${config.coin}:status:daemon`, 'lastStatus']) + healthCommands.push(['hget', `${config.coin}:status:wallet`, 'lastStatus']) + healthCommands.push(['hget', `${config.coin}:status:price`, 'lastReponse']) +/* + config.childPools.forEach(pool => { + healthCommands.push(['hmget', `${pool.coin}:status:daemon`, 'lastStatus']) + healthCommands.push(['hmget', `${pool.coin}:status:wallet`, 'lastStatus']) + keys.push(pool.coin) + + }) +*/ + redisClient.multi(healthCommands).exec(function(error, replies){ + + if (error){ + data = {daemon: 'fail', wallet: 'fail', price: 'fail'} + callback(null, data) + } + for (var i = 0, index = 0; i < keys.length; index+=2, i++) { + data[keys[i]] = {daemon: replies[index], wallet: replies[index+1], price: replies[index+2]} + + } + callback(null, data) + }) + }, + pool: function(callback){ + redisClient.multi(redisCommands).exec(function(error, replies){ + redisFinished = Date.now(); + let dateNowSeconds = Date.now() / 1000 | 0; + + if (error){ + log('error', logSystem, 'Error getting redis data %j', [error]); + callback(true); + return; + } + + let data = { + stats: replies[2], + blocks: truncateMinerAddress(replies[3].concat(replies[4])), + totalBlocks: 0, + totalBlocksSolo: 0, + totalDiff: 0, + totalDiffSolo: 0, + totalShares: 0, + totalSharesSolo: 0, + payments: replies[7], + totalPayments: parseInt(replies[8]), + totalMinersPaid: replies[9] && replies[9].length > 0 ? replies[9].length - 1 : 0, + miners: 0, + minersSolo: 0, + workers: 0, + workersSolo: 0, + hashrate: 0, + hashrateSolo: 0, + roundScore: 0, + roundHashes: 0 + }; + + calculateBlockData(data, replies[3].concat(replies[11])) + + minerStats = {}; + + minersHashrate = {}; + minersHashrateSolo = {}; + + minersHashrate = {}; + minersRewardType = {} + + let hashrates = replies[1]; + for (let i = 0; i < hashrates.length; i++){ + let hashParts = hashrates[i].split(':'); + minersHashrate[hashParts[1]] = (minersHashrate[hashParts[1]] || 0) + parseInt(hashParts[0]); + minersRewardType[hashParts[1]] = hashParts[3] + } + + let totalShares = 0 + let totalSharesSolo = 0 + + for (let miner in minersHashrate){ + if (minersRewardType[miner] === 'prop') { + if (miner.indexOf('~') !== -1) { + data.workers ++; + totalShares += minersHashrate[miner]; + } else { + data.miners ++; + } + } else if (minersRewardType[miner] === 'solo') { + if (miner.indexOf('~') !== -1) { + data.workersSolo ++; + totalSharesSolo += minersHashrate[miner]; + } else { + data.minersSolo ++; + } + } + minersHashrate[miner] = Math.round(minersHashrate[miner] / config.api.hashrateWindow); + if (!minerStats[miner]) { minerStats[miner] = {}; } + minerStats[miner]['hashrate'] = minersHashrate[miner]; + } + + + data.hashrate = Math.round(totalShares / config.api.hashrateWindow); + data.hashrateSolo = Math.round(totalSharesSolo / config.api.hashrateWindow); + + data.roundScore = 0; + + if (replies[5]){ + for (let miner in replies[5]){ + let roundScore = parseFloat(replies[5][miner]); + + data.roundScore += roundScore; + + if (!minerStats[miner]) { minerStats[miner] = {}; } + minerStats[miner]['roundScore'] = roundScore; + } + } + + data.roundHashes = 0; + + if (replies[10]){ + for (let miner in replies[10]){ + let roundHashes = parseInt(replies[10][miner]) + data.roundHashes += roundHashes; + + if (!minerStats[miner]) { minerStats[miner] = {}; } + minerStats[miner]['roundHashes'] = roundHashes; + } + } + + if (replies[6]) { + if (!replies[6].lastBlockFound || parseInt(replies[6].lastBlockFound) < parseInt(replies[6].lastBlockFoundprop)) + data.lastBlockFound = replies[6].lastBlockFoundprop; + else + data.lastBlockFound = replies[6].lastBlockFound + + if (replies[6].lastBlockFoundsolo) + data.lastBlockFoundSolo = replies[6].lastBlockFoundsolo; + } + + callback(null, data); + }); + }, + lastblock: function(callback){ + getLastBlockData(function(error, data) { + daemonFinished = Date.now(); + callback(error, data); + }); + }, + network: function(callback){ + getNetworkData(function(error, data) { + daemonFinished = Date.now(); + callback(error, data); + }); + }, + config: function(callback){ + callback(null, { + poolHost: config.poolHost || '', + ports: getPublicPorts(config.poolServer.ports), + cnAlgorithm: config.cnAlgorithm || 'cryptonight', + cnVariant: config.cnVariant || 0, + cnBlobType: config.cnBlobType || 0, + hashrateWindow: config.api.hashrateWindow, + fee: config.blockUnlocker.poolFee, + networkFee: config.blockUnlocker.networkFee || 0, + coin: config.coin, + coinUnits: config.coinUnits, + coinDecimalPlaces: config.coinDecimalPlaces || 4, // config.coinUnits.toString().length - 1, + coinDifficultyTarget: config.coinDifficultyTarget, + symbol: config.symbol, + depth: config.blockUnlocker.depth, + donation: donations, + version: version, + paymentsInterval: config.payments.interval, + minPaymentThreshold: config.payments.minPayment, + maxPaymentThreshold: config.payments.maxPayment || null, + transferFee: config.payments.transferFee, + denominationUnit: config.payments.denomination, + slushMiningEnabled: config.poolServer.slushMining.enabled, + weight: config.poolServer.slushMining.weight, + priceSource: config.prices ? config.prices.source : 'cryptonator', + priceCurrency: config.prices ? config.prices.currency : 'USD', + paymentIdSeparator: config.poolServer.paymentId && config.poolServer.paymentId.addressSeparator ? config.poolServer.paymentId.addressSeparator : ".", + fixedDiffEnabled: config.poolServer.fixedDiff.enabled, + fixedDiffSeparator: config.poolServer.fixedDiff.addressSeparator, + sendEmails: config.email ? config.email.enabled : false, + blocksChartEnabled: (config.charts.blocks && config.charts.blocks.enabled), + blocksChartDays: config.charts.blocks && config.charts.blocks.days ? config.charts.blocks.days : null, + telegramBotName: config.telegram && config.telegram.botName ? config.telegram.botName : null, + telegramBotStats: config.telegram && config.telegram.botCommands ? config.telegram.botCommands.stats : "/stats", + telegramBotReport: config.telegram && config.telegram.botCommands ? config.telegram.botCommands.report : "/report", + telegramBotNotify: config.telegram && config.telegram.botCommands ? config.telegram.botCommands.notify : "/notify", + telegramBotBlocks: config.telegram && config.telegram.botCommands ? config.telegram.botCommands.blocks : "/blocks" + }); + }, + charts: function (callback) { + // Get enabled charts data + charts.getPoolChartsData(function(error, data) { + if (error) { + callback(error, data); + return; + } + + // Blocks chart + if (!config.charts.blocks || !config.charts.blocks.enabled || !config.charts.blocks.days) { + callback(error, data); + return; + } + + let chartDays = config.charts.blocks.days; + + let beginAtTimestamp = (Date.now() / 1000) - (chartDays * 86400); + let beginAtDate = new Date(beginAtTimestamp * 1000); + if (chartDays > 1) { + beginAtDate = new Date(beginAtDate.getFullYear(), beginAtDate.getMonth(), beginAtDate.getDate(), 0, 0, 0, 0); + beginAtTimestamp = beginAtDate / 1000 | 0; + } + + let blocksCount = {}; + let blocksCountSolo = {}; + if (chartDays === 1) { + for (let h = 0; h <= 24; h++) { + let date = utils.dateFormat(new Date((beginAtTimestamp + (h * 60 * 60)) * 1000), 'yyyy-mm-dd HH:00'); + blocksCount[date] = 0; + blocksCountSolo[date] = 0 + } + } else { + for (let d = 0; d <= chartDays; d++) { + let date = utils.dateFormat(new Date((beginAtTimestamp + (d * 86400)) * 1000), 'yyyy-mm-dd'); + blocksCount[date] = 0; + blocksCountSolo[date] = 0 + } + } + + redisClient.zrevrange(config.coin + ':blocks:matured', 0, -1, 'WITHSCORES', function(err, result) { + for (let i = 0; i < result.length; i++){ + let block = result[i].split(':'); + if (block[0] === 'prop' || block[0] === 'solo') { + let blockTimestamp = block[3]; + if (blockTimestamp < beginAtTimestamp) { + continue; + } + let date = utils.dateFormat(new Date(blockTimestamp * 1000), 'yyyy-mm-dd'); + if (chartDays === 1) utils.dateFormat(new Date(blockTimestamp * 1000), 'yyyy-mm-dd HH:00'); + if (block[0] === 'prop'){ + if (!blocksCount[date]) blocksCount[date] = 0; + blocksCount[date] ++; + continue + } + if (block[0] === 'solo') { + if (!blocksCountSolo[date]) blocksCountSolo[date] = 0; + blocksCountSolo[date] ++; + } + } + else + { + if (block[5]) { + let blockTimestamp = block[1]; + if (blockTimestamp < beginAtTimestamp) { + continue; + } + let date = utils.dateFormat(new Date(blockTimestamp * 1000), 'yyyy-mm-dd'); + if (chartDays === 1) utils.dateFormat(new Date(blockTimestamp * 1000), 'yyyy-mm-dd HH:00'); + if (!blocksCount[date]) blocksCount[date] = 0; + blocksCount[date] ++; + } + } + } + data.blocks = blocksCount; + data.blocksSolo = blocksCountSolo; + callback(error, data); + }); + }); + } + }, function(error, results){ + log('info', logSystem, 'Stat collection finished: %d ms redis, %d ms daemon', [redisFinished - startTime, daemonFinished - startTime]); + + if (error){ + log('error', logSystem, 'Error collecting all stats'); + } + else{ + currentStats = results; + broadcastLiveStats(); + broadcastFinished = Date.now(); + log('info', logSystem, 'Stat collection broadcastLiveStats: %d ms', [broadcastFinished - startTime]); + } + + setTimeout(collectStats, config.api.updateInterval * 1000); + }); + +} + +function truncateMinerAddress(blocks) { + for (let i = 0; i < blocks.length; i++){ + let block = blocks[i].split(':'); + if (block[0] === 'solo' || block[0] === 'prop') { + block[1] = `${block[1].substring(0,7)}...${block[1].substring(block[1].length-7)}` + blocks[i] = block.join(':') + } + } + return blocks +} + +/** +* Calculate the Diff, shares and totalblocks +**/ +function calculateBlockData(data, blocks) { + for (let i = 0; i < blocks.length; i++){ + let block = blocks[i].split(':'); + if ( block[0] === 'solo') { + data.totalDiffSolo += parseInt(block[4]) + data.totalSharesSolo += parseInt(block[5]) + data.totalBlocksSolo += 1 + } + else if (block[0] === 'prop') + { + data.totalDiff += parseInt(block[4]) + data.totalShares += parseInt(block[5]) + data.totalBlocks += 1 + } + else { + if (block[5]) { + data.totalDiff += parseInt(block[2]) + data.totalShares += parseInt(block[3]) + data.totalBlocks += 1 + } + } + } +} + +/** + * Get Network data + **/ +let networkDataRpcMode = 'getinfo'; +function getNetworkData(callback, rpcMode) { + if (!rpcMode) rpcMode = networkDataRpcMode; + + // Try get_info RPC method first if available (not all coins support it) + if (rpcMode === 'getinfo') { + apiInterfaces.rpcDaemon('getinfo', {}, function(error, reply){ + if (error) { + getNetworkData(callback, 'getlastblockheader'); + return; + } else { + networkDataRpcMode = 'getinfo'; + callback(null, { + difficulty: reply.pow_difficulty, + height: reply.height + }); + } + }); + } + + // Else fallback to getlastblockheader + else { + apiInterfaces.rpcDaemon('getlastblockheader', {}, function(error, reply){ + if (error){ + log('error', logSystem, 'Error getting network data %j', [error]); + callback(true); + return; + } else { + networkDataRpcMode = 'getlastblockheader'; + + let blockHeader = reply.block_header; + callback(null, { + difficulty: blockHeader.difficulty, + height: blockHeader.height + 1 + }); + } + }); + } +} + +/** + * Get Last Block data + **/ +function getLastBlockData(callback) { + apiInterfaces.rpcDaemon('getlastblockheader', {}, function(error, reply){ + if (error){ + log('error', logSystem, 'Error getting last block data %j', [error]); + callback(true); + return; + } + let blockHeader = reply.block_header; +/* + if (config.blockUnlocker.useFirstVout) { + apiInterfaces.rpcDaemon('getblock', { height: blockHeader.height }, function(error, result) { + if (error) { + log('error', logSystem, 'Error getting last block details: %j', [error]); + callback(true); + return; + } + let vout = JSON.parse(result.json).miner_tx.vout; + if (!vout.length) { + log('error', logSystem, 'Error: tx at height %s has no vouts!', [blockHeader.height]); + callback(true); + return; + } + callback(null, { + difficulty: blockHeader.difficulty, + height: blockHeader.height, + timestamp: blockHeader.timestamp, + reward: vout[0].amount, + hash: blockHeader.hash + }); + }); + return; + } +*/ + callback(null, { + difficulty: blockHeader.difficulty, + height: blockHeader.height, + timestamp: blockHeader.timestamp, + reward: config.reward, + hash: blockHeader.hash + }); + }); +} + +function handleGetApis(callback) { + let apis = {} + config.childPools.forEach(pool => { + if (pool.enabled) + apis[pool.coin] = {api: pool.api} + }) + callback(apis) +} + +/** + * Broadcast live statistics + **/ +function broadcastLiveStats(){ + log('info', logSystem, 'Broadcasting to %d visitors and %d address lookups', [Object.keys(liveConnections).length, Object.keys(addressConnections).length]); + + // Live statistics + let processAddresses = {}; + for (let key in liveConnections){ + let addrOffset = key.indexOf(':'); + let address = key.substr(0, addrOffset); + if (!processAddresses[address]) processAddresses[address] = []; + processAddresses[address].push(liveConnections[key]); + } + + for (let address in processAddresses) { + let data = currentStats; + + data.miner = {}; + if (address && minerStats[address]){ + data.miner = minerStats[address]; + } + + let destinations = processAddresses[address]; + sendLiveStats(data, destinations); + } + + // Workers Statistics + processAddresses = {}; + for (let key in addressConnections){ + let addrOffset = key.indexOf(':'); + let address = key.substr(0, addrOffset); + if (!processAddresses[address]) processAddresses[address] = []; + processAddresses[address].push(addressConnections[key]); + } + + for (let address in processAddresses) { + broadcastWorkerStats(address, processAddresses[address]); + } +} + +/** + * Takes a chart data JSON string and uses it to compute the average over the past hour, 6 hours, + * and 24 hours. Returns [AVG1, AVG6, AVG24]. + **/ +function extractAverageHashrates(chartdata) { + let now = new Date() / 1000 | 0; + + let sums = [0, 0, 0]; // 1h, 6h, 24h + let counts = [0, 0, 0]; + + let sets = chartdata ? JSON.parse(chartdata) : []; // [time, avgValue, updateCount] + for (let j in sets) { + let hr = sets[j][1]; + if (now - sets[j][0] <= 1*60*60) { sums[0] += hr; counts[0]++; } + if (now - sets[j][0] <= 6*60*60) { sums[1] += hr; counts[1]++; } + if (now - sets[j][0] <= 24*60*60) { sums[2] += hr; counts[2]++; } + } + + return [sums[0] * 1.0 / (counts[0] || 1), sums[1] * 1.0 / (counts[1] || 1), sums[2] * 1.0 / (counts[2] || 1)]; +} + +/** + * Broadcast worker statistics + **/ +function broadcastWorkerStats(address, destinations) { + let redisCommands = [ + ['hgetall', `${config.coin}:workers:${address}`], + ['zrevrange', `${config.coin}:payments:${address}`, 0, config.api.payments - 1, 'WITHSCORES'], + ['keys', `${config.coin}:unique_workers:${address}~*`], + ['get', `${config.coin}:charts:hashrate:${address}`] + ]; + redisClient.multi(redisCommands).exec(function(error, replies){ + if (error || !replies || !replies[0]){ + sendLiveStats({error: 'Not found'}, destinations); + return; + } + + let stats = replies[0]; + stats.hashrate = minerStats[address] && minerStats[address]['hashrate'] ? minerStats[address]['hashrate'] : 0; + stats.roundScore = minerStats[address] && minerStats[address]['roundScore'] ? minerStats[address]['roundScore'] : 0; + stats.roundHashes = minerStats[address] && minerStats[address]['roundHashes'] ? minerStats[address]['roundHashes'] : 0; + if (replies[3]) { + let hr_avg = extractAverageHashrates(replies[3]); + stats.hashrate_1h = hr_avg[0]; + stats.hashrate_6h = hr_avg[1]; + stats.hashrate_24h = hr_avg[2]; + } + + let paymentsData = replies[1]; + + let workersData = []; + for (let j=0; j { + if (pool.enabled) + apis[pool.coin] = {api: pool.api} + }) + callback(null, apis) + } + ], function(error, data) { + if(error) { + response.end(JSON.stringify({error: 'Error collecting Api Information'})); + return; + } + let reply = JSON.stringify(data); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(reply, 'utf8') + }); + response.end(reply); + }) +} + +/** + * Return top 10 miners + **/ +function handleBlockExplorers(response) { + async.waterfall([ + function(callback) { + let blockExplorers = {} + blockExplorers[config.coin] = {"blockchainExplorer": config.blockchainExplorer, "transactionExplorer": config.transactionExplorer} + config.childPools.forEach(pool => { + if (pool.enabled) + blockExplorers[pool.coin] = {"blockchainExplorer": pool.blockchainExplorer, "transactionExplorer": pool.transactionExplorer} + }) + callback(null, blockExplorers) + } + ], function(error, data) { + if(error) { + response.end(JSON.stringify({error: 'Error collecting Block Explorer Information'})); + return; + } + let reply = JSON.stringify(data); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(reply, 'utf8') + }); + response.end(reply); + }) +} + +/** + * Return top 10 miners + **/ +function handleTopMiners(response) { + async.waterfall([ + function(callback) { + redisClient.keys(`${config.coin}:workers:*`, callback); + }, + function(workerKeys, callback) { + let redisCommands = workerKeys.map(function(k) { + return ['hmget', k, 'lastShare', 'hashes']; + }); + redisClient.multi(redisCommands).exec(function(error, redisData) { + let minersData = []; + let keyParts = []; + let address = ''; + let data = ''; + for (let i in redisData) { + keyParts = workerKeys[i].split(':'); + address = keyParts[keyParts.length-1]; + data = redisData[i]; + minersData.push({ + miner: address.substring(0,7)+'...'+address.substring(address.length-7), + hashrate: minersHashrate[address] && minerStats[address]['hashrate'] ? minersHashrate[address] : 0, + lastShare: data[0], + hashes: data[1] + }); + } + callback(null, minersData); + }); + } + ], function(error, data) { + if(error) { + response.end(JSON.stringify({error: 'Error collecting top 10 miners stats'})); + return; + } + + data.sort(compareTopMiners); + data = data.slice(0,10); + + let reply = JSON.stringify(data); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(reply, 'utf8') + }); + response.end(reply); + }); +} + +function compareTopMiners(a,b) { + let v1 = a.hashrate ? parseInt(a.hashrate) : 0; + let v2 = b.hashrate ? parseInt(b.hashrate) : 0; + if (v1 > v2) return -1; + if (v1 < v2) return 1; + return 0; +} + +/** + * Miner settings: minimum payout level + **/ + +// Get current minimum payout level +function handleGetMinerPayoutLevel(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let address = urlParts.query.address; + + // Check the minimal required parameters for this handle. + if (address === undefined) { + response.end(JSON.stringify({status: 'Parameters are incomplete'})); + return; + } + + // Return current miner payout level + redisClient.hget(`${config.coin}:workers:${address}`, 'minPayoutLevel', function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to get the current minimum payout level from database'})); + return; + } + + let minLevel = config.payments.minPayment / config.coinUnits; + if (minLevel < 0) minLevel = 0; + + let maxLevel = config.payments.maxPayment ? config.payments.maxPayment / config.coinUnits : null; + + let currentLevel = value / config.coinUnits; + if (currentLevel < minLevel) currentLevel = minLevel; + if (maxLevel && currentLevel > maxLevel) currentLevel = maxLevel; + + response.end(JSON.stringify({status: 'done', level: currentLevel})); + }); +} + +// Set minimum payout level +function handleSetMinerPayoutLevel(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let address = urlParts.query.address; + let ip = urlParts.query.ip; + let level = urlParts.query.level; + + // Check the minimal required parameters for this handle. + if (ip === undefined || address === undefined || level === undefined) { + response.end(JSON.stringify({status: 'Parameters are incomplete'})); + return; + } + + // Do not allow wildcards in the queries. + if (ip.indexOf('*') !== -1 || address.indexOf('*') !== -1) { + response.end(JSON.stringify({status: 'Remove the wildcard from your miner address'})); + return; + } + + level = parseFloat(level); + if (isNaN(level)) { + response.end(JSON.stringify({status: 'Your minimum payout level doesn\'t look like a number'})); + return; + } + + let minLevel = config.payments.minPayment / config.coinUnits; + if (minLevel < 0) minLevel = 0; + + let maxLevel = config.payments.maxPayment ? config.payments.maxPayment / config.coinUnits : null; + + if (level < minLevel) { + response.end(JSON.stringify({status: 'The minimum payout level is ' + minLevel})); + return; + } + + if (maxLevel && level > maxLevel) { + response.end(JSON.stringify({status: 'The maximum payout level is ' + maxLevel})); + return; + } + + // Only do a modification if we have seen the IP address in combination with the wallet address. + minerSeenWithIPForAddress(address, ip, function (error, found) { + if (!found || error) { + response.end(JSON.stringify({status: 'We haven\'t seen that IP for that wallet address in our record'})); + return; + } + + let payoutLevel = level * config.coinUnits; + redisClient.hset(config.coin + ':workers:' + address, 'minPayoutLevel', payoutLevel, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'An error occurred when updating the value in our database'})); + return; + } + + log('info', logSystem, 'Updated minimum payout level for ' + address + ' to: ' + payoutLevel); + response.end(JSON.stringify({status: 'done'})); + }); + }); +} + +/** + * Miner settings: email notifications + **/ + +// Get destination for email notifications +function handleGetMinerNotifications(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let address = urlParts.query.address; + + // Check the minimal required parameters for this handle. + if (address === undefined) { + response.end(JSON.stringify({status: 'Parameters are incomplete'})); + return; + } + + // Return current email for notifications + redisClient.hget(`${config.coin}:notifications`, address, function(error, value){ + if (error){ + response.end(JSON.stringify({'status': 'Unable to get current email from database'})); + return; + } + response.end(JSON.stringify({'status': 'done', 'email': value})); + }); +} + +// Set email notifications +function handleSetMinerNotifications(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let email = urlParts.query.email; + let address = urlParts.query.address; + let ip = urlParts.query.ip; + let action = urlParts.query.action; + + // Check the minimal required parameters for this handle. + if (ip === undefined || address === undefined || action === undefined) { + response.end(JSON.stringify({status: 'Parameters are incomplete'})); + return; + } + + // Do not allow wildcards in the queries. + if (ip.indexOf('*') !== -1 || address.indexOf('*') !== -1) { + response.end(JSON.stringify({status: 'Remove the wildcard from your input'})); + return; + } + + // Check the action + if (action === undefined || action === '' || (action != 'enable' && action != 'disable')) { + response.end(JSON.stringify({status: 'Invalid action'})); + return; + } + + // Now only do a modification if we have seen the IP address in combination with the wallet address. + minerSeenWithIPForAddress(address, ip, function (error, found) { + if (!found || error) { + response.end(JSON.stringify({status: 'We haven\'t seen that IP for your address'})); + return; + } + + if (action === "enable") { + if (email === undefined) { + response.end(JSON.stringify({status: 'No email address specified'})); + return; + } + redisClient.hset(config.coin + ':notifications', address, email, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to add email address in database'})); + return; + } + + log('info', logSystem, 'Enable email notifications to ' + email + ' for address: ' + address); + notifications.sendToMiner(address, 'emailAdded', { + 'ADDRESS': address, + 'EMAIL': email + }); + }); + response.end(JSON.stringify({status: 'done'})); + } + else if (action === "disable") { + redisClient.hdel(config.coin + ':notifications', address, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to remove email address from database'})); + return; + } + log('info', logSystem, 'Disabled email notifications for address: ' + address); + }); + response.end(JSON.stringify({status: 'done'})); + } + }); +} + +/** + * Miner settings: telegram notifications + **/ + +// Get destination for telegram notifications +function handleGetTelegramNotifications(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let chatId = urlParts.query.chatId; + let address = urlParts.query.address; + let type = urlParts.query.type || 'miner'; + + if (chatId === undefined || chatId === '') { + response.end(JSON.stringify({status: 'No chat id specified'})); + return; + } + + // Default miner address + if (type == 'default') { + redisClient.hget(config.coin + ':telegram:default', chatId, function(error, value){ + if (error){ + response.end(JSON.stringify({'status': 'Unable to get current telegram default miner address from database'})); + return; + } + response.end(JSON.stringify({'status': 'done', 'address': value})); + }); + } + + // Blocks notification + if (type === 'blocks') { + redisClient.hget(config.coin + ':telegram:blocks', chatId, function(error, value){ + if (error){ + response.end(JSON.stringify({'status': 'Unable to get current telegram chat id from database'})); + return; + } + response.end(JSON.stringify({'status': 'done', 'enabled': +value})); + }); + } + + // Miner notification + if (type === 'miner') { + if (address === undefined || address === '') { + response.end(JSON.stringify({status: 'No miner address specified'})); + return; + } + + redisClient.hget(config.coin + ':telegram', address, function(error, value){ + if (error){ + response.end(JSON.stringify({'status': 'Unable to get current telegram chat id from database'})); + return; + } + response.end(JSON.stringify({'status': 'done', 'chatId': value})); + }); + } +} + +// Enable/disable telegram notifications +function handleSetTelegramNotifications(urlParts, response){ + response.writeHead(200, { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + response.write('\n'); + + let chatId = urlParts.query.chatId; + let type = urlParts.query.type || 'miner'; + let action = urlParts.query.action; + let address = urlParts.query.address; + + // Check chat id + if (chatId === undefined || chatId === '') { + response.end(JSON.stringify({status: 'No chat id specified'})); + return; + } + + // Check action + if (type !== 'default' && (action === undefined || action === '' || (action != 'enable' && action != 'disable'))) { + response.end(JSON.stringify({status: 'Invalid action'})); + return; + } + + // Default miner address + if (type == 'default') { + if (address === undefined || address === '') { + response.end(JSON.stringify({status: 'No miner address specified'})); + return; + } + + redisClient.hset(config.coin + ':telegram:default', chatId, address, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to set default telegram miner address'})); + return; + } + }); + response.end(JSON.stringify({status: 'done'})); + } + + // Blocks notification + if (type === 'blocks') { + // Enable + if (action === "enable") { + redisClient.hset(config.coin + ':telegram:blocks', chatId, 1, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to enable telegram notifications'})); + return; + } + + log('info', logSystem, 'Enabled telegram notifications for blocks to ' + chatId); + }); + response.end(JSON.stringify({status: 'done'})); + } + + // Disable + else if (action === "disable") { + redisClient.hdel(config.coin + ':telegram:blocks', chatId, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to disable telegram notifications'})); + return; + } + log('info', logSystem, 'Disabled telegram notifications for blocks to ' + chatId); + }); + response.end(JSON.stringify({status: 'done'})); + } + } + + // Miner notification + if (type === 'miner') { + if (address === undefined || address === '') { + response.end(JSON.stringify({status: 'No miner address specified'})); + return; + } + + redisClient.exists(config.coin + ':workers:' + address, function(error, result){ + if (!result){ + response.end(JSON.stringify({status: 'Miner not found in database'})); + return; + } + + // Enable + if (action === "enable") { + redisClient.hset(config.coin + ':telegram', address, chatId, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to enable telegram notifications'})); + return; + } + log('info', logSystem, 'Enabled telegram notifications to ' + chatId + ' for address: ' + address); + }); + response.end(JSON.stringify({status: 'done'})); + } + + // Disable + else if (action === "disable") { + redisClient.hdel(config.coin + ':telegram', address, function(error, value){ + if (error){ + response.end(JSON.stringify({status: 'Unable to disable telegram notifications'})); + return; + } + log('info', logSystem, 'Disabled telegram notifications for address: ' + address); + }); + response.end(JSON.stringify({status: 'done'})); + } + }); + } +} + +/** + * Return miners hashrate + **/ +function handleGetMinersHashrate(response) { + let data = {}; + for (let miner in minersHashrate){ + if (miner.indexOf('~') !== -1) continue; + data[miner] = minersHashrate[miner]; + } + + data = { + minersHashrate: data + } + + let reply = JSON.stringify(data); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(reply, 'utf8') + }); + response.end(reply); +} + +/** + * Return workers hashrate + **/ +function handleGetWorkersHashrate(response) { + let data = {}; + for (let miner in minersHashrate){ + if (miner.indexOf('~') === -1) continue; + data[miner] = minersHashrate[miner]; + } + let reply = JSON.stringify({ + workersHashrate: data + }); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': reply.length + }); + response.end(reply); +} + + +/** + * Authorize access to a secured API call + **/ +function authorize(request, response){ + let sentPass = url.parse(request.url, true).query.password; + + let remoteAddress = request.connection.remoteAddress; + if(config.api.trustProxyIP && request.headers['x-forwarded-for']){ + remoteAddress = request.headers['x-forwarded-for']; + } + + let bindIp = config.api.bindIp ? config.api.bindIp : "0.0.0.0"; + if (typeof sentPass == "undefined" && (remoteAddress === '127.0.0.1' || remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '::1' || (bindIp != "0.0.0.0" && remoteAddress === bindIp))) { + return true; + } + + response.setHeader('Access-Control-Allow-Origin', '*'); + + let cookies = parseCookies(request); + if (typeof sentPass == "undefined" && cookies.sid && cookies.sid === authSid) { + return true; + } + + if (sentPass !== config.api.password){ + response.statusCode = 401; + response.end('Invalid password'); + return; + } + + log('warn', logSystem, 'Admin authorized from %s', [remoteAddress]); + response.statusCode = 200; + + let cookieExpire = new Date( new Date().getTime() + 60*60*24*1000); + response.setHeader('Set-Cookie', 'sid=' + authSid + '; path=/; expires=' + cookieExpire.toUTCString()); + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Content-Type', 'application/json'); + + return true; +} + +/** + * Administration: return pool statistics + **/ +function handleAdminStats(response){ + async.waterfall([ + + //Get worker keys & unlocked blocks + function(callback){ + redisClient.multi([ + ['keys', `${config.coin}:workers:*`], + ['zrange', `${config.coin}:blocks:matured`, 0, -1] + ]).exec(function(error, replies) { + if (error) { + log('error', logSystem, 'Error trying to get admin data from redis %j', [error]); + callback(true); + return; + } + callback(null, replies[0], replies[1]); + }); + }, + + //Get worker balances + function(workerKeys, blocks, callback){ + let redisCommands = workerKeys.map(function(k){ + return ['hmget', k, 'balance', 'paid']; + }); + redisClient.multi(redisCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with getting balances from redis %j', [error]); + callback(true); + return; + } + + callback(null, replies, blocks); + }); + }, + function(workerData, blocks, callback){ + let stats = { + totalOwed: 0, + totalPaid: 0, + totalRevenue: 0, + totalRevenueSolo: 0, + totalDiff: 0, + totalDiffSolo: 0, + totalShares: 0, + totalSharesSolo: 0, + blocksOrphaned: 0, + blocksUnlocked: 0, + blocksUnlockedSolo: 0, + totalWorkers: 0 + }; + + for (let i = 0; i < workerData.length; i++){ + stats.totalOwed += parseInt(workerData[i][0]) || 0; + stats.totalPaid += parseInt(workerData[i][1]) || 0; + stats.totalWorkers++; + } + + for (let i = 0; i < blocks.length; i++){ + let block = blocks[i].split(':'); + if (block[0] === 'prop' || block[0] === 'solo') { + if(block[7]){ + if (block[0] === 'solo') { + stats.blocksUnlockedSolo++ + stats.totalDiffSolo += parseInt(block[4]) + stats.totalSharesSolo += parseInt(block[5]) + stats.totalRevenueSolo += parseInt(block[7]) + } + else + { + stats.blocksUnlocked++ + stats.totalDiff += parseInt(block[4]) + stats.totalShares += parseInt(block[5]) + stats.totalRevenue += parseInt(block[7]) + } + } + else { + stats.blocksOrphaned++ + } + } + else { + if (block[5]) { + stats.blocksUnlocked++; + stats.totalDiff += parseInt(block[2]); + stats.totalShares += parseInt(block[3]); + stats.totalRevenue += parseInt(block[5]); + } + else{ + stats.blocksOrphaned++; + } + } + } + callback(null, stats); + } + ], function(error, stats){ + if (error){ + response.end(JSON.stringify({error: 'Error collecting stats'})); + return; + } + response.end(JSON.stringify(stats)); + } + ); + +} + +/** + * Administration: users list + **/ +function handleAdminUsers(request, response){ + let otherCoin = url.parse(request.url, true).query.otherCoin; + async.waterfall([ + // get workers Redis keys + function(callback) { + redisClient.keys(`${config.coin}:workers:*`, callback); + }, + + // get workers data + function(workerKeys, callback) { + let allCoins = config.childPools + .filter(pool => pool.enabled) + .map(pool => { + return `${pool.coin}` + }) + + allCoins.push(otherCoin) + + let redisCommands = workerKeys.map(function(k) { + return ['hmget', k, 'balance', 'paid', 'lastShare', 'hashes', ...allCoins]; + }); + redisClient.multi(redisCommands).exec(function(error, redisData) { + let workersData = {} + let keyParts = [] + let address = '' + let data = [] + let wallet = '' + let coin = null + for(let i in redisData) { + keyParts = workerKeys[i].split(':'); + address = keyParts[keyParts.length-1]; + data = redisData[i]; + + for (let a = 0, b = 4; b <= data.length; a++, b++) { + if (data[b]) { + coin = `${allCoins[a]}=${data[b]}` + break + } + } + + workersData[address] = { + pending: data[0], + paid: data[1], + lastShare: data[2], + hashes: data[3], + childWallet: coin, + hashrate: minerStats[address] && minerStats[address]['hashrate'] ? minerStats[address]['hashrate'] : 0, + roundScore: minerStats[address] && minerStats[address]['roundScore'] ? minerStats[address]['roundScore'] : 0, + roundHashes: minerStats[address] && minerStats[address]['roundHashes'] ? minerStats[address]['roundHashes'] : 0 + }; + } + callback(null, workersData); + }); + } + ], function(error, workersData) { + if(error) { + response.end(JSON.stringify({error: 'Error collecting users stats'})); + return; + } + response.end(JSON.stringify(workersData)); + } + ); +} + +/** + * Administration: pool monitoring + **/ +function handleAdminMonitoring(response) { + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json' + }); + async.parallel({ + monitoring: getMonitoringData, + logs: getLogFiles + }, function(error, result) { + response.end(JSON.stringify(result)); + }); +} + +/** + * Administration: log file data + **/ +function handleAdminLog(urlParts, response){ + let file = urlParts.query.file; + let filePath = config.logging.files.directory + '/' + file; + if(!file.match(/^\w+\.log$/)) { + response.end('wrong log file'); + } + response.writeHead(200, { + 'Content-Type': 'text/plain', + 'Cache-Control': 'no-cache', + 'Content-Length': fs.statSync(filePath).size + }); + fs.createReadStream(filePath).pipe(response); +} + +/** + * Administration: pool ports usage + **/ +function handleAdminPorts(request, response){ + async.waterfall([ + function(callback) { + redisClient.keys(`${config.coin}:ports:*`, callback); + }, + function(portsKeys, callback) { + let redisCommands = portsKeys.map(function(k) { + return ['hmget', k, 'port', 'users']; + }); + redisClient.multi(redisCommands).exec(function(error, redisData) { + let portsData = {} + let port = '' + let data = [] + for (let i in redisData) { + port = portsKeys[i]; + + data = redisData[i]; + portsData[port] = { + port: data[0], + users: data[1] + }; + } + callback(null, portsData); + }); + } + ], function(error, portsData) { + if(error) { + response.end(JSON.stringify({error: 'Error collecting Ports stats'})); + return; + } + response.end(JSON.stringify(portsData)); + }); +} + +/** + * Administration: test email notification + **/ +function handleTestEmailNotification(urlParts, response) { + let email = urlParts.query.email; + if (!config.email) { + response.end(JSON.stringify({status: 'Email system is not configured'})); + return; + } + if (!config.email.enabled) { + response.end(JSON.stringify({status: 'Email system is not enabled'})); + return; + } + if (!email) { + response.end(JSON.stringify({status: 'No email specified'})); + return; + } + log('info', logSystem, 'Sending test e-mail notification to %s', [email]); + notifications.sendToEmail(email, 'test', {}); + response.end(JSON.stringify({status: 'done'})); +} + +/** + * Administration: test telegram notification + **/ +function handleTestTelegramNotification(urlParts, response) { + if (!config.telegram) { + response.end(JSON.stringify({status: 'Telegram is not configured'})); + return; + } + if (!config.telegram.enabled) { + response.end(JSON.stringify({status: 'Telegram is not enabled'})); + return; + } + if (!config.telegram.token) { + response.end(JSON.stringify({status: 'No telegram bot token specified in configuration'})); + return; + } + if (!config.telegram.channel) { + response.end(JSON.stringify({status: 'No telegram channel specified in configuration'})); + return; + } + log('info', logSystem, 'Sending test telegram channel notification'); + notifications.sendToTelegramChannel('test', {}); + response.end(JSON.stringify({status: 'done'})); +} + +/** + * RPC monitoring of daemon and wallet + **/ + +// Start RPC monitoring +function startRpcMonitoring(rpc, module, method, interval) { + setInterval(function() { + rpc(method, {}, function(error, response) { + let stat = { + lastCheck: new Date() / 1000 | 0, + lastStatus: error ? 'fail' : 'ok', + lastResponse: JSON.stringify(error ? error : response) + }; + if(error) { + stat.lastFail = stat.lastCheck; + stat.lastFailResponse = stat.lastResponse; + } + let key = getMonitoringDataKey(module); + let redisCommands = []; + for(let property in stat) { + redisCommands.push(['hset', key, property, stat[property]]); + } + redisClient.multi(redisCommands).exec(); + }); + }, interval * 1000); +} + +// function startPriceMonitoring(rpc, module, method, endPoint, interval, coin) { +// setInterval(function() { +// let tickers = ['ARQ-BTC', 'ARQ-LTC', 'ARQ-USD', 'ARQ-EUR', 'ARQ-CAD'] +// let exchange = config.prices.source; +// market.get(exchange, tickers, function(data) { +// redisClient.set(`${config.coin}:status:prices`, JSON.stringify(data)) +// }); +// }, interval * 1000); +// } + +// Return monitoring data key +function getMonitoringDataKey(module) { + return config.coin + ':status:' + module; +} + +// Initialize monitoring +function initMonitoring() { + let modulesRpc = { + daemon: apiInterfaces.rpcDaemon, + wallet: apiInterfaces.rpcWallet, + price: apiInterfaces.jsonHttpRequest + }; + let daemonType = config.daemonType ? config.daemonType.toLowerCase() : "default"; + let settings = ''; + for(let module in config.monitoring) { + settings = config.monitoring[module]; + // if (module === "price") { + // startPriceMonitoring(modulesRpc[module], module, settings.rpcMethod, settings.checkInterval, settings.tickers ) + // break + // } + if (daemonType === "bytecoin" && module === "wallet" && settings.rpcMethod === "getbalance") { + settings.rpcMethod = "getBalance"; + } + if(settings.checkInterval) { + startRpcMonitoring(modulesRpc[module], module, settings.rpcMethod, settings.checkInterval); + } + } +} + +// Get monitoring data +function getMonitoringData(callback) { + let modules = Object.keys(config.monitoring); + let redisCommands = []; + for(let i in modules) { + redisCommands.push(['hgetall', getMonitoringDataKey(modules[i])]) + } + redisClient.multi(redisCommands).exec(function(error, results) { + let stats = {}; + for(let i in modules) { + if(results[i]) { + stats[modules[i]] = results[i]; + } + } + callback(error, stats); + }); +} + +/** + * Return pool public ports + **/ +function getPublicPorts(ports){ + return ports.filter(function(port) { + return !port.hidden; + }); +} + +/** + * Return list of pool logs file + **/ +function getLogFiles(callback) { + let dir = config.logging.files.directory; + fs.readdir(dir, function(error, files) { + let logs = {}; + let file = '' + let stats = ''; + for(let i in files) { + file = files[i]; + stats = fs.statSync(dir + '/' + file); + logs[file] = { + size: stats.size, + changed: Date.parse(stats.mtime) / 1000 | 0 + } + } + callback(error, logs); + }); +} + +/** + * Check if a miner has been seen with specified IP address + **/ +function minerSeenWithIPForAddress(address, ip, callback) { + let ipv4_regex = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/; + if (ipv4_regex.test(ip)) { + ip = '::ffff:' + ip; + } + redisClient.sismember([`${config.coin}:workers_ip:${address}`, ip], function(error, result) { + let found = result > 0 ? true : false; + callback(error, found); + }); +} + +/** + * Parse cookies data + **/ +function parseCookies(request) { + let list = {}, + rc = request.headers.cookie; + rc && rc.split(';').forEach(function(cookie) { + let parts = cookie.split('='); + list[parts.shift().trim()] = unescape(parts.join('=')); + }); + return list; +} +/** + * Start pool API + **/ + +// Collect statistics for the first time +collectStats(); + +// Initialize RPC monitoring +initMonitoring(); + +// Enable to be bind to a certain ip or all by default +let bindIp = config.api.bindIp ? config.api.bindIp : "0.0.0.0"; + +// Start API on HTTP port +let server = http.createServer(function(request, response){ + if (request.method.toUpperCase() === "OPTIONS"){ + response.writeHead("204", "No Content", { + "access-control-allow-origin": '*', + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "content-type, accept", + "access-control-max-age": 10, // Seconds. + "content-length": 0 + }); + return(response.end()); + } + + handleServerRequest(request, response); +}); + +server.listen(config.api.port, bindIp, function(){ + log('info', logSystem, 'API started & listening on %s port %d', [bindIp, config.api.port]); +}); + +// Start API on SSL port +if (config.api.ssl){ + if (!config.api.sslCert) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL certificate not configured', [bindIp, config.api.sslPort]); + } else if (!config.api.sslKey) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL key not configured', [bindIp, config.api.sslPort]); + } else if (!config.api.sslCA) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL certificate authority not configured', [bindIp, config.api.sslPort]); + } else if (!fs.existsSync(config.api.sslCert)) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL certificate file not found (configuration error)', [bindIp, config.api.sslPort]); + } else if (!fs.existsSync(config.api.sslKey)) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL key file not found (configuration error)', [bindIp, config.api.sslPort]); + } else if (!fs.existsSync(config.api.sslCA)) { + log('error', logSystem, 'Could not start API listening on %s port %d (SSL): SSL certificate authority file not found (configuration error)', [bindIp, config.api.sslPort]); + } else { + let options = { + key: fs.readFileSync(config.api.sslKey), + cert: fs.readFileSync(config.api.sslCert), + ca: fs.readFileSync(config.api.sslCA), + honorCipherOrder: true + }; + + let ssl_server = https.createServer(options, function(request, response){ + if (request.method.toUpperCase() === "OPTIONS"){ + response.writeHead("204", "No Content", { + "access-control-allow-origin": '*', + "access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS", + "access-control-allow-headers": "content-type, accept", + "access-control-max-age": 10, // Seconds. + "content-length": 0, + "strict-transport-security": "max-age=604800" + }); + return(response.end()); + } + + handleServerRequest(request, response); + }); + + ssl_server.listen(config.api.sslPort, bindIp, function(){ + log('info', logSystem, 'API started & listening on %s port %d (SSL)', [bindIp, config.api.sslPort]); + }); + } +} diff --git a/lib/apiInterfaces.js b/lib/apiInterfaces.js new file mode 100644 index 0000000..c1ca07b --- /dev/null +++ b/lib/apiInterfaces.js @@ -0,0 +1,121 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Handle communications to APIs + **/ + +// Load required modules +var http = require('http'); +var https = require('https'); + +function jsonHttpRequest(host, port, data, callback, path){ + path = path || '/json_rpc'; + callback = callback || function(){}; + var options = { + hostname: host, + port: port, + path: path, + method: data ? 'POST' : 'GET', + headers: { + 'Content-Length': data.length, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }; + var req = (port === 443 ? https : http).request(options, function(res){ + var replyData = ''; + res.setEncoding('utf8'); + res.on('data', function(chunk){ + replyData += chunk; + }); + res.on('end', function(){ + var replyJson; + try{ + replyJson = replyData ? JSON.parse(replyData) : {}; + } + catch(e){ + callback(e, {}); + return; + } + callback(null, replyJson); + }); + }); + + req.on('error', function(e){ + callback(e, {}); + }); + + req.end(data); +} + +/** + * Send RPC request + **/ +function rpc(host, port, method, params, callback){ + var data = JSON.stringify({ + id: "0", + jsonrpc: "2.0", + method: method, + params: params + }); + jsonHttpRequest(host, port, data, function(error, replyJson){ + if (error){ + callback(error, {}); + return; + } + callback(replyJson.error, replyJson.result) + }); +} + +/** + * Send RPC requests in batch mode + **/ +function batchRpc(host, port, array, callback){ + var rpcArray = []; + for (var i = 0; i < array.length; i++){ + rpcArray.push({ + id: i.toString(), + jsonrpc: "2.0", + method: array[i][0], + params: array[i][1] + }); + } + var data = JSON.stringify(rpcArray); + jsonHttpRequest(host, port, data, callback); +} + +/** + * Send RPC request to pool API + **/ +function poolRpc(host, port, path, callback){ + jsonHttpRequest(host, port, '', callback, path); +} + +/** + * Exports API interfaces functions + **/ +module.exports = function(daemonConfig, walletConfig, poolApiConfig){ + return { + batchRpcDaemon: function(batchArray, callback){ + batchRpc(daemonConfig.host, daemonConfig.port, batchArray, callback); + }, + rpcDaemon: function(method, params, callback, serverConfig){ + if (serverConfig) { + rpc(serverConfig.host, serverConfig.port, method, params, callback); + } else { + rpc(daemonConfig.host, daemonConfig.port, method, params, callback); + } + }, + rpcWallet: function(method, params, callback){ + rpc(walletConfig.host, walletConfig.port, method, params, callback); + }, + pool: function(path, callback){ + var bindIp = config.api.bindIp ? config.api.bindIp : "0.0.0.0"; + var poolApi = (bindIp !== "0.0.0.0" ? poolApiConfig.bindIp : "127.0.0.1"); + poolRpc(poolApi, poolApiConfig.port, path, callback); + }, + jsonHttpRequest: jsonHttpRequest + } +}; + diff --git a/lib/blockUnlocker.js b/lib/blockUnlocker.js new file mode 100644 index 0000000..5a0f52d --- /dev/null +++ b/lib/blockUnlocker.js @@ -0,0 +1,295 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Block unlocker + **/ + +// Load required modules +let async = require('async'); + +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api) +let notifications = require('./notifications.js') +let utils = require('./utils.js') + +let slushMiningEnabled = config.poolServer.slushMining && config.poolServer.slushMining.enabled + +// Initialize log system +let logSystem = 'unlocker' +require('./exceptionWriter.js')(logSystem) + +/** + * Run block unlocker + **/ + +log('info', logSystem, 'Started') + +function runInterval(){ + async.waterfall([ + + // Get all block candidates in redis + function(callback){ + redisClient.zrange(config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES', function(error, results){ + if (error){ + log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]) + callback(true) + return + } + if (results.length === 0){ + log('info', logSystem, 'No blocks candidates in redis') + callback(true) + return + } + + let blocks = [] + + for (let i = 0; i < results.length; i += 2){ + let parts = results[i].split(':') + blocks.push({ + serialized: results[i], + height: parseInt(results[i + 1]), + rewardType: parts[0], + login: parts[1], + hash: parts[2], + time: parts[3], + difficulty: parts[4], + shares: parts[5], + score: parts.length >= 7 ? parts[6] : parts[5] + }) + } + + callback(null, blocks) + }) + }, + + // Check if blocks are orphaned + function(blocks, callback){ + async.filter(blocks, function(block, mapCback){ + let daemonType = config.daemonType ? config.daemonType.toLowerCase() : "default" + let blockHeight = ((daemonType === "forknote" || daemonType === "bytecoin") && config.blockUnlocker.fixBlockHeightRPC) ? block.height + 1 : block.height + let rpcMethod = config.blockUnlocker.useFirstVout ? 'getblock' : 'getblockheaderbyheight' + apiInterfaces.rpcDaemon(rpcMethod, {height: blockHeight}, function(error, result){ + if (error){ + log('error', logSystem, 'Error with %s RPC request for block %s - %j', [rpcMethod, block.serialized, error]) + block.unlocked = false + mapCback() + return + } + if (!result.block_header){ + log('error', logSystem, 'Error with getblockheaderbyheight RPC request for block %s - %j', [block.serialized, error]) + block.unlocked = false + mapCback() + return + } + let blockHeader = result.block_header + block.orphaned = blockHeader.hash === block.hash ? 0 : 1 + block.unlocked = blockHeader.depth >= config.blockUnlocker.depth + block.reward = blockHeader.reward + if (config.blockUnlocker.useFirstVout) { + let vout = JSON.parse(result.json).miner_tx.vout + if (!vout.length) { + log('error', logSystem, 'Error: tx at height %s has no vouts!', [blockHeight]) + block.unlocked = false + mapCback() + return + } + block.reward = vout[0].amount + } else { + block.reward = blockHeader.reward + } + if (config.blockUnlocker.networkFee) { + let networkFeePercent = config.blockUnlocker.networkFee / 100 + block.reward = block.reward - (block.reward * networkFeePercent) + } + mapCback(block.unlocked) + }) + }, function(unlockedBlocks){ + + if (unlockedBlocks.length === 0){ + log('info', logSystem, 'No pending blocks are unlocked yet (%d pending)', [blocks.length]) + callback(true) + return + } + + callback(null, unlockedBlocks) + }) + }, + + // Get worker shares for each unlocked block + function(blocks, callback){ + + let redisCommands = blocks.map(function(block){ + if (block.rewardType === 'prop') + return ['hgetall', config.coin + ':scores:prop:round' + block.height] + else + return ['hgetall', config.coin + ':scores:solo:round' + block.height] + }) + + redisClient.multi(redisCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with getting round shares from redis %j', [error]) + callback(true) + return + } + for (let i = 0; i < replies.length; i++){ + let workerScores = replies[i] + blocks[i].workerScores = workerScores + } + callback(null, blocks) + }) + }, + + // Handle orphaned blocks + function(blocks, callback){ + let orphanCommands = [] + + blocks.forEach(function(block){ + if (!block.orphaned) return + + orphanCommands.push(['del', config.coin + ':scores:solo:round' + block.height]) + orphanCommands.push(['del', config.coin + ':scores:prop:round' + block.height]) + orphanCommands.push(['del', config.coin + ':shares_actual:solo:round' + block.height]) + orphanCommands.push(['del', config.coin + ':shares_actual:prop:round' + block.height]) + orphanCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]) + orphanCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.rewardType, + block.login, + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned + ].join(':')]) + + if (block.workerScores && !slushMiningEnabled) { + let workerScores = block.workerScores + Object.keys(workerScores).forEach(function (worker) { + orphanCommands.push(['hincrby', config.coin + ':scores:roundCurrent', worker, workerScores[worker]]) + }) + } + + notifications.sendToAll('blockOrphaned', { + 'HEIGHT': block.height, + 'BLOCKTIME': utils.dateFormat(new Date(parseInt(block.time) * 1000), 'yyyy-mm-dd HH:MM:ss Z'), + 'HASH': block.hash, + 'DIFFICULTY': block.difficulty, + 'SHARES': block.shares, + 'EFFORT': Math.round(block.shares / block.difficulty * 100) + '%' + }) + }) + + if (orphanCommands.length > 0){ + redisClient.multi(orphanCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with cleaning up data in redis for orphan block(s) %j', [error]) + callback(true) + return + } + callback(null, blocks) + }) + } + else{ + callback(null, blocks) + } + }, + + // Handle unlocked blocks + function(blocks, callback){ + let unlockedBlocksCommands = [] + let payments = {} + let totalBlocksUnlocked = 0 + blocks.forEach(function(block){ + if (block.orphaned) return + totalBlocksUnlocked++ + + unlockedBlocksCommands.push(['del', config.coin + ':scores:solo:round' + block.height]) + unlockedBlocksCommands.push(['del', config.coin + ':scores:prop:round' + block.height]) + unlockedBlocksCommands.push(['del', config.coin + ':shares_actual:solo:round' + block.height]) + unlockedBlocksCommands.push(['del', config.coin + ':shares_actual:prop:round' + block.height]) + unlockedBlocksCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]) + unlockedBlocksCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.rewardType, + block.login, + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned, + block.reward + ].join(':')]) + + let feePercent = config.blockUnlocker.poolFee / 100 + + if (Object.keys(donations).length) { + for(let wallet in donations) { + let percent = donations[wallet] / 100 + feePercent += percent + payments[wallet] = Math.round(block.reward * percent) + log('info', logSystem, 'Block %d donation to %s as %d percent of reward: %d', [block.height, wallet, percent, payments[wallet]]) + } + } + + let reward = Math.round(block.reward - (block.reward * feePercent)) + + log('info', logSystem, 'Unlocked %d block with reward %d and donation fee %d. Miners reward: %d', [block.height, block.reward, feePercent, reward]) + + if (block.workerScores) { + let totalScore = parseFloat(block.score) + //deal with solo block + if (block.rewardType === 'solo') { + let worker = block.login + payments[worker] = (payments[worker] || 0) + reward + log('info', logSystem, 'SOLO Block %d payment to %s for %d%% of total block score: %d', [block.height, worker, 100, payments[worker]]) + } else { + Object.keys(block.workerScores).forEach(function (worker) { + let percent = block.workerScores[worker] / totalScore + let workerReward = Math.round(reward * percent) + payments[worker] = (payments[worker] || 0) + workerReward + log('info', logSystem, 'PROP Block %d payment to %s for %d%% of total block score: %d', [block.height, worker, percent*100, payments[worker]]) + }) + } + } + + notifications.sendToAll('blockUnlocked', { + 'HEIGHT': block.height, + 'BLOCKTIME': utils.dateFormat(new Date(parseInt(block.time) * 1000), 'yyyy-mm-dd HH:MM:ss Z'), + 'HASH': block.hash, + 'REWARD': utils.getReadableCoins(block.reward), + 'DIFFICULTY': block.difficulty, + 'SHARES': block.shares, + 'EFFORT': Math.round(block.shares / block.difficulty * 100) + '%' + }) + }) + + for (let worker in payments) { + let amount = parseInt(payments[worker]) + if (amount <= 0){ + delete payments[worker] + continue + } + unlockedBlocksCommands.push(['hincrby', `${config.coin}:workers:${worker}`, 'balance', amount]) + } + + if (unlockedBlocksCommands.length === 0){ + log('info', logSystem, 'No unlocked blocks yet (%d pending)', [blocks.length]) + callback(true) + return + } + + redisClient.multi(unlockedBlocksCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with unlocking blocks %j', [error]) + callback(true) + return + } + log('info', logSystem, 'Unlocked %d blocks and update balances for %d workers', [totalBlocksUnlocked, Object.keys(payments).length]) + callback(null) + }) + } + ], function(error, result){ + setTimeout(runInterval, config.blockUnlocker.interval * 1000) + }) +} + +runInterval() + diff --git a/lib/charts.js b/lib/charts.js new file mode 100644 index 0000000..bd862d9 --- /dev/null +++ b/lib/charts.js @@ -0,0 +1,380 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Charts data functions + **/ + +// Load required modules +let fs = require('fs'); +let async = require('async'); +let http = require('http'); + +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let market = require('./market.js'); + +// Set charts cleanup interval +let cleanupInterval = config.redis.cleanupInterval && config.redis.cleanupInterval > 0 ? config.redis.cleanupInterval : 15; + +// Initialize log system +let logSystem = 'charts'; +require('./exceptionWriter.js')(logSystem); + +/** + * Charts data collectors (used by chartsDataCollector.js) + **/ + +// Start data collectors +function startDataCollectors() { + async.each(Object.keys(config.charts.pool), function(chartName) { + let settings = config.charts.pool[chartName]; + if(settings.enabled) { + setInterval(function() { + collectPoolStatWithInterval(chartName, settings); + }, settings.updateInterval * 1000); + } + }); + + let userSettings = config.charts.user.hashrate; + if(userSettings.enabled) { + setInterval(function() { + collectUsersHashrate('hashrate', userSettings); + }, userSettings.updateInterval * 1000) + } + + let workerSettings = config.charts.user.worker_hashrate; + if (workerSettings && workerSettings.enabled) { + setInterval(function() { + collectWorkersHashrate('worker_hashrate', workerSettings); + }, workerSettings.updateInterval * 1000); + } +} + + +// Chart data functions +let chartStatFuncs = { + hashrate: getPoolHashrate, + miners: getPoolMiners, + workers: getPoolWorkers, + difficulty: getNetworkDifficulty, + price: getCoinPrice, + profit: getCoinProfit +}; + +// Statistic value handler +let statValueHandler = { + avg: function(set, value) { + set[1] = (set[1] * set[2] + value) / (set[2] + 1); + }, + avgRound: function(set, value) { + statValueHandler.avg(set, value); + set[1] = Math.round(set[1]); + }, + max: function(set, value) { + if(value > set[1]) { + set[1] = value; + } + } +}; + +// Presave functions +let preSaveFunctions = { + hashrate: statValueHandler.avgRound, + workers: statValueHandler.max, + difficulty: statValueHandler.avgRound, + price: statValueHandler.avg, + profit: statValueHandler.avg +}; + +// Store collected values in redis database +function storeCollectedValues(chartName, values, settings) { + for(let i in values) { + storeCollectedValue(chartName + ':' + i, values[i], settings); + } +} + +// Store collected value in redis database +function storeCollectedValue(chartName, value, settings) { + let now = new Date() / 1000 | 0; + getChartDataFromRedis(chartName, function(sets) { + let lastSet = sets[sets.length - 1]; // [time, avgValue, updatesCount] + if(!lastSet || now - lastSet[0] > settings.stepInterval) { + lastSet = [now, value, 1]; + sets.push(lastSet); + while(now - sets[0][0] > settings.maximumPeriod) { // clear old sets + sets.shift(); + } + } + else { + preSaveFunctions[chartName] + ? preSaveFunctions[chartName](lastSet, value) + : statValueHandler.avgRound(lastSet, value); + lastSet[2]++; + } + + if(getStatsRedisKey(chartName).search(config.coin + ":charts:hashrate") >=0){ + redisClient.set(getStatsRedisKey(chartName), JSON.stringify(sets), 'EX', (86400 * cleanupInterval)); + } + else{ + redisClient.set(getStatsRedisKey(chartName), JSON.stringify(sets)); + } + + log('info', logSystem, chartName + ' chart collected value ' + value + '. Total sets count ' + sets.length); + }); +} + +// Collect pool statistics with an interval +function collectPoolStatWithInterval(chartName, settings) { + async.waterfall([ + chartStatFuncs[chartName], + function(value, callback) { + storeCollectedValue(chartName, value, settings, callback); + } + ]); +} + +/** + * Get chart data from redis database + **/ +function getChartDataFromRedis(chartName, callback) { + redisClient.get(getStatsRedisKey(chartName), function(error, data) { + callback(data ? JSON.parse(data) : []); + }); +} + +/** + * Return redis key for chart data + **/ +function getStatsRedisKey(chartName) { + return config.coin + ':charts:' + chartName; +} + +/** + * Get pool statistics from API + **/ +function getPoolStats(callback) { + apiInterfaces.pool('/stats', function(error, data) { + if (error) { + log('error', logSystem, 'Unable to get API data for stats: ' + error); + } + callback(error, data); + }); +} + +/** + * Get pool hashrate from API + **/ +function getPoolHashrate(callback) { + getPoolStats(function(error, stats) { + callback(error, stats.pool ? Math.round(stats.pool.hashrate) : null); + }); +} + +/** + * Get pool miners from API + **/ +function getPoolMiners(callback) { + getPoolStats(function(error, stats) { + callback(error, stats.pool ? stats.pool.miners : null); + }); +} + +/** + * Get pool workers from API + **/ +function getPoolWorkers(callback) { + getPoolStats(function(error, stats) { + callback(error, stats.pool ? stats.pool.workers : null); + }); +} + +/** + * Get network difficulty from API + **/ +function getNetworkDifficulty(callback) { + getPoolStats(function(error, stats) { + callback(error, stats.pool ? stats.network.difficulty : null); + }); +} + +/** + * Get users hashrate from API + **/ +function getUsersHashrates(callback) { + apiInterfaces.pool('/miners_hashrate', function(error, data) { + if (error) { + log('error', logSystem, 'Unable to get API data for miners_hashrate: ' + error); + } + let resultData = data && data.minersHashrate ? data.minersHashrate : {}; + callback(resultData); + }); +} + +/** + * Get workers' hashrates from API + **/ +function getWorkersHashrates(callback) { + apiInterfaces.pool('/workers_hashrate', function(error, data) { + if (error) { + log('error', logSystem, 'Unable to get API data for workers_hashrate: ' + error); + } + let resultData = data && data.workersHashrate ? data.workersHashrate : {}; + callback(resultData); + }); +} + +/** + * Collect users hashrate from API + **/ +function collectUsersHashrate(chartName, settings) { + let redisBaseKey = getStatsRedisKey(chartName) + ':'; + redisClient.keys(redisBaseKey + '*', function(keys) { + let hashrates = {}; + for(let i in keys) { + hashrates[keys[i].substr(redisBaseKey.length)] = 0; + } + getUsersHashrates(function(newHashrates) { + for(let address in newHashrates) { + hashrates[address] = newHashrates[address]; + } + storeCollectedValues(chartName, hashrates, settings); + }); + }); +} + +/** + * Get user hashrate chart data + **/ +function getUserHashrateChartData(address, callback) { + getChartDataFromRedis('hashrate:' + address, callback); +} + +/** + * Collect worker hashrates from API + **/ +function collectWorkersHashrate(chartName, settings) { + let redisBaseKey = getStatsRedisKey(chartName) + ':'; + redisClient.keys(redisBaseKey + '*', function(keys) { + let hashrates = {}; + for(let i in keys) { + hashrates[keys[i].substr(redisBaseKey.length)] = 0; + } + getWorkersHashrates(function(newHashrates) { + for(let addr_worker in newHashrates) { + hashrates[addr_worker] = newHashrates[addr_worker]; + } + storeCollectedValues(chartName, hashrates, settings); + }); + }); +} + +/** + * Convert payments data to chart + **/ +function convertPaymentsDataToChart(paymentsData) { + let data = []; + if(paymentsData && paymentsData.length) { + for(let i = 0; paymentsData[i]; i += 2) { + data.unshift([+paymentsData[i + 1], paymentsData[i].split(':')[1]]); + } + } + return data; +} + +/** + * Get current coin market price + **/ +function getCoinPrice(callback) { + let source = config.prices.source; + let currency = config.prices.currency; + + let tickers = [config.symbol.toUpperCase() + '-' + currency.toUpperCase()]; + market.get(source, tickers, function(data) { + let error = (!data || !data[0] || !data[0].price) ? 'No exchange data for ' + config.symbol.toUpperCase() + ' to ' + currency.toUpperCase() + ' using ' + source : null; + let price = (data && data[0] && data[0].price) ? data[0].price : null; + callback(error, price); + }); +} + +/** + * Get current coin profitability + **/ +function getCoinProfit(callback) { + getCoinPrice(function(error, price) { + if(error) { + callback(error); + return; + } + getPoolStats(function(error, stats) { + if(error) { + callback(error); + return; + } + callback(null, stats.lastblock.reward * price / stats.network.difficulty / config.coinUnits); + }); + }); +} + +/** + * Return pool charts data + **/ +function getPoolChartsData(callback) { + let chartsNames = []; + let redisKeys = []; + for(let chartName in config.charts.pool) { + if(config.charts.pool[chartName].enabled) { + chartsNames.push(chartName); + redisKeys.push(getStatsRedisKey(chartName)); + } + } + if(redisKeys.length) { + redisClient.mget(redisKeys, function(error, data) { + let stats = {}; + if(data) { + for(let i in data) { + if(data[i]) { + stats[chartsNames[i]] = JSON.parse(data[i]); + } + } + } + callback(error, stats); + }); + } + else { + callback(null, {}); + } +} + +/** + * Return user charts data + **/ +function getUserChartsData(address, paymentsData, callback) { + let stats = {}; + let chartsFuncs = { + hashrate: function(callback) { + getUserHashrateChartData(address, function(data) { + callback(null, data); + }); + }, + + payments: function(callback) { + callback(null, convertPaymentsDataToChart(paymentsData)); + } + }; + for(let chartName in chartsFuncs) { + if(!config.charts.user[chartName].enabled) { + delete chartsFuncs[chartName]; + } + } + async.parallel(chartsFuncs, callback); +} + + +/** + * Exports charts functions + **/ +module.exports = { + startDataCollectors: startDataCollectors, + getUserChartsData: getUserChartsData, + getPoolChartsData: getPoolChartsData +}; diff --git a/lib/chartsDataCollector.js b/lib/chartsDataCollector.js new file mode 100644 index 0000000..c5ffccc --- /dev/null +++ b/lib/chartsDataCollector.js @@ -0,0 +1,24 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Charts data collector + **/ + +// Load required modules +let fs = require('fs'); +let async = require('async'); +let http = require('http'); + +let charts = require('./charts.js'); + +// Initialize log system +let logSystem = 'chartsDataCollector'; +require('./exceptionWriter.js')(logSystem); + +/** + * Run charts data collector + **/ + +log('info', logSystem, 'Started'); +charts.startDataCollectors(); diff --git a/lib/childDaemon.js b/lib/childDaemon.js new file mode 100644 index 0000000..ad8d18e --- /dev/null +++ b/lib/childDaemon.js @@ -0,0 +1,79 @@ +let async = require('async'); +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let lastHash; +let POOL_NONCE_SIZE = 16 + 1; // +1 for old XMR/new TRTL bugs + +let logSystem = 'childDaemon' +require('./exceptionWriter.js')(logSystem); + + +let pool = config.childPools[process.env.poolId]; + +let blockData = JSON.stringify({ + id: "0", + jsonrpc: "2.0", + method: 'getlastblockheader', + params: {} + }) + +let templateData = JSON.stringify({ + id: "0", + jsonrpc: "2.0", + method: 'getblocktemplate', + params: {reserve_size: POOL_NONCE_SIZE, wallet_address: pool.poolAddress} + }) + + +function runInterval(){ + async.waterfall([ + function(callback) { + apiInterfaces.jsonHttpRequest(pool.childDaemon.host, pool.childDaemon.port, blockData , function(err, res){ + if(err){ + log('error', logSystem, '%s error from daemon', [pool.coin]); + setTimeout(runInterval, 3000); + return; + } + if (res && res.result && res.result.status === "OK" && res.result.hasOwnProperty('block_header')){ + let hash = res.result.block_header.hash.toString('hex'); + if (!lastHash || lastHash !== hash) { + lastHash = hash + log('info', logSystem, '%s found new hash %s', [pool.coin, hash]); + callback(null, true); + return; + } else if (config.daemon.alwaysPoll || false) { + callback(null, true); + return; + }else{ + callback(true); + return; + } + } else { + log('error', logSystem, '%s bad reponse from daemon', [pool.coin]); + setTimeout(runInterval, 3000); + return; + } + }); + }, + function(getbc, callback) { + apiInterfaces.jsonHttpRequest(pool.childDaemon.host, pool.childDaemon.port, templateData, function(err, res) { + if (err) { + log('error', logSystem, '%s Error polling getblocktemplate %j', [pool.coin, err]) + callback(null) + return + } + process.send({type: 'ChildBlockTemplate', block: res.result, poolIndex: process.env.poolId}) + callback(null) + }) + } + ], + function(error) { + if (error){} + setTimeout(function() { + runInterval() + }, config.poolServer.blockRefreshInterval) + }) +} + +runInterval() + + diff --git a/lib/configReader.js b/lib/configReader.js new file mode 100644 index 0000000..e7a0f4f --- /dev/null +++ b/lib/configReader.js @@ -0,0 +1,124 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Configuration Reader + **/ + +// Load required modules +let fs = require('fs'); + +// Set pool software version +global.version = "v1.0.0"; + +/** + * Load pool configuration + **/ + +// Get configuration file path +let configFile = (function(){ + for (let i = 0; i < process.argv.length; i++){ + if (process.argv[i].indexOf('-config=') === 0) + return process.argv[i].split('=')[1]; + } + return 'config.json'; +})(); + +// Read configuration file data +try { + global.config = JSON.parse(fs.readFileSync(configFile)); +} +catch(e){ + console.error('Failed to read config file ' + configFile + '\n\n' + e); + return; +} + +/** + * Developper donation addresses -- thanks for supporting my works! + **/ + +let donationAddresses = { + BTC: '34GDVuVbuxyYdR8bPZ7g6r12AhPPCrNfXt', + BCH: 'qpl0gr8u3yu7z4nzep955fqy3w8m6w769sec08u3dp', + ETH: '0xd4d9a4f22475039f115824b15999a5a8143d424c', + LTC: 'LW169WygGDMBN1PGSr8kNbrFBx94emGWfB', + DERO: 'dERirD3WyQi4udWH7478H66Ryqn3syEU8bywCQEu3k5ULohQRcz4uoXP12NjmN4STmEDbpHZWqa7bPRiHNFPFgTBPmcBmB4yyCF8mZmNUanDb', + GRFT: 'GMPHYf5KRkcAyik7Jw9oHRfJtUdw2Kj5f4VTFJ25AaFVYxofetir8Cnh7S76Q854oMXzwaguL8p5KEz1tm3rn1SA6r6p9dMjuV81yqXCgi', + LTHN: 'NaWe5B5NqvZ3TV2Mj1pxYtTgrnTBwQDMDNtqVzMR6Xa5ejxu6hbi6KULHTqd732ebc5qTHvKXonokghUBd3pjLa8czn8PNg57mR2XqEcvr7w', + MSR: '5t5mEm254JNJ9HqRjY9vCiTE8aZALHX3v8TqhyQ3TTF9VHKZQXkRYjPDweT9kK4rJw7dDLtZXGjav2z9y24vXCdRc3DY4daikoNTeK1v4e', + XMR: '4Cf2TfMKhCgJ2vsM3HeBUnYe52tXrvv8X1ajjuQEMUQ8iU8kvUzCSsCEacxFhEmeb2JgPpQ5chdyw3UiTfUgapJBhHdmH87gYyoDR6NMZj', + SUMO: 'SumipDETyjLYi8rqkmyE9c4SftzYzWPCGA3XvcXbGuBYcqDQJWe8wp8NEwNicFyzZgKTSjCjnpuXTitwn6VdBcFZEFXLcUYThVkF1dR9Q1uxEa', + XHV: 'hvi1aCqoAZF19J8pijvqnrUkeAeP8Rvr4XyfDMGJcarhbL15KgYKM1hN7kiHMu3fer5k8JJ8YRLKCahDKFgLFgJMYAfngJjDmkZAVuiRP15qv', + XTL: 'SEiStP7SMy1bvjkWc9dd1t2v1Et5q2DrmaqLqFTQQ9H7JKdZuATcPHUbUL3bRjxzxTDYitHsAPqF8EeCLw3bW8ARe8rYRNQQwys1JcJAs3qSH', + BLOC: 'abLoc7JNzYXijnKnPf7tSFUNSWBuwKrmUPMevvPkH4jc3b1K9LmS76DKpPamgQ5AYAC2CW9dJfTJ91AnXHYDNXAKRqPx5ZrtR49+9ea139b03f8ec0fce656096ad336a3c7b7041210b56386808dfbe4d1be8186c8', + AEON: 'WzWP347dSczJVmPpw65AsAGi5WhT85z5n66D8vcT3RcxRBBj4tFiDcd2CVFcQ1bBpjNQD5Z5kbXrLjVidvoKFaFK6uj4vyX3yrB1ap6jvPzB', + COAL: 'CoFbPDzEmLDHntwuC4WHkw3hQ4cX7g2JdVSGzqndAcDca4XgKyR4Wca7tkJw56eVX12iAQNGRzNPNXsegXmoJvUDSmkUKhL+3f128d248560b076805004a589de88bf6546bc9a0e0011dd56d6040d99b5d622', + XVG: 'DKSE7UW9Pwssq2ZF7rMvQVBG2EDio1GZHP', + BCN: '24WXh1qTEZzgDG3Ly1MtCJX7fRbDNqbzC4iEzpQBVhLwZ9jBeSs7GFQgu1bYxpHFKyjBQvABXicZ2MJ7si6rVBcQKQBCgLj+f122a0901996579ca5e8a7b5bdfb958a8e8d9fde470faeb7794315c3473b9aaa', + TRTL: 'TRTLv1Hqo3wHdqLRXuCyX3MwvzKyxzwXeBtycnkDy8ceFp4E23bm3P467xLEbUusH6Q1mqQUBiYwJ2yULJbvr5nKe8kcyc4uyps+0f2404e298d6e6c132b300713bcd723d7fa61dd1206ee4b5975c254c67783686', + KRB: 'KdAXXrRCGcENDhuRqEkocjZ5tpPfu17U35mqhgDEJTjTFBfs4hxhiBKK95XEpAuY8V9nomNcMeTz1E6tehWEvU6h8vCBs51+b7480fabf430e71b4702fbcf35e2454acba69be935e289fa84c485c2f07f5e63', + CIV: 'CM2ieMxxePe5z1BUkeNm9p5pmpn3k4juX1', + DASH: 'XhVA4JaYjtqvuSLSNFgxYUQKJaJnRhi1ww', + NAH: 'SWNNDA4wUvTkFVYvvhDP24yiCrR2p7SZbt', + PCN: 'P9GzJEnyYRgA6GHfdwtr7FhYq3s365ASKS', + LUX: 'LgozXD5vaHT3BDkNVRLWCBaNvSBmJxn5NT', + TUBE: 'bi1b95WYJRES7oBrvRo2eQV53ExLzFAzjKVM4wp9H9B6irCR6UuQxHf183XsJwemdoQm5PUHhQVwS67Hf5yUE7qg4SwbJJfAjLE1PH6T9V667', + DOGE: 'DM6FYmmLw4R5uFaSNdtYMrBLYuy1ank39R', + NBR: 'NDysWekoQnxeUquciujUQFPvVXTaU2D23eLbbboNFavCDozx2157EB3KzyKxk3mdyJRU1YfarvN35EfkGFbNEAQ4KHbkwy2+643750a2037d089470b8889d3a13edf673fdb0738e9c9bc4812ce41ded0644a7', + XUN: 'Xun3jQ4dLmfdRCnBuavjukJRm8EMntWqhL27JKJqeEf73rwi8nRoMYaMusv8pD9s5Y7oK8aHYCieB9rcNJ4uDfzZ8HUmzRq8yQ+cb22ae07df149caf85050ff14fd8b82d18fd0d62601a714b969855461046111b', + WAE: 'KfDMEcEpi7HANXK5N6vgAorWDLNqFfLnMk', + IRD: 'ir2btddJ78sicpKntYo3oRMLQh91VktzBfZzWbhwZnQxS815QLiG5WCAH9sgVGC5uwLZuMJCwW5CdFigNbJ3WTxU2CG5GnUDe+fe88be6c5a157ab1c97242ec0c6be699f48c604f752def45259097d20405a035', + D: 'DSHxGR1RFja4dwoqG2VXvFoSK5uS35pBWA', + PLURA: 'Pv8xzGjaY4TBPQUc8mLqcPGXCpWJbAYAjZ1uwZJK9Cfg1qvQBx3zZHqGF4XWnZHeYDKfkQVA8yDSjVdE56nz2Jab2Xu16PGb1+bda64dd03e45d67fd6fa4b99d91b507c02230bfe21cf2e862e436f18143d547a', + BTCP: 'b1B64ofQxHBPYXPYJvkuM9z1nexV8NSXtj6', + PIVX: 'DAE6oWhR1TwD8vFSxQHGaTMwyymHcCsmDH', + BSM: 'Sm4gdR5meAJAets9DwXCtq9tRZofxg6uubw6rYPKqYi7EN1MKADYG6obJczbjCmwfS752ThxYHUr3gBqY8KDWrB91dPcCbhJe', + OMB: 'casiLpKfELY6hyxUqS1zYjjAMTSWrcUm5Cpc1bSdwNyCEmP2ii8EfVWLvvjysm2YXBXM2vGvpkGUs42RD1ihi9uDATCr5crEjvw8AfnGHwkaw', + LOKI: 'LK8CGQ17G9R3ys3Xf33wCeViD2B95jgdpjAhcRsjuheJ784dumXn7g3RPAzedWpFq364jJKYL9dkQ8mY66sZG9BiCvqjvv6LgsyHf6H2gy', + BKC: 'bkc1o8uPqS7YD6Eo4yoaGp9N2MgbUrU7pUChtFFXS3K6fWF5hT9SrP2dGJfsTnn6pWZtQckuPYUSbJy5qv2MnLGo8eFS3t6qJw+29c6c1a056b3bc2598d0d57ab396c2d9c0053642dd9b8cbb7cb5bd63742f1eea', + SOLACE: 'Siz7GSHywyu3RxHDAcuW9iBKVLfgjjftDj5p9AucrNb9YV1jkPZNBdjDiWxcjMZK94Kdo97BzMuSZAU87U7UCzUL4JeRiP2zFz87AgfgBiFUW', + RTO: 'ALJj1xFnU8854yRhkrQKLyKeZYUxTt1oFQ4nEpbNsJoneXMBHyCgCJCW9xq2QZLVnbB4hBwxbEopSdWEmiyXXFiC66VQvaF+b40b160bf2f6efda8e56213c5907e960518633c8152f79c4645dbfda637c5122', + ITA: 'iz4FdJrFNkGNEmLSJAA39bT4cuQBVAgQTa818Gqshzg32PadREJSuWWEHFunmdfS8rjHGSHTxZoja5nWSZ8zWwiW51F2SAPWRN42mdXBvWe6', + INTU: 'intuvSUykQhFXJj22j2KJqC5CRMv3AWar467CtzZFVAVBxLUwb8yQi1LF72LzLRHtvAoNGkCYr3EZHYUJmeSqcZCLhxK73jJZDF+e35f3bdc1581764e8d8d778c2acbfd1acb838a2da61553ef637c980765bc3a26', + WOW: 'So2ifgjqGMZJhCrqpFMotQQAiJAiATuJLNAK2HrPLoNzK8hkqNbf9t8gmx6bzAQrXRMnWnoELoiD6GTv8guPBRwH5yoUvyBK1Ku1YYpQf2x8', + XMV: '4Cd7rzeiqwQJe8dCZbQeQxeNHUV4P4w7hAJ2g8U2ciiEao2AmqN9tXEfngQaPGV6T2Sx9mEtCaEMzaCR53iQRJqEgha9Dv26d83ML2wMiT', + XMC: '4H6kZARSRW9WAfxooKC2hkSpNf3RHo7ERBZWFdHN2BiX5BRxoiP5881EEK7P9wdzCZZmUxGWCZNuMjYdKFL1UuqdNUSZQs3uWGhCd4s4Xe', + WTIP: 'WtiptjGr6oRdZVFqumdfLbVd1XMtCYWKBT6dXppmCWA3TAk4tvesMCsbFLewv4rnmQHKimvwvcsbzC5FgWRrkGmc1DXreHk7sS+b319cf450209a800ea35c7f0d66754d0e243314650fe89ffd5db352733d9b9db', + BBS: 'fyT3HPm3Qrh87dR64wwLCi855A6bgYdF13mqtSHvX3RB67XEBb8aRrtVGsHa8u9juzByX8Mv6CPDjeuJwHhfqrjA19e7t2Fad+38adf2a9aadf58bf65374a683853ccbc169f75c9a38dc89cecb1beba3788c764', + XNV: 'NizKdaicW4bVfYB3AVhnsq9qnvUYSKe568YaNV2KQCYCDrNGzpvxqBo6mxF8cBkiQDU5xkgB2PrUGFKf66wVDVoNbQBhu2JRacy6uhsLoUyBJ', + XRN: 'PiyAfA1u1bNH9XjsPjXc5M64sip7LCj6ziBKEneKPmbVWM6kPMMAQs17h26tnCogW5Q72c5pVGJ4QW3kSKso4MW4h6hQJAVW23z6w2YMYKdHB', + XTRI: 'TixxgPgBkxgC4JM39WZuacjMLnJqm9YbjPq1YAR4BJbLXiCzf345r9SVbNuKMAG1CcjRMsv7kpatt7gStpUE3gGJRb8cbvWQHfk6L4pEbmdF7', + RVN: 'RFQvccQyLF3YMhKQby3bKvNXJczhgzEofu', + ETNXP: 'f4VR74XR616Tw2wAMMfaLV1vmYBSBBbmWXBUtaV8YDb6DHsfKRoYkFaCvhPhsGDDfm1afhzLNuf5XGFmNrvodPoQ6m4qGwXTuNt3gNnTEMYVA', + ZEL: 't1gsjJAhgGDB45ohJnJkbxtvr8WcUSzahyr', + RYO: 'SumipDETyjLYi8rqkmyE9c4SftzYzWPCGA3XvcXbGuBYcqDQJWe8wp8NEwNicFyzZgKTSjCjnpuXTitwn6VdBcFZEFXLcSos83oaS9wV7CJdKY', + INC: 'i9ajAmx77JLPtZL7JEs3ZEVbErmXqvQeGbg7PywpCaRKQGyypTv4TTpDYLMtrdGBGXMJM2mugBf14csz4wmNitZuGdXmy2c4hbG2iiUscAxQ', + QUAN: 'QRm4DREddkpmmC48CqXqkhV6S75fhtciBo', + PURK: 'PK2TdygFzH7X9jZPAdjvgQFHBR5bdNmsr6xefs2yQsDRHkKuoAsqQ7hX73nLgjWpiji4GqJMmNx357eu98TpU9Yr2es6SBBtQ+1246e3b3c4fdba89cbcad2113d0dff839d0f1f0a8a6f648b1c292b9bd8a0f8c2', + ACM: 'PDo5Z3pqN8weHSbfqayJEiSeAkAyr32NB3', + ARQ: 'aRi1cDd6LkAcc1p6W58dkPi8xSfbZ5EuYFrHxwH3py1MQ9rFrzmSaghguD4GGpCfHSMmKXWJrd4e5CkabC3viWJKfHuDLYqHNGs9D83sj6BPX', + NCP: 'cczJxhhLKTg7oNGiy1kmFadqTcKDschTb3LUKGvQdhxrVepPo9KjfxjSeqXBUKWVFwcHZRTGLi3k6USWHF1YP82e2TWRse4KD6+70fa2954af5cfc9fd292ce1643e276c8899b769255108c84caf2dcc406ea0678', + XGS: 'GTqypwunRe5ZkNdmAr26B9mmeMoGwhgoCV', + SUQA: 'Sic7A6F5r8RkjBvHjRwA9jWhHawXNrFxFX', + SHB: 'SVSEb9adxGpWckC2KuwuNbD1ved2x7YWTj', + GPKR: 'GdQ4ewDqJyhMU4BpchEs5uyn8U5VeHii7V', + ETNX: 'f4VR74XR616Tw2wAMMfaLV1vmYBSBBbmWXBUtaV8YDb6DHsfKRoYkFaCvhPhsGDDfm1afhzLNuf5XGFmNrvodPoQ6m4qTKUkc8Y2dL6d97BZw', + LMO: 'darkWdeodDHM5YWWGBHKa821DrL3HSzeaBNaVmXTr3svd75GUVcUBbxdSdFqJFUgTWfxfZJJcGTg58rfKct5hedk3Gz9dFLa6U', + TTNZ: 'Tri1J1prCp9VWj3AyjSTHU5aH1kVt4hsJ9xv7jbMauBzCLcqgKzJB2SATr2aypSFfmBW2dNfDVxMW3sQ4ys147Pz5pRgJvKzVS', + FHV: 'fh3ddFK3JqWNRZFsiL7xB5bGa79ejPfBNVbSdXVzdKiR7vRXHC3osL51vg9PyHKmxvWgz4ymZeHzcRZNRTJ5kwzM2BtrZMiq7', + SAFEX: 'Safex5zXVvH6GYJY2tnL4GcJy4W72GRutjhV1aCaRiPhYnfv4CyDjmGfLYQDd4GaJvHEKrpE7r9ux6UMCv5i1PmvjNwxA4r4Roi3K', + MUTX: 'ZYZZZDAz8vEfwGL3QfuRpMHneGuMbm6uTCtJ4h1kyhYkSi8Yjp62fBQWuHEviGoUdcXmSuYM3mDVa6tR9Rv5zRMra5i1F5UKvSXZc8X7MwHCki', + BTCN: 'MvweZSjjwTTdwrKJcRL2SzRZm9YrEAthNNeyzGjdbbeujoPsayzLM4efBhLZQoV6GreZTNgHv2gBZ9Hzz7SkTYVN2EgTbqh', + ARMS: 'gunsGSAGHJweDXBXGwCQrwUqACk67GkZB991b4HHkze8a7bnif8XwPF1NMdoY6oRhm8qjbPu2Jh7F4egLD3mpFTN1oQNydPdNv', + ZANO: 'iZ2CFBHEsbjPYCWHns75LFT5NwNLc2i9hPTG1SwJjonyBnBUGCtdCdpKwBbo6KdZgH1Azg9vNcyLoBXBLEaz3HADLhnuSEVsy9i2oiGChh15' +}; + +global.donations = {}; + +global.devFee = config.blockUnlocker.devDonation || 0.2; +if (config.blockUnlocker.devDonation === 0) + global.devFee = 0.2; + +let wallet = donationAddresses[config.symbol.toUpperCase()]; +if (devFee && wallet) + global.donations[wallet] = devFee; diff --git a/lib/daemon.js b/lib/daemon.js new file mode 100644 index 0000000..97e7998 --- /dev/null +++ b/lib/daemon.js @@ -0,0 +1,148 @@ +// Copyright (c)2020, The Arqma Network +// Copyright (c)2020, Gary Rusher +// Portions of this software are available under BSD-3 license. Please see ORIGINAL-LICENSE for details + +// All rights reserved. + +// Authors and copyright holders give permission for following: + +// 1. Redistribution and use in source and binary forms WITHOUT modification. + +// 2. Modification of the source form for your own personal use. + +// As long as the following conditions are met: + +// 3. You must not distribute modified copies of the work to third parties. This includes +// posting the work online, or hosting copies of the modified work for download. + +// 4. Any derivative version of this work is also covered by this license, including point 8. + +// 5. Neither the name of the copyright holders nor the names of the authors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. + +// 6. You agree that this licence is governed by and shall be construed in accordance +// with the laws of England and Wales. + +// 7. You agree to submit all disputes arising out of or in connection with this licence +// to the exclusive jurisdiction of the Courts of England and Wales. + +// Authors and copyright holders agree that: + +// 8. This licence expires and the work covered by it is released into the +// public domain on 1st of March 2021 + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +let utils = require('./utils.js'); +let async = require('async'); +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let lastHash; + +let POOL_NONCE_SIZE = 16 + 1; // +1 for old XMR/new TRTL bugs +let EXTRA_NONCE_TEMPLATE = "02" + POOL_NONCE_SIZE.toString(16) + "00".repeat(POOL_NONCE_SIZE); +let POOL_NONCE_MM_SIZE = POOL_NONCE_SIZE + utils.cnUtil.get_merged_mining_nonce_size(); +let EXTRA_NONCE_NO_CHILD_TEMPLATE = "02" + POOL_NONCE_MM_SIZE.toString(16) + "00".repeat(POOL_NONCE_MM_SIZE); + + +let logSystem = 'daemon' +let blockData = JSON.stringify({ + id: "0", + jsonrpc: "2.0", + method: 'getlastblockheader', + params: {} + }) + +let templateData = JSON.stringify({ + id: "0", + jsonrpc: "2.0", + method: 'getblocktemplate', + params: {reserve_size: config.poolServer.mergedMining ? POOL_NONCE_MM_SIZE : POOL_NONCE_SIZE, wallet_address: config.poolServer.poolAddress} + }) + + +require('./exceptionWriter.js')(logSystem); + + +function runInterval(){ + async.waterfall([ + function(callback) { + apiInterfaces.jsonHttpRequest(config.daemon.host, config.daemon.port, blockData , function(err, res){ + if(err){ + log('error', logSystem, '%s error from daemon', [config.coin]); + setTimeout(runInterval, 3000); + return; + } + if (res && res.result && res.result.status === "OK" && res.result.hasOwnProperty('block_header')){ + let hash = res.result.block_header.hash.toString('hex'); + if (!lastHash || lastHash !== hash) { + lastHash = hash + log('info', logSystem, '%s found new hash %s', [config.coin, hash]); + callback(null, true); + return; + } else if (config.daemon.alwaysPoll || false) { + callback(null, true); + return; + }else{ + callback(true); + return; + } + } else { + log('error', logSystem, 'bad reponse from daemon'); + setTimeout(runInterval, 3000); + return; + } + }); + }, + function(getbc, callback) { + apiInterfaces.jsonHttpRequest(config.daemon.host, config.daemon.port, templateData, function(err, res) { + if (err) { + log('error', logSystem, 'Error polling getblocktemplate %j', [err]) + callback(null) + return + } + if (res.error) { + log('error', logSystem, 'Error polling getblocktemplate %j', [res.error]) + callback(null) + return + } + process.send({type: 'BlockTemplate', block: res.result}) + callback(null) + }) + } + ], + function(error) { + if (error){} + setTimeout(function() { + runInterval() + }, config.poolServer.blockRefreshInterval) + }) +} + +function runZmq() { + let zmqDaemon = require("./zmqDaemon.js") + let zmqDirector = zmqDaemon.startZMQ() + zmqDirector.subscribe(x => { + let json = JSON.parse(x.toString()).result + process.send({ + type: 'BlockTemplate', + block: json + }) + log('info', logSystem, '%s ZMQ found new blockhashing_blob %s', [config.coin, json.blockhashing_blob]); + }) + zmqDaemon.sendMessage('get_block_template', config.poolServer.poolAddress) +} + +if (config.zmq.enabled) { + runZmq() +} else { + runInterval() +} diff --git a/lib/email.js b/lib/email.js new file mode 100644 index 0000000..b9db5bb --- /dev/null +++ b/lib/email.js @@ -0,0 +1,72 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Email system + * Supports: sendmail, smtp, mailgun + * + * Author: Daniel Vandal + **/ + +// Load required modules +let nodemailer = require('nodemailer'); +let mailgun = require('mailgun.js'); + +// Initialize log system +let logSystem = 'email'; +require('./exceptionWriter.js')(logSystem); + +/** + * Sends out an email + **/ +exports.sendEmail = function(email, subject, content) { + // Return error if no destination email address + if (!email) { + log('warn', logSystem, 'Unable to send e-mail: no destination email.'); + return ; + } + + // Check email system configuration + if (!config.email) { + log('error', logSystem, 'Email system not configured!'); + return ; + } + + // Do nothing if email system is disabled + if (!config.email.enabled) return ; + + // Set content data + let messageData = { + from: config.email.fromAddress, + to: email, + subject: subject, + text: content + }; + + // Get email transport + let transportMode = config.email.transport; + let transportCfg = config.email[transportMode] ? config.email[transportMode] : {}; + + if (transportMode === "mailgun") { + let mg = mailgun.client({username: 'api', key: transportCfg.key}); + mg.messages.create(transportCfg.domain, messageData) + .then(() => { + log('info', logSystem, 'E-mail sent to %s: %s', [messageData.to, messageData.subject]); + }) + .catch(error => { + log('error', logSystem, 'Unable to send e-mail to %s: %s', [messageData.to, JSON.stringify(error)]); + }); + } + + else { + transportCfg['transport'] = transportMode; + let transporter = nodemailer.createTransport(transportCfg); + transporter.sendMail(messageData, function(error){ + if(error){ + log('error', logSystem, 'Unable to send e-mail to %s: %s', [messageData.to, error.toString()]); + } else { + log('info', logSystem, 'E-mail sent to %s: %s', [email, subject]); + } + }); + } +}; diff --git a/lib/exceptionWriter.js b/lib/exceptionWriter.js new file mode 100644 index 0000000..4215a9b --- /dev/null +++ b/lib/exceptionWriter.js @@ -0,0 +1,25 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Exception writer + **/ + +// Load required modules +let fs = require('fs'); +let cluster = require('cluster'); +let dateFormat = require('dateformat'); + +/** + * Handle exceptions + **/ +module.exports = function(logSystem){ + process.on('uncaughtException', function(err) { + console.log('\n' + err.stack + '\n'); + let time = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss'); + fs.appendFile(config.logging.files.directory + '/' + logSystem + '_crash.log', time + '\n' + err.stack + '\n\n', function(err){ + if (cluster.isWorker) + process.exit(); + }); + }); +}; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..8218406 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,96 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Log system + **/ + +// Load required modules +let fs = require('fs'); +let util = require('util'); +let dateFormat = require('dateformat'); +let clc = require('cli-color'); + +/** + * Initialize log system + **/ + +// Set CLI colors +let severityMap = { + 'info': clc.blue, + 'warn': clc.yellow, + 'error': clc.red +}; + +// Set severity levels +let severityLevels = ['info', 'warn', 'error']; + +// Set log directory +let logDir = config.logging.files.directory; + +// Create log directory if not exists +if (!fs.existsSync(logDir)){ + try { + fs.mkdirSync(logDir); + } + catch(e){ + throw e; + } +} + +/** + * Write log entries to file at specified flush interval + **/ +let pendingWrites = {}; + +setInterval(function(){ + for (let fileName in pendingWrites){ + let data = pendingWrites[fileName]; + fs.appendFile(fileName, data, function(err) { + if (err) { + console.log("Error writing log data to disk: %s", err); + callback(null, "Error writing data to disk"); + } + }); + delete pendingWrites[fileName]; + } +}, config.logging.files.flushInterval * 1000); + +/** + * Add new log entry + **/ +global.log = function(severity, system, text, data){ + + let logConsole = severityLevels.indexOf(severity) >= severityLevels.indexOf(config.logging.console.level); + let logFiles = severityLevels.indexOf(severity) >= severityLevels.indexOf(config.logging.files.level); + let prefix = config.logging.files.prefix? (config.logging.files.prefix + '_') : ''; + + if (!logConsole && !logFiles) return; + + let time = dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss'); + let formattedMessage = text; + + if (data) { + data.unshift(text); + formattedMessage = util.format.apply(null, data); + } + + if (logConsole){ + if (config.logging.console.colors) + if (system === 'daemon' || system === 'childDaemon') { + console.log(severityMap[severity](time) + clc.green.bold(' [' + system + '] ' + formattedMessage)); + } + else { + console.log(severityMap[severity](time) + clc.white.bold(' [' + system + '] ') + formattedMessage); + } + else + console.log(time + ' [' + system + '] ' + formattedMessage); + } + + + if (logFiles) { + let fileName = logDir + '/' + prefix+ system + '_' + severity + '.log'; + let fileLine = time + ' ' + formattedMessage + '\n'; + pendingWrites[fileName] = (pendingWrites[fileName] || '') + fileLine; + } +}; diff --git a/lib/market.js b/lib/market.js new file mode 100644 index 0000000..9ad75f1 --- /dev/null +++ b/lib/market.js @@ -0,0 +1,635 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Market Exchanges + **/ + +// Load required modules +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet); + +// Initialize log system +let logSystem = 'market'; +require('./exceptionWriter.js')(logSystem); + +/** + * Get market prices + **/ +exports.get = function(exchange, tickers, callback) { + if (!exchange) { + callback('No exchange specified', null); + } + exchange = exchange.toLowerCase(); + + if (!tickers || tickers.length === 0) { + callback('No tickers specified', null); + } + + let marketPrices = []; + let numTickers = tickers.length; + let completedFetches = 0; + + getExchangeMarkets(exchange, function(error, marketData) { + if (!marketData || marketData.length === 0) { + callback({}); + return ; + } + + for (let i in tickers) { + (function(i){ + let pairName = tickers[i]; + let pairParts = pairName.split('-'); + let base = pairParts[0] || null; + let target = pairParts[1] || null; + + if (!marketData[base]) { + completedFetches++; + if (completedFetches === numTickers) callback(marketPrices); + } else { + let price = marketData[base][target] || null; + if (!price || price === 0) { + let cryptonatorBase; + if (marketData[base]['BTC']) cryptonatorBase = 'BTC'; + else if (marketData[base]['ETH']) cryptonatorBase = 'ETH'; + else if (marketData[base]['LTC']) cryptonatorBase = 'LTC'; + + if (!cryptonatorBase) { + completedFetches++; + if (completedFetches === numTickers) callback(marketPrices); + } else { + getExchangePrice("cryptonator", cryptonatorBase, target, function(error, tickerData) { + completedFetches++; + if (tickerData && tickerData.price) { + marketPrices[i] = { + ticker: pairName, + price: tickerData.price * marketData[base][cryptonatorBase], + source: tickerData.source + }; + } + if (completedFetches === numTickers) callback(marketPrices); + }); + } + } else { + completedFetches++; + marketPrices[i] = { ticker: pairName, price: price, source: exchange }; + if (completedFetches === numTickers) callback(marketPrices); + } + } + })(i); + } + }); +} + +/** + * Get Exchange Market Prices + **/ + +let marketRequestsCache = {}; + +function getExchangeMarkets(exchange, callback) { + callback = callback || function(){}; + if (!exchange) { + callback('No exchange specified', null); + } + exchange = exchange.toLowerCase(); + + // Return cache if available + let cacheKey = exchange; + let currentTimestamp = Date.now() / 1000; + + if (marketRequestsCache[cacheKey] && marketRequestsCache[cacheKey].ts > (currentTimestamp - 60)) { + callback(null, marketRequestsCache[cacheKey].data); + return ; + } + + let target = null; + let symbol = null; + let price = 0.0; + let data = {}; + + // Altex + if (exchange == "altex") { + apiInterfaces.jsonHttpRequest('api.altex.exchange', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + if (error) callback(error, {}); + if (!response || !response.success) callback('No market informations', {}); + let ticker = null; + for (ticker in response.data) { + tickerParts = ticker.split('_'); + target = tickerParts[0]; + symbol = tickerParts[1]; + + price = +parseFloat(response.data[ticker].last); + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/v1/ticker'); + } + + // Crex24 + else if (exchange == "crex24") { + apiInterfaces.jsonHttpRequest('api.crex24.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + if (error) callback(error, {}); + if (!response || !response.Tickers) callback('No market informations', {}); + + let ticker = null; + let pairName = null; + let pairParts = null; + for (let i in response.Tickers) { + ticker = response.Tickers[i]; + + pairName = ticker.PairName; + pairParts = pairName.split('_'); + target = pairParts[0]; + symbol = pairParts[1]; + + price = +ticker.Last; + if (!price || price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/CryptoExchangeService/BotPublic/ReturnTicker'); + } + + // Cryptopia + else if (exchange == "cryptopia") { + apiInterfaces.jsonHttpRequest('www.cryptopia.co.nz', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + if (error) callback(error, {}); + if (!response || !response.Success) callback('No market informations', {}); + + let ticker = null; + let pairName = null; + let pairParts = null; + for (let i in response.Data) { + ticker = response.Data[i]; + + pairName = ticker.Label; + pairParts = pairName.split('/'); + target = pairParts[1]; + symbol = pairParts[0]; + + price = +ticker.LastPrice; + if (!price || price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/GetMarkets'); + } + + // Stocks.Exchange + else if (exchange == "stocks.exchange") { + apiInterfaces.jsonHttpRequest('stocks.exchange', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + if (error) callback(error, {}); + if (!response) callback('No market informations', {}); + + let ticker = null; + let pairName = null; + let pairParts = null; + for (let i in response) { + ticker = response[i]; + + pairName = ticker.market_name; + pairParts = pairName.split('_'); + target = pairParts[1]; + symbol = pairParts[0]; + + price = +ticker.last; + if (!price || price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api2/ticker'); + } + + // TradeOgre + else if (exchange == "tradeogre") { + apiInterfaces.jsonHttpRequest('tradeogre.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + let pairParts = null; + if (!error && response) { + for (let i in response) { + for (let pairName in response[i]) { + pairParts = pairName.split('-'); + target = pairParts[0]; + symbol = pairParts[1]; + + price = +response[i][pairName].price; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/v1/markets'); + } + else if (exchange == "maplechange") { + apiInterfaces.jsonHttpRequest('maplechange.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + let data = {}; + if (!error && response) { + for (let model in response) { + let len = model.length; + if (len <= 3) continue; + target = model.substring(len-3, len).toUpperCase(); + symbol = model.substring(0, len -3).toUpperCase(); + + price = +response[model]['ticker']['last']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/v2/tickers'); + } + else if (exchange == "stex") { + apiInterfaces.jsonHttpRequest('app.stex.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + let pieces = null; + + if (!error && response) { + for (let model in response) { + pieces = response[model]['market_name'].split('_'); + target = pieces[1]; + symbol = pieces[0]; + + price = +response[model]['last']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api2/ticker'); + } + // Btc-Alpha + else if (exchange == "btcalpha") { + apiInterfaces.jsonHttpRequest('btc-alpha.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + let pieces = null; + if (!error && response) { + for (let model in response) { + pieces = response[model]['pair'].split('_'); + target = pieces[1]; + symbol = pieces[0]; + + price = +response[model]['price']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/v1/exchanges/' /*JUST FOR 20DEC!! '/api/v1/exchanges/?pair=BDX_BTC'*/); + } + // tradesatoshi + else if (exchange == "tradesatoshi") { + apiInterfaces.jsonHttpRequest('tradesatoshi.com', 443, '', function(error, response) { + if (error) console.log('error', 'API request to has failed: ' + error); + + let pieces = null; + if (!error && response.success) { + for (let model in response.result) { + pieces = response.result[model]['market'].split('_'); + target = pieces[1]; + symbol = pieces[0]; + + price = +response.result[model]['last']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/public/getmarketsummaries'); + } + else if (exchange == "coinmarketcap") { + apiInterfaces.jsonHttpRequest('coinmarketcap.coindeal.com', 443, '', function(error, response) { + if (error) console.log('error', 'API request to has failed: ' + error); + var data = {}; + if (!error && response) { + for (var model in response) { + var pieces = model.split('_'); + var target = pieces[1]; + var symbol = pieces[0]; + if (symbol === 'BTC') continue; + var price = +response[model]['last']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + console.log(data) + }, '/api/v1/ticker'); + } + else if (exchange == "tradecx") { + apiInterfaces.jsonHttpRequest('tradecx.io', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + let data = {}; + if (!error && response) { + for (let model in response) { + let len = model.length; + if (len <= 3) continue; + target = model.substring(len-3, len).toUpperCase(); + symbol = model.substring(0, len -3).toUpperCase(); + + price = +response[model]['ticker']['last']; + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, '/api/tickers'); + } +else if (exchange == "coingecko") { + apiInterfaces.jsonHttpRequest('api.coingecko.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (!error && response) { + let matchingCoin = response.filter(coin => { + return coin.symbol === config.symbol.toLowerCase() ? coin.name.toLowerCase() : '' + }) + apiInterfaces.jsonHttpRequest('api.coingecko.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + let data = {}; + if (!error && response.tickers) { + for (let model in response.tickers) { + target = response.tickers[model].target + symbol = response.tickers[model].base + + price = +response.tickers[model].last + if (price === 0) continue; + + if (!data[symbol]) data[symbol] = {}; + data[symbol][target] = price; + } + } + if (!error) marketRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(null, data); + }, `/api/v3/coins/${matchingCoin[0].id}/tickers`); + + } + }, `/api/v3/coins/list`); + } + // Unknown + else { + callback('Exchange not supported: ' + exchange); + } +} +exports.getExchangeMarkets = getExchangeMarkets; + +/** + * Get Exchange Market Price + **/ + +let priceRequestsCache = {}; + +function getExchangePrice(exchange, base, target, callback) { + callback = callback || function(){}; + + if (!exchange) { + callback('No exchange specified'); + } + else if (!base) { + callback('No base specified'); + } + else if (!target) { + callback('No target specified'); + } + + exchange = exchange.toLowerCase(); + base = base.toUpperCase(); + target = target.toUpperCase(); + + // Return cache if available + let cacheKey = exchange + '-' + base + '-' + target; + let currentTimestamp = Date.now() / 1000; + + let error = null; + let price = 0.0; + let data = {}; + let ticker = null; + + if (priceRequestsCache[cacheKey] && priceRequestsCache[cacheKey].ts > (currentTimestamp - 60)) { + callback(null, priceRequestsCache[cacheKey].data); + return ; + } + + // Cryptonator + if (exchange == "cryptonator") { + ticker = base + '-' + target; + apiInterfaces.jsonHttpRequest('api.cryptonator.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.error) log('warn', logSystem, 'Cryptonator API error: %s', [response.error]); + + error = response.error ? response.error : error; + price = response.success ? +response.ticker.price : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/api/ticker/' + ticker); + } + + // Altex + else if (exchange == "altex") { + getExchangeMarkets(exchange, function(error, data) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + price = null; + if (!error && data[base] && data[base][target]) { + price = data[base][target]; + } + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }); + } + + // Crex24 + else if (exchange == "crex24") { + ticker = base + '_' + target; + apiInterfaces.jsonHttpRequest('api.crex24.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.Error) log('warn', logSystem, 'Crex24 API error: %s', [response.Error]); + + error = response.Error ? response.Error : error; + price = (response.Tickers && response.Tickers[0]) ? +response.Tickers[0].Last : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/CryptoExchangeService/BotPublic/ReturnTicker?request=[NamePairs=' + ticker + ']'); + } + + // Cryptopia + else if (exchange == "cryptopia") { + ticker = base + '_' + target; + apiInterfaces.jsonHttpRequest('www.cryptopia.co.nz', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.Error) log('warn', logSystem, 'Cryptopia API error: %s', [response.Error]); + + error = response.Error ? response.Error : error; + price = (response.Data && response.Data.LastPrice) ? +response.Data.LastPrice : null; + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/api/GetMarket/' + ticker); + } + + // Stocks.Exchange + else if (exchange == "stocks.exchange") { + getExchangeMarkets(exchange, function(error, data) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + + if (!error && data[base] && data[base][target]) { + price = data[base][target]; + } + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }); + } + + // TradeOgre + else if (exchange == "tradeogre") { + ticker = target + '-' + base; + apiInterfaces.jsonHttpRequest('tradeogre.com', 443, '', function(error, response) { + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.message) log('warn', logSystem, 'TradeOgre API error: %s', [response.message]); + + error = response.message ? response.message : error; + price = +response.price || null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange }; + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/api/v2/ticker/' + ticker); + } + // Btc-Alpha + else if (exchange == "btcalpha") { + ticker = base + '_' + target; + apiInterfaces.jsonHttpRequest('btc-alpha.com', 443, '', function(error, response) { + + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.message) log('warn', logSystem, 'BTC-Alpha API error: %s', [response.message]); + + error = response.message ? response.message : error; + + price = response[0] != undefined ? response[0]['price'] : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange } + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/api/v1/exchanges/?pair=' + ticker + '&limit=1'); + + } + // tradesatoshi + else if (exchange == "tradesatoshi") { + ticker = base + '_' + target; + apiInterfaces.jsonHttpRequest('tradesatoshi.com', 443, '', function(error, response) { + + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.message) log('warn', logSystem, 'BTC-Alpha API error: %s', [response.message]); + + error = response.message ? response.message : error; + + price = response.result != undefined ? response.result['last'] : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange } + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/api/public/getmarketsummary?market=' + ticker); + + } + // coinmarketcap + else if (exchange == "coinmarketcap") { + apiInterfaces.jsonHttpRequest('api.coinmarketcap.com', 443, '', function(error, response) { + + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.message) log('warn', logSystem, 'CoinMarketCap API error: %s', [response.message]); + + error = response.message ? response.message : error; + + price = response ? +response.data.quotes[ticker].price : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange } + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/v2/ticker/1/?convert=' + target); + + } + // tradecx + else if (exchange == "tradecx") { + apiInterfaces.jsonHttpRequest('tradecx.io', 443, '', function(error, response) { + + if (error) log('error', logSystem, 'API request to %s has failed: %s', [exchange, error]); + if (response.message) log('warn', logSystem, 'CoinMarketCap API error: %s', [response.message]); + + error = response.message ? response.message : error; + + price = response ? +response.data.quotes[ticker].price : null; + if (!price) log('warn', logSystem, 'No exchange data for %s using %s', [ticker, exchange]); + + data = { ticker: ticker, price: price, source: exchange } + if (!error) priceRequestsCache[cacheKey] = { ts: currentTimestamp, data: data }; + callback(error, data); + }, '/v2/tickers/' + target); + + } + // Unknown + else { + callback('Exchange not supported: ' + exchange); + } +} +exports.getExchangePrice = getExchangePrice; diff --git a/lib/notifications.js b/lib/notifications.js new file mode 100644 index 0000000..5bebaa7 --- /dev/null +++ b/lib/notifications.js @@ -0,0 +1,308 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Notifications system + * Supports: email, telegram + * + * Author: Daniel Vandal + **/ + +// Load required modules +let fs = require('fs'); + +let emailSystem = require('./email.js'); +let telegram = require('./telegram.js'); +let utils = require('./utils.js'); + +// Initialize log system +let logSystem = 'notifications'; +require('./exceptionWriter.js')(logSystem); + +// Load notification settings +let notificationSettings = { + emailTemplate: "email/template.txt", + emailSubject: { + emailAdded: "Your email was registered", + workerConnected: "Worker %WORKER_NAME% connected", + workerTimeout: "Worker %WORKER_NAME% stopped hashing", + workerBanned: "Worker %WORKER_NAME% banned", + blockFound: "Block %HEIGHT% found !", + blockUnlocked: "Block %HEIGHT% unlocked !", + blockOrphaned: "Block %HEIGHT% orphaned !", + payment: "We sent you a payment !" + }, + emailMessage: { + emailAdded: "Your email has been registered to receive pool notifications.", + workerConnected: "Your worker %WORKER_NAME% is now connected.", + workerTimeout: "Your worker %WORKER_NAME% has stopped submitting hashes on %LAST_HASH%.", + workerBanned: "Your worker %WORKER_NAME% has been banned.", + blockFound: "Block found at height %HEIGHT% by miner %MINER% on %TIME%. Waiting maturity.", + blockUnlocked: "Block mined at %HEIGHT% with %REWARD% and %EFFORT% effort on %TIME%.", + blockOrphaned: "Block orphaned at height %HEIGHT% :(", + payment: "A payment of %AMOUNT% has been sent to %ADDRESS% wallet." + }, + telegramMessage: { + workerConnected: "Your worker _%WORKER_NAME%_ is now connected.", + workerTimeout: "Your worker _%WORKER_NAME%_ has stopped submitting hashes on _%LAST_HASH%_.", + workerBanned: "Your worker _%WORKER_NAME%_ has been banned.", + blockFound: "*Block found at height _%HEIGHT%_ by miner _%MINER%_. Waiting maturity.*", + blockUnlocked: "*Block mined at _%HEIGHT%_ with _%REWARD%_ and _%EFFORT%_ effort on _%TIME%_.*", + blockOrphaned: "*Block orphaned at height _%HEIGHT%_ :(*", + payment: "A payment of _%AMOUNT%_ has been sent." + } +}; + +if (config.notifications) { + Object.assign(notificationSettings, config.notifications); +} + +// Test notification message +notificationSettings.emailSubject['test'] = "Test notification"; +notificationSettings.emailMessage['test'] = "This is a test notification from the pool."; +notificationSettings.telegramMessage['test'] = "This is a test notification from the pool."; + +/** + * Send global notification + **/ +exports.sendToAll = function(id, variables) { + // Send telegram to channel + sendToTelegramChannel(id, variables); + + // Send blocks notifications to telegram + if (id === "blockFound" || id === "blockUnlocked" || id === "blockOrphaned") { + sendBlockTelegram(id, variables); + } + + // Send to all pool email addresses + sendToAllEmails(id, variables); +} + +/** + * Send miner notification + **/ +exports.sendToMiner = function(miner, id, variables) { + // Send telegram + sendToMinerTelegram(miner, id, variables); + + // Send email + sendToMinerEmail(miner, id, variables); +} + +/** + * Send telegram channel notification + **/ +function sendToTelegramChannel(id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send notification + if (config.telegram && config.telegram.enabled) { + let message = getTelegramMessage(id, variables); + if (!message || message === '') { + log('info', logSystem, 'Notification disabled for %s: empty telegram message.', [id]); + return ; + } + + let channel = config.telegram.channel.replace(/@/g, '') || ''; + if (!channel) { + log('error', logSystem, 'No telegram channel specified in configuration!'); + return ; + } + + let chatId = '@' + channel; + telegram.sendMessage(chatId, message); + } +} +exports.sendToTelegramChannel = sendToTelegramChannel; + +/** + * Send telegram to miner in private message + **/ +function sendToMinerTelegram(miner, id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send telegram + if (config.telegram && config.telegram.enabled) { + let message = getTelegramMessage(id, variables); + if (!message || message === '') { + log('info', logSystem, 'Notification disabled for %s: empty telegram message.', [id]); + return; + } + + redisClient.hget(config.coin + ':telegram', miner, function(error, chatId) { + if (error || !chatId) return; + telegram.sendMessage(chatId, message); + }); + } +} +exports.sendToMinerTelegram = sendToMinerTelegram; + +/** + * Send block notification telegram to miner in private message + **/ +function sendBlockTelegram(id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send telegram + if (config.telegram && config.telegram.enabled) { + let message = getTelegramMessage(id, variables); + if (!message || message === '') { + log('info', logSystem, 'Notification disabled for %s: empty telegram message.', [id]); + return; + } + + redisClient.hgetall(config.coin + ':telegram:blocks', function(error, data) { + if (error || !data) return ; + for (let chatId in data) { + if (!chatId) continue; + telegram.sendMessage(chatId, message); + } + }); + } +} +exports.sendBlockTelegram = sendBlockTelegram; + +/** + * Send email notification to all pool email addresses + **/ +function sendToAllEmails(id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send email + if (config.email && config.email.enabled) { + let subject = getEmailSubject(id, variables); + let content = getEmailContent(id, variables); + if (!content || content === '') { + log('info', logSystem, 'Notification disabled for %s: empty email content.', [id]); + return; + } + + redisClient.hgetall(config.coin + ':notifications', function(error, data) { + if (error || !data) return ; + for (let address in data) { + let email = data[address]; + emailSystem.sendEmail(email, subject, content); + } + }); + } +} +exports.sendToAllEmails = sendToAllEmails; + +/** + * Send email notification to miner email address + **/ +function sendToMinerEmail(miner, id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send email + if (config.email && config.email.enabled) { + let subject = getEmailSubject(id, variables); + let content = getEmailContent(id, variables); + if (!content || content === '') { + log('info', logSystem, 'Notification disabled for %s: empty email content.', [id]); + return; + } + + redisClient.hget(config.coin + ':notifications', miner, function(error, email) { + if (error || !email) return ; + emailSystem.sendEmail(email, subject, content); + }); + } +} +exports.sendToMinerEmail = sendToMinerEmail; + +/** + * Send email notification to a specific email address + **/ +function sendToEmail(email, id, variables) { + // Set custom variables + variables = setCustomVariables(variables); + + // Send notification + if (config.email && config.email.enabled) { + let subject = getEmailSubject(id, variables); + let content = getEmailContent(id, variables); + if (!content || content === '') { + log('info', logSystem, 'Notification disabled for %s: empty email content.', [id]); + return; + } + + emailSystem.sendEmail(email, subject, content); + } +} +exports.sendToEmail = sendToEmail; + +/** + * Email functions + **/ + +// Get email subject +function getEmailSubject(id, variables) { + let subject = replaceVariables(notificationSettings.emailSubject[id], variables) || ''; + return subject; +} + +// Get email content +function getEmailContent(id, variables) { + let message = notificationSettings.emailMessage[id] || ''; + if (!message || message === '') return ''; + + let content = message; + if (notificationSettings.emailTemplate) { + if (!fs.existsSync(notificationSettings.emailTemplate)) { + log('warn', logSystem, 'Email template file not found: %s', [notificationSettings.emailTemplate]); + } + content = fs.readFileSync(notificationSettings.emailTemplate, 'utf8'); + content = content.replace(/%MESSAGE%/g, message); + } + content = replaceVariables(content, variables); + return content; +} + +/** + * Telegram functions + **/ + +// Get telegram message +function getTelegramMessage(id, variables) { + let telegramVars = {}; + if (telegramVars) { + for (let varName in variables) { + let value = variables[varName].toString(); + value = value.replace(/\*/g, '.'); + value = value.replace(/_/g, ' '); + telegramVars[varName] = value; + } + } + let message = replaceVariables(notificationSettings.telegramMessage[id], telegramVars) || ''; + return message; +} + +/** + * Handle variables in texts + **/ + +// Set custom variables +function setCustomVariables(variables) { + if (!variables) variables = {}; + variables['TIME'] = utils.dateFormat(Date.now(), 'yyyy-mm-dd HH:MM:ss Z'); + variables['POOL_HOST'] = config.poolHost || ''; + return variables; +} + +// Replace variables in a string +function replaceVariables(string, variables) { + if (!string) return ''; + if (variables) { + for (let varName in variables) { + string = string.replace(new RegExp('%'+varName+'%', 'g'), variables[varName]); + } + string = string.replace(/ /g, ' '); + } + return string; +} diff --git a/lib/paymentProcessor.js b/lib/paymentProcessor.js new file mode 100644 index 0000000..c8e9aeb --- /dev/null +++ b/lib/paymentProcessor.js @@ -0,0 +1,291 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Payments processor + **/ + +// Load required modules +let fs = require('fs'); +let async = require('async'); + +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let notifications = require('./notifications.js'); +let utils = require('./utils.js'); + +// Initialize log system +let logSystem = 'payments'; +require('./exceptionWriter.js')(logSystem); + +/** + * Run payments processor + **/ + +log('info', logSystem, 'Started'); + +if (!config.poolServer.paymentId) config.poolServer.paymentId = {}; +if (!config.poolServer.paymentId.addressSeparator) config.poolServer.paymentId.addressSeparator = "+"; +if (!config.payments.priority) config.payments.priority = 0; + +function runInterval(){ + async.waterfall([ + + // Get worker keys + function(callback){ + redisClient.keys(config.coin + ':workers:*', function(error, result) { + if (error) { + log('error', logSystem, 'Error trying to get worker balances from redis %j', [error]); + callback(true); + return; + } + callback(null, result); + }); + }, + + // Get worker balances + function(keys, callback){ + let redisCommands = keys.map(function(k){ + return ['hget', k, 'balance']; + }); + redisClient.multi(redisCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with getting balances from redis %j', [error]); + callback(true); + return; + } + + let balances = {}; + for (let i = 0; i < replies.length; i++){ + let parts = keys[i].split(':'); + let workerId = parts[parts.length - 1]; + + balances[workerId] = parseInt(replies[i]) || 0; + } + callback(null, keys, balances); + }); + }, + + // Get worker minimum payout + function(keys, balances, callback){ + let redisCommands = keys.map(function(k){ + return ['hget', k, 'minPayoutLevel']; + }); + redisClient.multi(redisCommands).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Error with getting minimum payout from redis %j', [error]); + callback(true); + return; + } + + let minPayoutLevel = {}; + for (let i = 0; i < replies.length; i++){ + let parts = keys[i].split(':'); + let workerId = parts[parts.length - 1]; + + let minLevel = config.payments.minPayment; + let maxLevel = config.payments.maxPayment; + let defaultLevel = minLevel; + + let payoutLevel = parseInt(replies[i]) || minLevel; + if (payoutLevel < minLevel) payoutLevel = minLevel; + if (maxLevel && payoutLevel > maxLevel) payoutLevel = maxLevel; + minPayoutLevel[workerId] = payoutLevel; + + if (payoutLevel !== defaultLevel) { + log('info', logSystem, 'Using payout level of %s for %s (default: %s)', [ utils.getReadableCoins(minPayoutLevel[workerId]), workerId, utils.getReadableCoins(defaultLevel) ]); + } + } + callback(null, balances, minPayoutLevel); + }); + }, + + // Filter workers under balance threshold for payment + function(balances, minPayoutLevel, callback){ + let payments = {}; + + for (let worker in balances){ + let balance = balances[worker]; + if (balance >= minPayoutLevel[worker]){ + let remainder = balance % config.payments.denomination; + let payout = balance - remainder; + + if (config.payments.dynamicTransferFee && config.payments.minerPayFee){ + payout -= config.payments.transferFee; + } + if (payout < 0) continue; + + payments[worker] = payout; + } + } + + if (Object.keys(payments).length === 0){ + log('info', logSystem, 'No workers\' balances reached the minimum payment threshold'); + callback(true); + return; + } + + let transferCommands = []; + let addresses = 0; + let commandAmount = 0; + let commandIndex = 0; + let ringSize = config.payments.ringSize ? config.payments.ringSize : config.payments.mixin; + + for (let worker in payments){ + let amount = parseInt(payments[worker]); + if(config.payments.maxTransactionAmount && amount + commandAmount > config.payments.maxTransactionAmount) { + amount = config.payments.maxTransactionAmount - commandAmount; + } + + let address = worker; + let payment_id = null; + + let with_payment_id = false; + + let addr = address.split(config.poolServer.paymentId.addressSeparator); + if ((addr.length === 1 && utils.isIntegratedAddress(address)) || addr.length >= 2){ + with_payment_id = true; + if (addr.length >= 2){ + address = addr[0]; + payment_id = addr[1]; + payment_id = payment_id.replace(/[^A-Za-z0-9]/g,''); + if (payment_id.length !== 16 && payment_id.length !== 64) { + with_payment_id = false; + payment_id = null; + } + } + if (addresses > 0){ + commandIndex++; + addresses = 0; + commandAmount = 0; + } + } + + if (config.poolServer.fixedDiff && config.poolServer.fixedDiff.enabled) { + let addr = address.split(config.poolServer.fixedDiff.addressSeparator); + if (addr.length >= 2) address = addr[0]; + } + + if(!transferCommands[commandIndex]) { + transferCommands[commandIndex] = { + redis: [], + amount : 0, + rpc: { + destinations: [], + fee: config.payments.transferFee, + priority: config.payments.priority, + unlock_time: 0 + } + }; + if (config.payments.ringSize) + transferCommands[commandIndex].rpc.ring_size = ringSize; + else + transferCommands[commandIndex].rpc.mixin = ringSize; + } + + transferCommands[commandIndex].rpc.destinations.push({amount: amount, address: address}); + if (payment_id) transferCommands[commandIndex].rpc.payment_id = payment_id; + + transferCommands[commandIndex].redis.push(['hincrby', config.coin + ':workers:' + worker, 'balance', -amount]); + if(config.payments.dynamicTransferFee && config.payments.minerPayFee){ + transferCommands[commandIndex].redis.push(['hincrby', config.coin + ':workers:' + worker, 'balance', -config.payments.transferFee]); + } + transferCommands[commandIndex].redis.push(['hincrby', config.coin + ':workers:' + worker, 'paid', amount]); + transferCommands[commandIndex].amount += amount; + + addresses++; + commandAmount += amount; + + if (config.payments.dynamicTransferFee){ + transferCommands[commandIndex].rpc.fee = config.payments.transferFee * addresses; + } + + if (addresses >= config.payments.maxAddresses || (config.payments.maxTransactionAmount && commandAmount >= config.payments.maxTransactionAmount) || with_payment_id) { + commandIndex++; + addresses = 0; + commandAmount = 0; + } + } + + let timeOffset = 0; + let notify_miners = []; + + async.filterSeries(transferCommands, function(transferCmd, cback){ + let rpcCommand = "transfer"; + let rpcRequest = transferCmd.rpc; + + apiInterfaces.rpcWallet(rpcCommand, rpcRequest, function(error, result){ + if (error){ + log('error', logSystem, 'Error with %s RPC request to wallet daemon %j', [rpcCommand, error]); + log('error', logSystem, 'Payments failed to send to %j', transferCmd.rpc.destinations); + cback(false); + return; + } + + let now = (timeOffset++) + Date.now() / 1000 | 0; + let txHash = result.tx_hash; + txHash = txHash.replace('<', '').replace('>', ''); + + transferCmd.redis.push(['zadd', config.coin + ':payments:all', now, [ + txHash, + transferCmd.amount, + transferCmd.rpc.fee, + ringSize, + Object.keys(transferCmd.rpc.destinations).length + ].join(':')]); + + let notify_miners_on_success = []; + for (let i = 0; i < transferCmd.rpc.destinations.length; i++){ + let destination = transferCmd.rpc.destinations[i]; + if (transferCmd.rpc.payment_id){ + destination.address += config.poolServer.paymentId.addressSeparator + transferCmd.rpc.payment_id; + } + transferCmd.redis.push(['zadd', config.coin + ':payments:' + destination.address, now, [ + txHash, + destination.amount, + transferCmd.rpc.fee, + ringSize + ].join(':')]); + + notify_miners_on_success.push(destination); + } + + log('info', logSystem, 'Payments sent via wallet daemon %j', [result]); + redisClient.multi(transferCmd.redis).exec(function(error, replies){ + if (error){ + log('error', logSystem, 'Super critical error! Payments sent yet failing to update balance in redis, double payouts likely to happen %j', [error]); + log('error', logSystem, 'Double payments likely to be sent to %j', transferCmd.rpc.destinations); + cback(false); + return; + } + + for (let m in notify_miners_on_success) { + notify_miners.push(notify_miners_on_success[m]); + } + + cback(true); + }); + }); + }, function(succeeded){ + let failedAmount = transferCommands.length - succeeded.length; + + for (let m in notify_miners) { + let notify = notify_miners[m]; + log('info', logSystem, 'Payment of %s to %s', [ utils.getReadableCoins(notify.amount), notify.address ]); + notifications.sendToMiner(notify.address, 'payment', { + 'ADDRESS': notify.address.substring(0,7)+'...'+notify.address.substring(notify.address.length-7), + 'AMOUNT': utils.getReadableCoins(notify.amount), + }); + } + log('info', logSystem, 'Payments splintered and %d successfully sent, %d failed', [succeeded.length, failedAmount]); + + callback(null); + }); + + } + + ], function(error, result){ + setTimeout(runInterval, config.payments.interval * 1000); + }); +} + +runInterval(); diff --git a/lib/pool.js b/lib/pool.js new file mode 100644 index 0000000..edb3973 --- /dev/null +++ b/lib/pool.js @@ -0,0 +1,1407 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Pool TCP daemon + **/ + +// Load required modules +let fs = require('fs'); +let net = require('net'); +let tls = require('tls'); +let async = require('async'); +let bignum = require('bignum'); +let socketMap = new Set() +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let notifications = require('./notifications.js'); +let utils = require('./utils.js'); + +config.hashingUtil = config.hashingUtil || false; +let cnHashing = require('cryptonight-hashing'); +if (config.hashingUtil) + cnHashing = require('turtlecoin-multi-hashing'); + +// Set nonce pattern - must exactly be 8 hex chars +let noncePattern = new RegExp("^[0-9A-Fa-f]{16}$"); + +// Set redis database cleanup interval +let cleanupInterval = config.redis.cleanupInterval && config.redis.cleanupInterval > 0 ? config.redis.cleanupInterval : 15; +let fallBackCoin = typeof config.poolServer.fallBackCoin !== 'undefined' && config.poolServer.fallBackCoin ? config.poolServer.fallBackCoin : 0 + +// Initialize log system +let logSystem = 'pool'; +require('./exceptionWriter.js')(logSystem); + +let threadId = '(Thread ' + process.env.forkId + ') '; +let log = function(severity, system, text, data){ + global.log(severity, system, threadId + text, data); +}; + +// Set cryptonight algorithm +let cnAlgorithm = config.cnAlgorithm || "cryptonight"; +let cnVariant = config.cnVariant || 0; +let cnBlobType = config.cnBlobType || 0; + +let cryptoNight; +if (!cnHashing || !cnHashing[cnAlgorithm]) { + log('error', logSystem, 'Invalid cryptonight algorithm: %s', [cnAlgorithm]); +} else { + cryptoNight = cnHashing[cnAlgorithm]; +} + +// Set instance id +let instanceId = utils.instanceId(); + +// Pool variables +let poolStarted = false; +let connectedMiners = {}; +// Get merged mining tag reseved space size +let POOL_NONCE_SIZE = 16 + 1; // +1 for old XMR/new TRTL bugs +let EXTRA_NONCE_TEMPLATE = "02" + POOL_NONCE_SIZE.toString(16) + "00".repeat(POOL_NONCE_SIZE); +let POOL_NONCE_MM_SIZE = POOL_NONCE_SIZE + utils.cnUtil.get_merged_mining_nonce_size(); +let EXTRA_NONCE_NO_CHILD_TEMPLATE = "02" + POOL_NONCE_MM_SIZE.toString(16) + "00".repeat(POOL_NONCE_MM_SIZE); +let mergedMining = config.poolServer.mergedMining && (Array.isArray(config.childPools) && config.childPools.length > 0) + +function randomIntFromInterval(min,max){ + return Math.floor(Math.random()*(max-min+1)+min); +} + + +// Pool settings +let shareTrustEnabled = config.poolServer.shareTrust && config.poolServer.shareTrust.enabled; +let shareTrustStepFloat = shareTrustEnabled ? config.poolServer.shareTrust.stepDown / 100 : 0; +let shareTrustMinFloat = shareTrustEnabled ? config.poolServer.shareTrust.min / 100 : 0; + +let banningEnabled = config.poolServer.banning && config.poolServer.banning.enabled; +let bannedIPs = {}; +let perIPStats = {}; + + +let slushMiningEnabled = config.poolServer.slushMining && config.poolServer.slushMining.enabled; + +if (!config.poolServer.paymentId) config.poolServer.paymentId = {}; +if (!config.poolServer.paymentId.addressSeparator) config.poolServer.paymentId.addressSeparator = "+"; + +if (config.poolServer.paymentId.validation == null) config.poolServer.paymentId.validation = true; +if (config.poolServer.paymentId.ban == null) config.poolServer.paymentId.ban = false; +if (config.poolServer.paymentId.validations == null) { + config.poolServer.paymentId.validations = [] + config.poolServer.paymentId.validation = false; +} + +config.isRandomX = config.isRandomX || false + +let previousOffset = config.previousOffset || 7 +let offset = config.offset || 2 +config.daemonType = config.daemonType || 'default' +if (config.daemonType === 'bytecoin') +{ + previousOffset = config.previousOffset || 3 + offset = config.offset || 3 +} + +function Create2DArray(rows) { + let arr = []; + + for (let i=0;i pool.enabled) + + +// Block templates +let validBlockTemplates = mergedMining ? Create2DArray(config.childPools.length) : Create2DArray(1); +let currentBlockTemplate = []; + + +// Child Block templates +let currentChildBlockTemplate = new Array(mergedMining ? config.childPools.length : 1); + + +// Difficulty buffer +let diff1 = bignum('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 16); + +/** + * Convert buffer to byte array + **/ +Buffer.prototype.toByteArray = function () { + return Array.prototype.slice.call(this, 0); +}; + +/** + * Periodical updaters + **/ + +// Variable difficulty retarget +setInterval(function(){ + let now = Date.now() / 1000 | 0; + for (let minerId in connectedMiners){ + let miner = connectedMiners[minerId]; + if(!miner.noRetarget) { + miner.retarget(now); + } + } +}, config.poolServer.varDiff.retargetTime * 1000); + +// Every 30 seconds clear out timed-out miners and old bans +setInterval(function(){ + let now = Date.now(); + let timeout = config.poolServer.minerTimeout * 1000; + for (let minerId in connectedMiners){ + let miner = connectedMiners[minerId]; + if (now - miner.lastBeat > timeout){ + log('warn', logSystem, 'Miner timed out and disconnected %s@%s', [miner.login, miner.ip]); + delete connectedMiners[minerId]; + removeConnectedWorker(miner, 'timeout'); + } + } + + if (banningEnabled){ + for (ip in bannedIPs){ + let banTime = bannedIPs[ip]; + if (now - banTime > config.poolServer.banning.time * 1000) { + delete bannedIPs[ip]; + delete perIPStats[ip]; + log('info', logSystem, 'Ban dropped for %s', [ip]); + } + } + } + +}, 30000); + +/** + * Handle multi-thread messages + **/ +process.on('message', function(message) { + switch (message.type) { + case 'banIP': + bannedIPs[message.ip] = Date.now(); + break; + case 'BlockTemplate': + let buffer = Buffer.from(message.block.blocktemplate_blob, 'hex'); + let new_hash = Buffer.alloc(32); + buffer.copy(new_hash, 0, previousOffset, 39); + try { + log('info', logSystem, 'New %s block to mine at height %d w/ difficulty of %d (%d transactions)', [config.coin, message.block.height, message.block.difficulty, (message.block.num_transactions || 0)]); + if (mergedMining) { + for (var childPoolIndex = 0; childPoolIndex < config.childPools.length; childPoolIndex++) { + processBlockTemplate(message.block, childPoolIndex); + } + } else { + processBlockTemplate(message.block, 0); + } + return; + }catch(e){log('error', logSystem, `BlockTemplate ${e}`)} + break; + case 'ChildBlockTemplate': + let poolIndex = parseInt(message.poolIndex) + try { + if (!currentChildBlockTemplate[poolIndex] || message.block.height > currentChildBlockTemplate[poolIndex].height || (currentChildBlockTemplate[poolIndex].num_transactions == 0 && message.block.num_transactions > 0)) { + log('info', logSystem, 'New %s child block to mine at height %d w/ difficulty of %d (%d transactions)', [config.childPools[poolIndex].coin, message.block.height, message.block.difficulty, (message.block.num_transactions || 0)]); + processChildBlockTemplate(poolIndex, message.block); + return; + }else{ + return; + } + }catch(e){log('error', logSystem, `ChildBlockTemplate ${e}`)} + + break; + } +}); + +/** + * Block template + **/ +function BlockTemplate(template, parent, indexOfChildPool){ + this.blob = template.blocktemplate_blob + + this.difficulty = template.difficulty + this.height = template.height + this.seed_hash = template.seed + + this.reserveOffset = template.reserved_offset; + // Set this.buffer to the binary decoded version of the BT blob. + this.buffer = Buffer.from(this.blob, 'hex'); + // Generate a clean, shiny new buffer. + this.previous_hash = Buffer.alloc(32); + // Copy in bytes 9 through 41 to this.previous_hash from the current BT. + this.buffer.copy(this.previous_hash, 0, 9, 41); + // 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.bits = template.bits + + this.buffer = Buffer.from(this.blob, 'hex') + + //this.hash = this.idHash = this.prev_hash = template.hash + this.block_version = 0 + + } +BlockTemplate.prototype = { + nextBlob: function(idBuffer) { + return utils.cnUtil.get_hash_from_block_template_with_extra(this.buffer, idBuffer).toString('hex') + }, + nextBlobWithChildNonce: function() { + return this.buffer.toString('hex') + } +} + + +/** + * Process block template + **/ +function processBlockTemplate(template, indexOfChildPool){ + let block_template = new BlockTemplate(template, true, indexOfChildPool) + + if (currentBlockTemplate[indexOfChildPool]) + validBlockTemplates[indexOfChildPool].push(currentBlockTemplate[indexOfChildPool]); + + while (validBlockTemplates[indexOfChildPool].length > (mergedMining ? 6 : 3)) + validBlockTemplates[indexOfChildPool].shift(); + + currentBlockTemplate[indexOfChildPool] = block_template; + notifyConnectedMiners(indexOfChildPool) +} + + + +/** + * Process child block template + **/ +function processChildBlockTemplate(indexOfChildPool, template){ + let block_template = new BlockTemplate(template, false); + + currentChildBlockTemplate[indexOfChildPool] = block_template; + + // Update the parent block template to include this new child + if (currentBlockTemplate[indexOfChildPool]){ + processBlockTemplate(currentBlockTemplate[indexOfChildPool], indexOfChildPool); + } +} + +function notifyConnectedMiners(indexOfChildPool){ + let now = Date.now() / 1000 | 0; + for (let minerId in connectedMiners){ + let miner = connectedMiners[minerId]; + if (indexOfChildPool === miner.activeChildPool) + miner.sendReply(null, miner.getJob(), true) + //miner.pushMessage('job', miner.getJob()); + } +} + +/** + * Variable difficulty + **/ +let VarDiff = (function(){ + let variance = config.poolServer.varDiff.variancePercent / 100 * config.poolServer.varDiff.targetTime; + return { + variance: variance, + bufferSize: config.poolServer.varDiff.retargetTime / config.poolServer.varDiff.targetTime * 4, + tMin: config.poolServer.varDiff.targetTime - variance, + tMax: config.poolServer.varDiff.targetTime + variance, + maxJump: config.poolServer.varDiff.maxJump + }; +})(); + +function GetRewardTypeAsKey(rewardType){ + switch (rewardType) { + case 'solo': + return ':solo' + case 'prop': + return '' + default: + return '' + } +} + +/** + * Miner + **/ +function Miner(rewardType, childRewardType, id, childPoolIndex, login, pass, ip, port, agent, childLogin, startingDiff, noRetarget, pushMessage, sendReply){ + this.rewardType = rewardType + this.childRewardType = childRewardType + this.rewardTypeAsKey = GetRewardTypeAsKey(rewardType) + this.childRewardTypeAsKey = GetRewardTypeAsKey(childRewardType) + + this.lastChildBlockHeight = 0 + this.id = id; + this.activeChildPool = childPoolIndex || 0; + this.login = login; + this.pass = pass; + this.ip = ip; + this.port = port; + this.proxy = false; + if (agent && agent.includes('xmr-node-proxy')) { + this.proxy = true; + } + this.workerName = 'undefined'; + this.childLogin = childLogin; + if (pass.lastIndexOf('@') >= 0 && pass.lastIndexOf('@') < pass.length) { + passDelimiterPos = pass.lastIndexOf('@') + 1; + this.workerName = pass.substr(passDelimiterPos, pass.length).trim(); + } + this.pushMessage = pushMessage; + this.sendReply = sendReply + this.heartbeat(); + this.noRetarget = noRetarget; + this.difficulty = startingDiff; + this.validJobs = []; + this.workerName2 = pass; + + // Vardiff related variables + this.shareTimeRing = utils.ringBuffer(16); + this.lastShareTime = Date.now() / 1000 | 0; + + if (shareTrustEnabled) { + this.trust = { + threshold: config.poolServer.shareTrust.threshold, + probability: 1, + penalty: 0 + }; + } +} +Miner.prototype = { + retarget: function(now){ + + let options = config.poolServer.varDiff; + + let sinceLast = now - this.lastShareTime; + let decreaser = sinceLast > VarDiff.tMax; + + let avg = this.shareTimeRing.avg(decreaser ? sinceLast : null); + let newDiff; + + let direction; + + if (avg > VarDiff.tMax && this.difficulty > options.minDiff){ + newDiff = options.targetTime / avg * this.difficulty; + newDiff = newDiff > options.minDiff ? newDiff : options.minDiff; + direction = -1; + } + else if (avg < VarDiff.tMin && this.difficulty < options.maxDiff){ + newDiff = options.targetTime / avg * this.difficulty; + newDiff = newDiff < options.maxDiff ? newDiff : options.maxDiff; + direction = 1; + } + else{ + return; + } + + if (Math.abs(newDiff - this.difficulty) / this.difficulty * 100 > options.maxJump){ + let change = options.maxJump / 100 * this.difficulty * direction; + newDiff = this.difficulty + change; + } + + this.setNewDiff(newDiff); + this.shareTimeRing.clear(); + if (decreaser) this.lastShareTime = now; + }, + setNewDiff: function(newDiff){ + newDiff = Math.round(newDiff); + if (this.difficulty === newDiff) return; + log('info', logSystem, 'Retargetting difficulty %d to %d for %s', [this.difficulty, newDiff, this.login]); + this.pendingDifficulty = newDiff; + this. sendReply(null, this.getJob(), true) + }, + heartbeat: function(){ + this.lastBeat = Date.now(); + }, + getTargetHex: function(){ + if (this.pendingDifficulty){ + this.lastDifficulty = this.difficulty; + this.difficulty = this.pendingDifficulty; + this.pendingDifficulty = null; + } + + let padded = Buffer.alloc(32); + padded.fill(0); + + let diffBuff = diff1.div(this.difficulty).toBuffer(); + diffBuff.copy(padded, 32 - diffBuff.length); + + let buff = padded.slice(0, 4); + let buffArray = buff.toByteArray().reverse(); + let buffReversed = Buffer.from(buffArray); + this.target = buffReversed.readUInt32BE(0); + let hex = buffReversed.toString('hex'); + return hex; + }, + getJob: function(){ + let idBuffer = utils.instanceId(4) + let blockTemplate = currentBlockTemplate[this.activeChildPool] + if (mergedMining) { + if (this.lastBlockHeight === blockTemplate.height + && (!currentChildBlockTemplate[this.activeChildPool] || this.lastChildBlockHeight === currentChildBlockTemplate[this.activeChildPool].height) + && !this.pendingDifficulty + && this.cachedJob !== null + && !config.daemon.alwaysPoll) { + return this.cachedJob; + } + this.lastChildBlockHeight = currentChildBlockTemplate ? currentChildBlockTemplate[this.activeChildPool].height : -1; + newJob.activeChildPool = this.activeChildPool + newJob.childHeight = this.lastChildBlockHeight + } + + let blob = this.proxy ? blockTemplate.nextBlobWithChildNonce() : blockTemplate.nextBlob(idBuffer); + this.lastBlockHeight = blockTemplate.height; + let target = this.getTargetHex(); + + let heightBuffer = Buffer.alloc(8) + heightBuffer.writeUInt32BE(blockTemplate.height, 4) + let newJob = { + id: blob, + unique_id: idBuffer, + height: blockTemplate.height, + submissions: [], + difficulty: miner.difficulty, + extraNonce: blockTemplate.extraNonce + } + +let padded = Buffer.alloc(32) +padded.fill(0) +let diffBuff = diff1.div(miner.difficulty).toBuffer() +diffBuff.copy(padded, 32 - diffBuff.length) +let buff = padded.slice(0, 32) +target = buff.toString('hex') + + this.validJobs.push(newJob); + + while (this.validJobs.length > 4) + this.validJobs.shift(); + + this.cachedJob = [`0x${blob}`, `0x${blockTemplate.seed_hash}`, `0x${target}`, `0x${heightBuffer.toString('hex')}`] + + + if (this.proxy) { + newJob.clientPoolLocation = blockTemplate.clientPoolLocation + newJob.clientNonceLocation = blockTemplate.clientNonceLocation + } + + return this.cachedJob; + }, + checkBan: function(validShare){ + if (!banningEnabled) return; + + // Init global per-ip shares stats + if (!perIPStats[this.ip]){ + perIPStats[this.ip] = { validShares: 0, invalidShares: 0 }; + } + + let stats = perIPStats[this.ip]; + validShare ? stats.validShares++ : stats.invalidShares++; + + if (stats.validShares + stats.invalidShares >= config.poolServer.banning.checkThreshold){ + if (stats.invalidShares / stats.validShares >= config.poolServer.banning.invalidPercent / 100){ + validShare ? this.validShares++ : this.invalidShares++; + log('warn', logSystem, 'Banned %s@%s', [this.login, this.ip]); + bannedIPs[this.ip] = Date.now(); + delete connectedMiners[this.id]; + process.send({type: 'banIP', ip: this.ip}); + removeConnectedWorker(this, 'banned'); + } + else{ + stats.invalidShares = 0; + stats.validShares = 0; + } + } + } +}; + +validateMinerPaymentId_difficulty = (address, ip, poolServerConfig, coin, sendReply) => { + if (utils.characterCount(address, '\\+') > 1) { + let message = `Invalid paymentId specified for ${coin}login, ${ip}`; + if (poolServerConfig.paymentId.validation) { + process.send({type: 'banIP', ip: ip}); + message += ` banned for ${poolServerConfig.banning.time / 60} minutes` + } + sendReply(message) + log('warn', logSystem, message); + return false + } + + if (utils.characterCount(address, '\\.') > 1) { + log('warn', logSystem, `Invalid difficulty specified for ${coin}login`); + sendReply(`Invalid difficulty specified for ${coin}login, ${ip}`) + return false + } + return true +} + +/** + * Handle miner method + **/ +function handleMinerMethod(socket, method, params, ip, portData, sendReply, pushMessage){ + + // Check for ban here, so preconnected attackers can't continue to screw you + if (IsBannedIp(ip)){ + sendReply('Your IP is banned'); + return; + } + + switch(method){ + case 'eth_submitLogin': + if (!params || !(params instanceof Array)) { + sendReply("No array params specified") + return + } + params = { + login: params[0], + pass: params[1] + }; + case 'login': + let login = params.login ? params.login.trim() : null; + if (!login){ + sendReply('Missing login'); + return; + } + + if (!validateMinerPaymentId_difficulty(login, ip, config.poolServer, 'parent ', sendReply)) + return + + let workerName = params.rigid ? params.rigid.trim() : 'undefined' + if (login.indexOf('@') >= 0 && login.lastIndexOf('@') < login.length) { + passDelimiterPos = login.lastIndexOf('@'); + workerName = login.substr(passDelimiterPos + 1, login.length).trim() + login = login.substr(0, passDelimiterPos).trim(); + } + + let calculated = utils.determineRewardData(login) + login = calculated.address + let rewardType = calculated.rewardType + + let address = '' + let paymentid = null + let port = portData.port; + let pass = params.pass; + let childLogin = pass.trim(); + let childPoolIndex = 0; + let childRewardType = rewardType + if (mergedMining) { + childPoolIndex = -1 + if (!validateMinerPaymentId_difficulty(pass, ip, config.poolServer, 'child ', sendReply)) + return + + calculated = utils.determineRewardData(pass) + pass = calculated.address + childRewardType = calculated.rewardType + + if (pass.indexOf('@') >= 0 && pass.indexOf('@') >= 0) { + passDelimiterPos = pass.lastIndexOf('@'); + childLogin = pass.substr(0, passDelimiterPos).trim(); + } + childLogin = childLogin.replace(/\s/g, ''); + childLogin = utils.cleanupSpecialChars(childLogin); + + + let addr = childLogin.split(config.poolServer.paymentId.addressSeparator); + address = addr[0] || null; + paymentId = addr[1] || null + + if (!address) { + log('warn', logSystem, 'No address specified for login'); + sendReply('No address specified for login'); + } + + + if (paymentId && config.poolServer.paymentId.validation) { + let valid = false; + config.poolServer.paymentId.validations.forEach(validation => { + if (paymentId.match(`^([a-zA-Z0-9]){${validation}}$`)) + { + return valid = true + } + }) + if (!valid) { + log('warn', logSystem, 'Invalid paymentId specified for login'); + } + if (config.poolServer.paymentId.ban) { + process.send({type: 'banIP', ip: ip}); + sendReply(`Invalid paymentId specified for login, ${portData.ip} banned for ${config.poolServer.banning.time / 60} minutes`); + } + } + + for (i = 0; i < config.childPools.length; i++) { + if(config.childPools[i].pattern) { + if (new RegExp(config.childPools[i].pattern, 'i').test(address)) + { + childPoolIndex = i + break + } + } + } + if (childPoolIndex < 0) + { + childPoolIndex = fallBackCoin + address = config.childPools[childPoolIndex].poolAddress + childLogin = config.childPools[childPoolIndex].poolAddress + } + if (!utils.validateChildMinerAddress(address, childPoolIndex)) { + let addressPrefix = utils.getAddressPrefix(address); + if (!addressPrefix) addressPrefix = 'N/A'; + + log('warn', logSystem, 'Invalid address used for childLogin (prefix: %s): %s', [addressPrefix, address]); + sendReply('Invalid address used for childLogin'); + return; + } + } + + + let difficulty = portData.difficulty; + let noRetarget = false; + if(config.poolServer.fixedDiff.enabled) { + let fixedDiffCharPos = login.indexOf(config.poolServer.fixedDiff.addressSeparator); + if (fixedDiffCharPos !== -1 && (login.length - fixedDiffCharPos < 32)){ + diffValue = login.substr(fixedDiffCharPos + 1); + difficulty = parseInt(diffValue); + login = login.substr(0, fixedDiffCharPos); + if (!difficulty || difficulty != diffValue) { + log('warn', logSystem, 'Invalid difficulty value "%s" for login: %s', [diffValue, login]); + difficulty = portData.difficulty; + } else { + noRetarget = true; + if (difficulty < config.poolServer.varDiff.minDiff) { + difficulty = config.poolServer.varDiff.minDiff; + } + } + } + } + + addr = login.split(config.poolServer.paymentId.addressSeparator); + address = addr[0] || null; + paymentId = addr[1] || null; + if (!address) { + log('warn', logSystem, 'No address specified for login'); + sendReply('Invalid address used for login'); + return + } + + if (paymentId && paymentId.match('^([a-zA-Z0-9]){0,15}$')) { + if (config.poolServer.paymentId.validation) { + process.send({type: 'banIP', ip: ip}); + log('warn', logSystem, 'Invalid paymentId specified for login'); + } else { + log('warn', logSystem, 'Invalid paymentId specified for login'); + } + sendReply(`Invalid paymentId specified for login, ${portData.ip} banned for ${config.poolServer.banning.time / 60} minutes`) + return; + } + + if (!utils.validateMinerAddress(address)) { + let addressPrefix = utils.getAddressPrefix(address); + if (!addressPrefix) addressPrefix = 'N/A'; + + log('warn', logSystem, 'Invalid address used for login (prefix: %s): %s', [addressPrefix, address]); + sendReply('Invalid address used for login'); + return; + } + + let minerId = utils.uid(); + miner = new Miner(rewardType, childRewardType, minerId, childPoolIndex, login, pass, ip, port, params.agent, childLogin, difficulty, noRetarget, pushMessage, sendReply); + if (!socket.miner_ids) + socket.miner_ids = [] + socket.miner_ids.push(minerId) + connectedMiners[minerId] = miner; + sendReply(null, true) + newConnectedWorker(miner); + break; + case 'eth_getWork': { + const minerId = params.id ? params.id : (socket.miner_ids && socket.miner_ids.length == 1 ? socket.miner_ids[0] : ""); + let miner = connectedMiners[minerId]; + if (!miner) { + sendReply('Unauthenticated'); + return; + } + miner.heartbeat(); + sendReply(null, miner.getJob()); + break + } + case 'getjob': + if (!miner){ + sendReply('Unauthenticated'); + return; + } + miner.heartbeat(); + sendReply(null, miner.getJob()); + break; + case 'eth_submitWork': { + if (!params || !(params instanceof Array)) { + sendReply("No array params specified"); + return; + } + params = { + nonce: params[0].substr(2), + job_id: params[1].substr(2) + } + } + case 'submit': { + const minerId = params.id ? params.id : (socket.miner_ids && socket.miner_ids.length == 1 ? socket.miner_ids[0] : ""); + let miner = connectedMiners[minerId]; + if (!miner){ + sendReply('Unauthenticated'); + return; + } + miner.heartbeat(); + + let job = miner.validJobs.filter(function(job){ + return job.id === params.job_id; + })[0]; + + if (!job){ + sendReply('Invalid job id'); + return; + } + if (typeof params.nonce !== 'string' || !noncePattern.test(params.nonce)) { + let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : ''; + log('warn', logSystem, 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + minerText); + perIPStats[miner.ip] = { validShares: 0, invalidShares: 999999 }; + miner.checkBan(false); + sendReply('Duplicate share1'); + return; + } + + // Force lowercase for further comparison + params.nonce = params.nonce.toLowerCase(); + + if (!miner.proxy) { + if (job.submissions.indexOf(params.nonce) !== -1){ + let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : ''; + log('warn', logSystem, 'Duplicate share: ' + JSON.stringify(params) + ' from ' + minerText); + perIPStats[miner.ip] = { validShares: 0, invalidShares: 999999 }; + miner.checkBan(false); + sendReply('Duplicate share2'); + return; + } + + job.submissions.push(params.nonce); + } else { + if (!Number.isInteger(params.poolNonce) || !Number.isInteger(params.workerNonce)) { + let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : ''; + log('warn', logSystem, 'Malformed nonce: ' + JSON.stringify(params) + ' from ' + minerText); + perIPStats[miner.ip] = { validShares: 0, invalidShares: 999999 }; + miner.checkBan(false); + sendReply('Duplicate share3'); + return; + } + let nonce_test = `${params.nonce}_${params.poolNonce}_${params.workerNonce}`; + if (job.submissions.indexOf(nonce_test) !== -1) { + let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : ''; + log('warn', logSystem, 'Duplicate share: ' + JSON.stringify(params) + ' from ' + minerText); + perIPStats[miner.ip] = { validShares: 0, invalidShares: 999999 }; + miner.checkBan(false); + sendReply('Duplicate share4'); + return; + } + job.submissions.push(nonce_test); + + } + + let isJobBlock = function(b) { + return b.height === job.height && job.childHeight === ( + b.childBlockTemplate ? b.childBlockTemplate.height : undefined); + }; + let blockTemplate = currentBlockTemplate[miner.activeChildPool] + if (job.childHeight) + blockTemplate = isJobBlock(currentBlockTemplate[miner.activeChildPool]) ? currentBlockTemplate[miner.activeChildPool] : validBlockTemplates[miner.activeChildPool].filter(isJobBlock)[0]; + + if (!blockTemplate){ + sendReply('Block expired'); + return; + } + + let shareAccepted = processShare(miner, job, blockTemplate, params); + miner.checkBan(shareAccepted); + + if (shareTrustEnabled){ + if (shareAccepted){ + miner.trust.probability -= shareTrustStepFloat; + if (miner.trust.probability < shareTrustMinFloat) + miner.trust.probability = shareTrustMinFloat; + miner.trust.penalty--; + miner.trust.threshold--; + } + else{ + log('warn', logSystem, 'Share trust broken by %s@%s', [miner.login, miner.ip]); + miner.trust.probability = 1; + miner.trust.penalty = config.poolServer.shareTrust.penalty; + } + } + + if (!shareAccepted){ + sendReply('Rejected share: invalid result'); + return; + } + + let now = Date.now() / 1000 | 0; + miner.shareTimeRing.append(now - miner.lastShareTime); + miner.lastShareTime = now; + + sendReply(null, {status: 'OK'}); + break; + } + case 'keepalived' : + if (!miner){ + sendReply('Unauthenticated'); + return; + } + miner.heartbeat(); + sendReply(null, { status:'KEEPALIVED' }); + break; + default: + sendReply('Invalid method'); + let minerText = miner ? (' ' + miner.login + '@' + miner.ip) : ''; + log('warn', logSystem, 'Invalid method: %s (%j) from %s', [method, params, minerText]); + break; + } +} + +/** + * New connected worker + **/ +function newConnectedWorker(miner){ + log('info', logSystem, 'Miner connected %s@%s on port', [miner.login, miner.ip, miner.port]); + if (miner.workerName !== 'undefined') log('info', logSystem, 'Worker Name: %s', [miner.workerName]); + if (miner.difficulty) log('info', logSystem, 'Miner difficulty fixed to %s', [miner.difficulty]); + + redisClient.sadd(`${config.coin}:workers_ip:${miner.login}`, miner.ip); + redisClient.hincrby(`${config.coin}:ports:${miner.port}`, 'users', 1); + + redisClient.hincrby(`${config.coin}:active_connections${miner.rewardTypeAsKey}`, `${miner.login}~${miner.workerName}`, 1, function(error, connectedWorkers) { + if (connectedWorkers === 1) { + notifications.sendToMiner(miner.login, 'workerConnected', { + 'LOGIN' : miner.login, + 'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`, + 'IP': miner.ip.replace('::ffff:', ''), + 'PORT': miner.port, + 'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : '' + }); + } + }); + if (config.poolServer.mergedMining) { + redisClient.sadd(`${config.childPools[miner.activeChildPool].coin}:workers_ip:${miner.childLogin}`, miner.ip); + redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:ports:${miner.port}`, 'users', 1); + + redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:active_connections${miner.childRewardTypeAsKey}`, `${miner.childLogin}~${miner.workerName}`, 1, function(error, connectedWorkers) { + }); + + + let redisCommands = config.childPools.map(item => { + return ['hdel', `${config.coin}:workers:${miner.login}`, `${item.coin}`,] + }) + redisClient.multi(redisCommands).exec(function(error, replies) { + if(error) { + log('error', logSystem, 'Failed to clear childCoins from parent in redis %j \n %j', [err, redisCommands]); + } + }) + + redisClient.hset(`${config.coin}:workers:${miner.login}`, `${config.childPools[miner.activeChildPool].coin}`, miner.childLogin) + + redisClient.hset(`${config.childPools[miner.activeChildPool].coin}:workers:${miner.childLogin}`, `${config.coin}`, miner.login) + + } +} + +/** + * Remove connected worker + **/ +function removeConnectedWorker(miner, reason){ + redisClient.hincrby(`${config.coin}:ports:${miner.port}`, 'users', '-1'); + if (mergedMining) { + redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:ports:${miner.port}`, 'users', '-1'); + redisClient.hincrby(`${config.childPools[miner.activeChildPool].coin}:active_connections${miner.childRewardTypeAsKey}`, `${miner.childLogin}~${miner.workerName}`, 1, function(error, connectedWorkers) { + }); + } + + redisClient.hincrby(`${config.coin}:active_connections${miner.rewardTypeAsKey}`, `${miner.login}~${miner.workerName}`, -1, function(error, connectedWorkers) { + if (reason === 'banned') { + notifications.sendToMiner(miner.login, 'workerBanned', { + 'LOGIN' : miner.login, + 'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`, + 'IP': miner.ip.replace('::ffff:', ''), + 'PORT': miner.port, + 'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : '' + }); + } else if (!connectedWorkers || connectedWorkers <= 0) { + notifications.sendToMiner(miner.login, 'workerTimeout', { + 'LOGIN' : miner.login, + 'MINER': `${miner.login.substring(0,7)}...${miner.login.substring(miner.login.length-7)}`, + 'IP': miner.ip.replace('::ffff:', ''), + 'PORT': miner.port, + 'WORKER_NAME': miner.workerName !== 'undefined' ? miner.workerName : '', + 'LAST_HASH': utils.dateFormat(new Date(miner.lastBeat), 'yyyy-mm-dd HH:MM:ss Z') + }); + } + }); +} + +/** + * Return if IP has been banned + **/ +function IsBannedIp(ip){ + if (!banningEnabled || !bannedIPs[ip]) return false; + + let bannedTime = bannedIPs[ip]; + let bannedTimeAgo = Date.now() - bannedTime; + let timeLeft = config.poolServer.banning.time * 1000 - bannedTimeAgo; + if (timeLeft > 0){ + return true; + } + else { + delete bannedIPs[ip]; + log('info', logSystem, 'Ban dropped for %s', [ip]); + return false; + } +} + +function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate, pool){ + let dateNow = Date.now(); + let dateNowSeconds = dateNow / 1000 | 0; + let coin = pool !== null ? pool.coin : config.coin; + let login = pool !== null ? miner.childLogin : miner.login; + let job_height = pool !== null ? job.childHeight : job.height + let workerName = miner.workerName; + let rewardType = pool !== null ? miner.childRewardType : miner.rewardType + + let updateScore; + // Weighting older shares lower than newer ones to prevent pool hopping + if (slushMiningEnabled) { + // We need to do this via an eval script because we need fetching the last block time and + // calculating the score to run in a single transaction (otherwise we could have a race + // condition where a block gets discovered between the time we look up lastBlockFound and + // insert the score, which would give the miner an erroneously huge proportion on the new block) + updateScore = ['eval', ` + local age = (ARGV[3] - redis.call('hget', KEYS[2], 'lastBlockFound')) / 1000 + local score = string.format('%.17g', ARGV[2] * math.exp(age / ARGV[4])) + redis.call('hincrbyfloat', KEYS[1], ARGV[1], score) + return {score, tostring(age)} + `, + 2 /*keys*/, coin + ':scores:roundCurrent', coin + ':stats', + /* args */ login, job.difficulty, Date.now(), config.poolServer.slushMining.weight]; + } + else { + job.score = job.difficulty; + updateScore = ['hincrbyfloat', `${coin}:scores:${rewardType}:roundCurrent`, login, job.score] + } + + let redisCommands = [ + updateScore, + ['hincrby', `${coin}:shares_actual:${rewardType}:roundCurrent`, login, job.difficulty], + ['zadd', `${coin}:hashrate`, dateNowSeconds, [job.difficulty, login, dateNow, rewardType].join(':')], + ['hincrby', `${coin}:workers:${login}`, 'hashes', job.difficulty], + ['hset', `${coin}:workers:${login}`, 'lastShare', dateNowSeconds], + ['expire', `${coin}:workers:${login}`, (86400 * cleanupInterval)], + ['expire', `${coin}:payments:${login}`, (86400 * cleanupInterval)] + ]; + + if (workerName) { + redisCommands.push(['zadd', `${coin}:hashrate`, dateNowSeconds, [job.difficulty, login + '~' + workerName, dateNow, rewardType].join(':')]); + redisCommands.push(['hincrby', `${coin}:unique_workers:${login}~${workerName}`, 'hashes', job.difficulty]); + redisCommands.push(['hset', `${coin}:unique_workers:${login}~${workerName}`, 'lastShare', dateNowSeconds]); + redisCommands.push(['expire', `${coin}:unique_workers:${login}~${workerName}`, (86400 * cleanupInterval)]); + } + + if (blockCandidate){ + redisCommands.push(['hset', `${coin}:stats`, `lastBlockFound${rewardType}`, Date.now()]); + redisCommands.push(['rename', `${coin}:scores:prop:roundCurrent`, coin + ':scores:prop:round' + job_height]); + redisCommands.push(['rename', `${coin}:scores:solo:roundCurrent`, coin + ':scores:solo:round' + job_height]); + redisCommands.push(['rename', `${coin}:shares_actual:prop:roundCurrent`, `${coin}:shares_actual:prop:round${job_height}`]); + redisCommands.push(['rename', `${coin}:shares_actual:solo:roundCurrent`, `${coin}:shares_actual:solo:round${job_height}`]); + if (rewardType === 'prop') { + redisCommands.push(['hgetall', `${coin}:scores:prop:round${job_height}`]); + redisCommands.push(['hgetall', `${coin}:shares_actual:prop:round${job_height}`]); + } + if (rewardType === 'solo') { + redisCommands.push(['hget', `${coin}:scores:solo:round${job_height}`, login]); + redisCommands.push(['hget', `${coin}:shares_actual:solo:round${job_height}`, login]); + } + + } + + redisClient.multi(redisCommands).exec(function(err, replies){ + if (err){ + log('error', logSystem, 'Failed to insert share data into redis %j \n %j', [err, redisCommands]); + return; + } + + if (slushMiningEnabled) { + job.score = parseFloat(replies[0][0]); + let age = parseFloat(replies[0][1]); + log('info', logSystem, 'Submitted score ' + job.score + ' for difficulty ' + job.difficulty + ' and round age ' + age + 's'); + } + + if (blockCandidate){ + let workerScores = replies[replies.length - 2]; + let workerShares = replies[replies.length - 1]; + let totalScore = 0; + let totalShares = 0; + if (rewardType === 'solo') { + totalScore = workerScores + totalShares = workerShares + } + if (rewardType === 'prop') { + totalScore = Object.keys(workerScores).reduce(function(p, c){ + return p + parseFloat(workerScores[c]) + }, 0); + totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + } + redisClient.zadd(coin + ':blocks:candidates', job_height, [ + rewardType, + login, + hashHex, + Date.now() / 1000 | 0, + blockTemplate.difficulty, + totalShares, + totalScore + ].join(':'), function(err, result){ + if (err){ + log('error', logSystem, 'Failed inserting block candidate %s \n %j', [hashHex, err]); + } + }); + + notifications.sendToAll('blockFound', { + 'HEIGHT': job_height, + 'HASH': hashHex, + 'DIFFICULTY': blockTemplate.difficulty, + 'SHARES': totalShares, + 'MINER': login.substring(0,7)+'...'+login.substring(login.length-7) + }); + } + + }); + + log('info', logSystem, 'Accepted %s share at difficulty %d/%d from %s@%s', [shareType, job.difficulty, shareDiff, login, miner.ip]); +} + +function getShareBuffer(miner, job, blockTemplate, params) { + if (miner.proxy) { + template.writeUInt32BE(params.poolNonce, job.clientPoolLocation); + template.writeUInt32BE(params.workerNonce, job.clientNonceLocation); + } + + try { + let nonceBuffer = Buffer.from(params.nonce, 'hex') + let nonceBufferReversed = nonceBuffer.reverse() + return utils.cnUtil.get_blob_from_block_template(blockTemplate.buffer, job.unique_id, nonceBufferReversed) + + } catch (e) { + log('error', logSystem, "Can't get share buffer with nonce %s from %s@%s: %s", [params.nonce, miner.login, miner.ip, e]); + return null; + } + } + +/** + * Process miner share data + **/ +function processShare(miner, job, blockTemplate, params){ + let shareBuffer = getShareBuffer(miner, job, blockTemplate, params) + if (!shareBuffer) { + return false + } + let shareType; + + let convertedBlob = shareBuffer + + let resultBuffer = Buffer.from(params.job_id, 'hex') + let nonceBuffer = Buffer.from(params.nonce, 'hex') + let nonceBufferReversed = nonceBuffer.reverse() + let heightBuffer = Buffer.alloc(8) + heightBuffer.writeUInt32LE(job.height, 0) + + let hash = utils.cnUtil.get_pow_hash(resultBuffer, nonceBufferReversed, heightBuffer) + log('info', logSystem, 'Mining pool algorithm: %s', [cnAlgorithm]); + shareType = 'valid' + + let hashDiff = diff1.div(bignum.fromBuffer(hash)) + + console.log('hashDiff ', hashDiff.toNumber()) + console.log('blocDiff ', blockTemplate.difficulty) + console.log('job Diff ', job.difficulty) + + if (hashDiff.ge(blockTemplate.difficulty)){ + + apiInterfaces.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function(error, result){ + if (error){ + log('error', logSystem, 'Error submitting block at height %d from %s@%s, share type: "%s" - %j', [job.height, miner.login, miner.ip, shareType, error]); + } + else{ + let blockFastHash = utils.cnUtil.get_id_hash(shareBuffer).toString('hex'); + log('info', logSystem, + 'Block %s found at height %d by miner %s@%s - submit result: %j', + [blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result] + ); + recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate, null); + } + }); + } + else if (hashDiff.lt(job.difficulty)){ + log('warn', logSystem, 'Rejected low difficulty share of %s from %s@%s', [hashDiff.toString(), miner.login, miner.ip]); + return false; + } + else{ + recordShareData(miner, job, hashDiff.toString(), false, null, shareType, null, null); + } + + if (!job.childHeight) + return true + + var childBlockTemplate = blockTemplate.childBlockTemplate; + + if (childBlockTemplate) { + if (mergedMining){ + let pool = config.childPools[miner.activeChildPool] + if (hashDiff.ge(childBlockTemplate.difficulty)){ + let mergedBuffer = null + try { + mergedBuffer = utils.cnUtil.construct_mm_child_block_blob(shareBuffer, cnBlobType, childBlockTemplate.buffer); + } catch (e) { + log('error', logSystem, "Failed to construct MM child block: " + e); + } + if (mergedBuffer === null) { + recordShareStatusMerged(miner, 'invalid'); + } else { + + let onChildSuccess = (result) => { + let blockFastHash = utils.cnUtil.get_block_id(mergedBuffer, 2).toString('hex') + log('info', logSystem, + 'Child Block %s found at height %d by miner %s@%s - submit result: %j', + [blockFastHash.substr(0, 6), job.childHeight, miner.workerName, miner.ip, result]); + recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, childBlockTemplate, pool); + } + + apiInterfaces.rpcDaemon('submitblock', [mergedBuffer.toString('hex')], function(error, result){ + if (error){ + log('error', logSystem, 'Error submitting child block at height %d from %s@%s, share type: "%s" - %j', [job.childHeight, miner.login, miner.ip, shareType, error]); + } + else{ + onChildSuccess(result) + } + }, pool.childDaemon); + } + } + else if (hashDiff.lt(job.difficulty)){ + log('warn', logSystem, 'Rejected low difficulty share of %s from %s@%s', [hashDiff.toString(), miner.workerName, miner.ip]); + return false; + } + else{ + recordShareData(miner, job, hashDiff.toString(), false, null, shareType, null, pool); + } + } + return true; + } + return true; +} + +/** + * Start pool server on TCP ports + **/ +let httpResponse = ' 200 OK\nContent-Type: text/plain\nContent-Length: 20\n\nMining server online'; + +function startPoolServerTcp(callback){ + log('info', logSystem, 'Clear values for connected workers in redis database.'); + redisClient.del(config.coin + ':active_connections'); + + async.each(config.poolServer.ports, function(portData, cback){ + let handleMessage = function(socket, jsonData, pushMessage){ + if (!jsonData.id) { + log('warn', logSystem, 'Miner RPC request missing RPC id'); + return; + } + else if (!jsonData.method) { + log('warn', logSystem, 'Miner RPC request missing RPC method'); + return; + } + else if (!jsonData.params) { + log('warn', logSystem, 'Miner RPC request missing RPC params'); + return; + } + + let sendReply = function(error, result, excludeId = false){ + if(!socket.writable) return; + let data = { + jsonrpc: "2.0", + result: result + } + if (!excludeId) + data.id = jsonData.id + if (error) + data.error = {code: -1, message: error} + + let sendData = JSON.stringify( + data + ) + "\n"; + socket.write(sendData); + }; + + handleMinerMethod(socket, jsonData.method, jsonData.params, socket.remoteAddress, portData, sendReply, pushMessage); + }; + + let socketResponder = function(socket){ + socket.setKeepAlive(true); + socket.setEncoding('utf8'); + socketMap.add(socket) + let dataBuffer = ''; + + let pushMessage = function(method, params){ + if(!socket.writable) return; + let sendData = JSON.stringify({ + jsonrpc: "2.0", + method: method, + params: params + }) + "\n"; + socket.write(sendData); + }; + + socket.on('data', function(d){ + dataBuffer += d; + if (Buffer.byteLength(dataBuffer, 'utf8') > 10240){ //10KB + dataBuffer = null; + log('warn', logSystem, 'Socket flooding detected and prevented from %s', [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; + } + } + + log('warn', logSystem, 'Malformed message from %s: %s', [socket.remoteAddress, message]); + socket.destroy(); + + break; + } + try { + handleMessage(socket, jsonData, pushMessage); + } catch (e) { + log('warn', logSystem, 'Malformed message from ' + socket.remoteAddress + ' generated an exception. Message: ' + message); + if (e.message) log('warn', logSystem, 'Exception: ' + e.message); + } + } + dataBuffer = incomplete; + } + }).on('error', function(err){ + if (err.code !== 'ECONNRESET') + log('warn', logSystem, 'Socket error from %s %j', [socket.remoteAddress, err]); + socketMap.delete[socket] + socket.destroy() + }).on('close', function(){ + pushMessage = function(){}; + if (socket.miner_ids) socket.miner_ids.forEach(miner_id => delete connectedMiners[miner_id]); + socketMap.delete[socket] + }); + }; + + if (portData.ssl) { + if (!config.poolServer.sslCert) { + log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL certificate not configured', [portData.port]); + cback(true); + } else if (!config.poolServer.sslKey) { + log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL key not configured', [portData.port]); + cback(true); + } else if (!fs.existsSync(config.poolServer.sslCert)) { + log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL certificate file not found (configuration error)', [portData.port]); + cback(true); + } else if (!fs.existsSync(config.poolServer.sslKey)) { + log('error', logSystem, 'Could not start server listening on port %d (SSL): SSL key file not found (configuration error)', [portData.port]); + cback(true); + } else { + let options = { + key: fs.readFileSync(config.poolServer.sslKey), + cert: fs.readFileSync(config.poolServer.sslCert), + }; + + if (config.poolServer.sslCA && fs.existsSync(config.poolServer.sslCA)) { + options.ca = fs.readFileSync(config.poolServer.sslCA) + } + + tls.createServer(options, socketResponder).listen(portData.port, function (error, result) { + if (error) { + log('error', logSystem, 'Could not start server listening on port %d (SSL), error: $j', [portData.port, error]); + cback(true); + return; + } + + log('info', logSystem, 'Clear values for SSL port %d in redis database.', [portData.port]); + redisClient.del(config.coin + ':ports:'+portData.port); + redisClient.hset(config.coin + ':ports:'+portData.port, 'port', portData.port); + + log('info', logSystem, 'Started server listening on port %d (SSL)', [portData.port]); + cback(); + }); + } + } + else { + net.createServer(socketResponder).listen(portData.port, function (error, result) { + if (error) { + log('error', logSystem, 'Could not start server listening on port %d, error: $j', [portData.port, error]); + cback(true); + return; + } + + log('info', logSystem, 'Clear values for port %d in redis database.', [portData.port]); + redisClient.del(config.coin + ':ports:'+portData.port); + redisClient.hset(config.coin + ':ports:'+portData.port, 'port', portData.port); + + log('info', logSystem, 'Started server listening on port %d', [portData.port]); + cback(); + }); + } + }, function(err){ + if (err) + callback(false); + else + callback(true); + }); +} + +/** + * Initialize pool server + **/ + +(function init(loop){ + async.waterfall([ + function(callback){ + if (!poolStarted) { + startPoolServerTcp(function(successful){ poolStarted = true }); + setTimeout(init, 1000, loop); + return; + } + callback(true) + } + ], + function(err){ + if (loop === true){ + setTimeout(function(){ + init(true); + },config.poolServer.blockRefreshInterval); + } + } + ); +})(); + diff --git a/lib/telegram.js b/lib/telegram.js new file mode 100644 index 0000000..7e42415 --- /dev/null +++ b/lib/telegram.js @@ -0,0 +1,62 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Telegram notifications system + * + * Author: Daniel Vandal + **/ + +// Load required modules +process.env.NTBA_FIX_319 = 1; +const TelegramBot = require('node-telegram-bot-api'); + +// Initialize log system +const logSystem = 'telegram'; +require('./exceptionWriter.js')(logSystem); + +/** + * Send telegram message + **/ +exports.sendMessage = function(chatId, messageText) { + // Return error if no text content + if (!messageText) { + log('warn', logSystem, 'No text to send.'); + return ; + } + + // Check telegram configuration + if (!config.telegram) { + log('error', logSystem, 'Telegram is not configured!'); + return ; + } + + // Do nothing if telegram is disabled + if (!config.telegram.enabled) return ; + + // Telegram bot token + const token = config.telegram.token || ''; + if (!token || token === '') { + log('error', logSystem, 'No telegram token specified in configuration!'); + return ; + } + + // Telegram chat id + if (!chatId || chatId === '' || chatId === '@') { + log('error', logSystem, 'No telegram chat id specified!'); + return ; + } + + const bot = new TelegramBot(token); + bot.sendMessage(chatId, messageText, { parse_mode: 'Markdown' }) + .then(() => { + log('info', logSystem, 'Telegram message sent to %s: %s', [chatId, messageText]); + }, error => { + log('error', logSystem, 'Telegram request failed: %s', [error.code]); + if (error.code === 'EFATAL') { + log('error', logSystem, 'Telegram request failed: communication error (no data)'); + } else { + log('error', logSystem, 'Telegram API error: [%s] %s', [error.response.body.error_code, error.response.body.description]); + } + }); +} diff --git a/lib/telegramBot.js b/lib/telegramBot.js new file mode 100644 index 0000000..f4e85e7 --- /dev/null +++ b/lib/telegramBot.js @@ -0,0 +1,347 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Telegram bot + * + * Author: Daniel Vandal + **/ + +// Load required modules +process.env.NTBA_FIX_319 = 1; +let TelegramBot = require('node-telegram-bot-api'); + +let timeAgo = require('time-ago'); + +let apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api); +let notifications = require('./notifications.js'); +let utils = require('./utils.js'); + +// Initialize log system +let logSystem = 'telegramBot'; +require('./exceptionWriter.js')(logSystem); + +/** + * Check telegram configuration + **/ + +// Check bot settings +if (!config.telegram) { + log('error', logSystem, 'Telegram is not enabled'); +} +else if (!config.telegram.enabled) { + log('error', logSystem, 'Telegram is not enabled'); +} +else if (!config.telegram.token) { + log('error', logSystem, 'No telegram token found in configuration'); +} + +// Bot commands +let botCommands = { + stats: "/stats", + report: "/report", + notify: "/notify", + blocks: "/blocks" +} + +if (config.telegram.botCommands) { + Object.assign(botCommands, config.telegram.botCommands); +} + +// Telegram channel +let channel = config.telegram.channel.replace(/@/g, '') || ''; + +// Periodical channel statistics +let periodicalStats = (channel && config.telegram.channelStats && config.telegram.channelStats.enabled) +let statsInterval = (config.telegram.channelStats && config.telegram.channelStats.interval > 0) ? parseInt(config.telegram.channelStats.interval) : 0; + +/** + * Initialize new telegram bot + **/ + +log('info', logSystem, 'Started'); + +let token = config.telegram.token; +let bot = new TelegramBot(token, {polling: true}); + +/** + * Periodical pool statistics + **/ + +if (periodicalStats && statsInterval > 0 && channel) { + log('info', logSystem, 'Sending pool statistics to telegram channel @%s each %d minutes', [channel, statsInterval]); + setInterval(function(){ sendPoolStats('@'+channel); }, (statsInterval*60)*1000); +} + +/** + * Handle "/start" or "/help" + **/ + +bot.onText(new RegExp('^/(start|help)$', 'i'), (telegramMsg) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + + log('info', logSystem, 'Commands list request from @%s (%s)', [telegramMsg.from.username, telegramMsg.from.id]); + + let message = 'Hi @' + telegramMsg.from.username + ',\n\n' + + 'Here are the commands you can use:\n\n' + + 'Pool statistics: ' + botCommands['stats'] + '\n' + + 'Blocks notifications: ' + botCommands['blocks'] + '\n' + + 'Miner statistics: ' + botCommands['report'] + ' _address_\n' + + 'Miner notifications: ' + botCommands['notify'] + ' _address_\n\n' + + 'Thank you!'; + + bot.sendMessage(telegramMsg.from.id, message, { parse_mode: 'Markdown' }); +}); + +/** + * Pool Statistics + **/ + +bot.onText(new RegExp('^'+botCommands['stats']+'$', 'i'), (telegramMsg) => { + log('info', logSystem, 'Pool statistics request from @%s (%s)', [telegramMsg.from.username, telegramMsg.from.id]); + sendPoolStats(telegramMsg.chat.id); +}); + +function sendPoolStats(chatId) { + apiInterfaces.pool('/stats', function(error, stats) { + if (error || !stats) { + log('error', logSystem, 'Unable to get API data for stats: ' + error); + return bot.sendMessage(id, 'Unable to get pool statistics. Please retry.'); + } + + let poolHost = config.poolHost || "Pool"; + let poolHashrate = utils.getReadableHashRate(stats.pool.hashrate); + let poolMiners = stats.pool.miners || 0; + let poolWorkers = stats.pool.workers || 0; + let poolBlocks = stats.pool.totalBlocks || 0; + let poolLastBlock = (stats.pool.lastBlockFound) ? timeAgo.ago(new Date(parseInt(stats.pool.lastBlockFound))) : 'Never'; + + let networkHashrate = utils.getReadableHashRate(stats.network.difficulty / stats.config.coinDifficultyTarget); + let networkDiff = stats.network.difficulty || 'N/A'; + let networkHeight = stats.network.height || 'N/A'; + let networkLastReward = utils.getReadableCoins(stats.lastblock.reward); + let networkLastBlock = (stats.lastblock.timestamp) ? timeAgo.ago(new Date(parseInt(stats.lastblock.timestamp * 1000))) : 'Never'; + + let currentEffort = stats.pool.roundHashes ? (stats.pool.roundHashes / stats.network.difficulty * 100).toFixed(1) + '%' : '0%'; + + let response = ''; + response += '*' + poolHost + '*\n'; + response += 'Hashrate: ' + poolHashrate + '\n'; + response += 'Connected Miners: ' + poolMiners + '\n'; + response += 'Active Workers: ' + poolWorkers + '\n'; + response += 'Blocks Found: ' + poolBlocks + '\n'; + response += 'Last Block: ' + poolLastBlock + '\n'; + response += 'Current Effort: ' + currentEffort + '\n'; + response += '\n'; + response += '*Network*\n'; + response += 'Hashrate: ' + networkHashrate + '\n'; + response += 'Difficulty: ' + networkDiff + '\n'; + response += 'Block Height: ' + networkHeight + '\n'; + response += 'Block Found: ' + networkLastBlock + '\n'; + response += 'Last Reward: ' + networkLastReward; + + return bot.sendMessage(chatId, response, { parse_mode: 'Markdown' }); + }); +} + +/** + * Miner Statistics + **/ + +bot.onText(new RegExp('^'+botCommands['report']+'$', 'i'), (telegramMsg) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + + let apiRequest = '/get_telegram_notifications?chatId='+telegramMsg.from.id+'&type=default'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (response.address) { + sendMinerStats(telegramMsg, response.address); + } else { + let message = 'To display miner report you need to specify the miner address on first request'; + bot.sendMessage(telegramMsg.from.id, message, { parse_mode: 'Markdown' }); + } + }); +}); + +bot.onText(new RegExp('^'+botCommands['report']+' (.*)$', 'i'), (telegramMsg, match) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + + let address = (match && match[1]) ? match[1].trim() : ''; + if (!address || address == '') { + return bot.sendMessage(telegramMsg.from.id, 'No address specified!'); + } + + sendMinerStats(telegramMsg, address); +}); + +function sendMinerStats(telegramMsg, address) { + log('info', logSystem, 'Miner report request from @%s (%s) for address: %s', [telegramMsg.from.username, telegramMsg.from.id, address]); + apiInterfaces.pool('/stats_address?address='+address, function(error, data) { + if (error || !data) { + log('error', logSystem, 'Unable to get API data for miner stats: ' + error); + return bot.sendMessage(telegramMsg.from.id, 'Unable to get miner statistics. Please retry.'); + } + if (!data.stats) { + return bot.sendMessage(telegramMsg.from.id, 'No miner statistics found for that address. Please check the address and try again.'); + } + + let minerHashrate = utils.getReadableHashRate(data.stats.hashrate); + let minerBalance = utils.getReadableCoins(data.stats.balance); + let minerPaid = utils.getReadableCoins(data.stats.paid); + let minerLastShare = timeAgo.ago(new Date(parseInt(data.stats.lastShare * 1000))); + + let response = '*Report for ' + address.substring(0,7)+'...'+address.substring(address.length-7) + '*\n'; + response += 'Hashrate: ' + minerHashrate + '\n'; + response += 'Last share: ' + minerLastShare + '\n'; + response += 'Balance: ' + minerBalance + '\n'; + response += 'Paid: ' + minerPaid + '\n'; + if (data.workers && data.workers.length > 0) { + let f = true; + for (let i in data.workers) { + if (!data.workers[i] || !data.workers[i].hashrate || data.workers[i].hashrate === 0) continue; + if (f) { + response += '\n'; + response += '*Active Workers*\n'; + } + let workerName = data.workers[i].name; + let workerHashrate = utils.getReadableHashRate(data.workers[i].hashrate); + response += workerName + ': ' + workerHashrate + '\n'; + f = false; + } + } + bot.sendMessage(telegramMsg.from.id, response, { parse_mode: 'Markdown' }); + + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=default&address='+address; + apiInterfaces.pool(apiRequest, function(error, response) {}); + }); +} + +/** + * Miner notifications + **/ + +bot.onText(new RegExp('^'+botCommands['notify']+'$', 'i'), (telegramMsg) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + + let apiRequest = '/get_telegram_notifications?chatId='+telegramMsg.from.id+'&type=default'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (response.address) { + toggleMinerNotifications(telegramMsg, response.address); + } else { + let message = 'To enable or disable notifications you need to specify the miner address on first request'; + bot.sendMessage(telegramMsg.from.id, message, { parse_mode: 'Markdown' }); + } + }); +}); + +bot.onText(new RegExp('^'+botCommands['notify']+' (.*)$', 'i'), (telegramMsg, match) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + + let address = (match && match[1]) ? match[1].trim() : ''; + if (!address || address == '') { + return bot.sendMessage(telegramMsg.from.id, 'No address specified!'); + } + + toggleMinerNotifications(telegramMsg, address); +}); + +function toggleMinerNotifications(telegramMsg, address) { + let apiRequest = '/get_telegram_notifications?chatId='+telegramMsg.from.id+'&type=miner&address='+address; + apiInterfaces.pool(apiRequest, function(error, response) { + if (response.chatId && response.chatId == telegramMsg.from.id) { + disableMinerNotifications(telegramMsg, address); + } else { + enableMinerNotifications(telegramMsg, address); + } + }); +} + +function enableMinerNotifications(telegramMsg, address) { + log('info', logSystem, 'Enable miner notifications to @%s (%s) for address: %s', [telegramMsg.from.username, telegramMsg.from.id, address]); + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=miner&address='+address+'&action=enable'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (error) { + log('error', logSystem, 'Unable to enable telegram notifications: ' + error); + return bot.sendMessage(telegramMsg.from.id, 'An error occurred. Please retry.'); + } + if (response.status != 'done') { + return bot.sendMessage(telegramMsg.from.id, response.status); + } + + bot.sendMessage(telegramMsg.from.id, 'Miner notifications enabled for ' + address.substring(0,7)+'...'+address.substring(address.length-7)); + + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=default&address='+address; + apiInterfaces.pool(apiRequest, function(error, response) {}); + }); +} + +function disableMinerNotifications(telegramMsg, address) { + log('info', logSystem, 'Disable miner notifications to @%s (%s) for address: %s', [telegramMsg.from.username, telegramMsg.from.id, address]); + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=miner&address='+address+'&action=disable'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (error) { + log('error', logSystem, 'Unable to disable telegram notifications: ' + error); + return bot.sendMessage(telegramMsg.from.id, 'An error occurred. Please retry.'); + } + if (response.status != 'done') { + return bot.sendMessage(telegramMsg.from.id, response.status); + } + + bot.sendMessage(telegramMsg.from.id, 'Miner notifications disabled for ' + address.substring(0,7)+'...'+address.substring(address.length-7)); + + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=default&address='+address; + apiInterfaces.pool(apiRequest, function(error, response) {}); + }); +} + +/** + * Blocks notifications + **/ + +bot.onText(new RegExp('^'+botCommands['blocks']+'$', 'i'), (telegramMsg) => { + if (telegramMsg.from.id != telegramMsg.chat.id) return ; + toggleBlocksNotifications(telegramMsg); +}); + +function toggleBlocksNotifications(telegramMsg) { + let apiRequest = '/get_telegram_notifications?chatId='+telegramMsg.from.id+'&type=blocks'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (error) { + return bot.sendMessage(telegramMsg.from.id, 'An error occurred. Please retry.'); + } + if (response.enabled) { + disableBlocksNotifications(telegramMsg); + } else { + enableBlocksNotifications(telegramMsg); + } + }); +} + +function enableBlocksNotifications(telegramMsg) { + log('info', logSystem, 'Enable blocks notifications to @%s (%s)', [telegramMsg.from.username, telegramMsg.from.id]); + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=blocks&action=enable'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (error) { + log('error', logSystem, 'Unable to enable telegram notifications: ' + error); + return bot.sendMessage(telegramMsg.from.id, 'An error occurred. Please retry.'); + } + if (response.status != 'done') { + return bot.sendMessage(telegramMsg.from.id, response.status); + } + return bot.sendMessage(telegramMsg.from.id, 'Blocks notifications enabled'); + }); +} + +function disableBlocksNotifications(telegramMsg) { + log('info', logSystem, 'Disable blocks notifications to @%s (%s)', [telegramMsg.from.username, telegramMsg.from.id]); + let apiRequest = '/set_telegram_notifications?chatId='+telegramMsg.from.id+'&type=blocks&action=disable'; + apiInterfaces.pool(apiRequest, function(error, response) { + if (error) { + log('error', logSystem, 'Unable to disable telegram notifications: ' + error); + return bot.sendMessage(telegramMsg.from.id, 'An error occurred. Please retry.'); + } + if (response.status != 'done') { + return bot.sendMessage(telegramMsg.from.id, response.status); + } + return bot.sendMessage(telegramMsg.from.id, 'Blocks notifications disabled'); + }); +} diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..073b9a6 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,191 @@ +/** + * Cryptonote Node.JS Pool + * https://github.com/dvandal/cryptonote-nodejs-pool + * + * Utilities functions + **/ + +// Load required module +let crypto = require('crypto'); + +let dateFormat = require('dateformat'); +exports.dateFormat = dateFormat; + +let cnUtil = require('cryptoforknote-util'); +exports.cnUtil = cnUtil; + +/** + * Generate random instance id + **/ +exports.instanceId = function(size=4) { + return crypto.randomBytes(size); +} + +/** + * Validate miner address + **/ +let addressBase58Prefix = parseInt(cnUtil.address_decode(Buffer.from(config.poolServer.poolAddress)).toString()); +let integratedAddressBase58Prefix = config.poolServer.intAddressPrefix ? parseInt(config.poolServer.intAddressPrefix) : addressBase58Prefix + 1; +let subAddressBase58Prefix = config.poolServer.subAddressPrefix ? parseInt(config.poolServer.subAddressPrefix) : "N/A"; + +// Get address prefix +function getAddressPrefix(address) { + let addressBuffer = Buffer.from(address); + + let addressPrefix = cnUtil.address_decode(addressBuffer); + if (addressPrefix) { + if (typeof addressPrefix === 'number') + addressPrefix = parseInt(addressPrefix) + else + addressPrefix = parseInt(addressPrefix.toString('hex')); + } + if (!addressPrefix && cnUtil.address_decode_integrated) { + addressPrefix = cnUtil.address_decode_integrated(addressBuffer); + if (addressPrefix) addressPrefix = parseInt(addressPrefix.toString()); + } + + return addressPrefix || null; +} +exports.getAddressPrefix = getAddressPrefix; + +// Validate miner address +exports.validateMinerAddress = function(address) { + let addressPrefix = getAddressPrefix(address); + if (addressPrefix === addressBase58Prefix) return true; + else if (addressPrefix === integratedAddressBase58Prefix) return true; + else if (addressPrefix === subAddressBase58Prefix) return true; + else if (cnUtil.is_address_valid) { + return cnUtil.is_address_valid(Buffer.from(address)) || false + } + return false; +} + +function characterCount(string, char) { + let re = new RegExp(char,"gi") + let matches = string.match(re) + return matches === null ? 0 : matches.length; +} +exports.characterCount = characterCount + +// Validate miner address +exports.validateChildMinerAddress = (address, index) => { + let childAddressBase58Prefix = parseInt(cnUtil.address_decode(Buffer.from(config.childPools[index].poolAddress)).toString()); + let childIntegratedAddressBase58Prefix = config.poolServer.intChildAddressPrefix ? parseInt(config.childPools[index].intAddressPrefix) : childAddressBase58Prefix + 1; + + let addressPrefix = getAddressPrefix(address); + if (addressPrefix === childAddressBase58Prefix) return true; + else if (addressPrefix === childIntegratedAddressBase58Prefix) return true; + return false; +} + +// Return if value is an integrated address +exports.isIntegratedAddress = function(address) { + let addressPrefix = getAddressPrefix(address); + return (addressPrefix === integratedAddressBase58Prefix); +} + +exports.determineRewardData = (value) => { + let calculatedData = {'address': value, 'rewardType': 'prop'} + if (/^solo:/i.test(value)) + { + calculatedData['address'] = value.substr(5) + calculatedData['rewardType'] = 'solo' + return calculatedData + } + if (/^prop:/i.test(value)) + { + calculatedData['address'] = value.substr(5) + calculatedData['rewardType'] = 'prop' + return calculatedData + } + return calculatedData +} + +/** + * Cleanup special characters (fix for non latin characters) + **/ +function cleanupSpecialChars(str) { + str = str.replace(/[ÀÁÂÃÄÅ]/g,"A"); + str = str.replace(/[àáâãäå]/g,"a"); + str = str.replace(/[ÈÉÊË]/g,"E"); + str = str.replace(/[èéêë]/g,"e"); + str = str.replace(/[ÌÎÏ]/g,"I"); + str = str.replace(/[ìîï]/g,"i"); + str = str.replace(/[ÒÔÖ]/g,"O"); + str = str.replace(/[òôö]/g,"o"); + str = str.replace(/[ÙÛÜ]/g,"U"); + str = str.replace(/[ùûü]/g,"u"); + return str.replace(/[^A-Za-z0-9\-\_+]/gi,''); +} +exports.cleanupSpecialChars = cleanupSpecialChars; + +/** + * Get readable hashrate + **/ +exports.getReadableHashRate = function(hashrate){ + let i = 0; + let byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ]; + while (hashrate > 1000){ + hashrate = hashrate / 1000; + i++; + } + return hashrate.toFixed(2) + byteUnits[i] + '/sec'; +} + +/** + * Get readable coins + **/ +exports.getReadableCoins = function(coins, digits, withoutSymbol){ + let coinDecimalPlaces = config.coinDecimalPlaces || config.coinUnits.toString().length - 1; + let amount = (parseInt(coins || 0) / config.coinUnits).toFixed(digits || coinDecimalPlaces); + return amount + (withoutSymbol ? '' : (' ' + config.symbol)); +} + +/** + * Generate unique id + **/ +exports.uid = function(){ + let min = 100000000000000; + let max = 999999999999999; + let id = Math.floor(Math.random() * (max - min + 1)) + min; + return id.toString(); +}; + +/** + * Ring buffer + **/ +exports.ringBuffer = function(maxSize){ + let data = []; + let cursor = 0; + let isFull = false; + + return { + append: function(x){ + if (isFull){ + data[cursor] = x; + cursor = (cursor + 1) % maxSize; + } + else{ + data.push(x); + cursor++; + if (data.length === maxSize){ + cursor = 0; + isFull = true; + } + } + }, + avg: function(plusOne){ + let sum = data.reduce(function(a, b){ return a + b }, plusOne || 0); + return sum / ((isFull ? maxSize : cursor) + (plusOne ? 1 : 0)); + }, + size: function(){ + return isFull ? maxSize : cursor; + }, + clear: function(){ + data = []; + cursor = 0; + isFull = false; + } + }; +}; + diff --git a/lib/zmqDaemon.js b/lib/zmqDaemon.js new file mode 100644 index 0000000..e1eb2aa --- /dev/null +++ b/lib/zmqDaemon.js @@ -0,0 +1,100 @@ +// Copyright (c)2020, The Arqma Network +// Copyright (c)2020, Gary Rusher +// Portions of this software are available under BSD-3 license. Please see ORIGINAL-LICENSE for details + +// All rights reserved. + +// Authors and copyright holders give permission for following: + +// 1. Redistribution and use in source and binary forms WITHOUT modification. + +// 2. Modification of the source form for your own personal use. + +// As long as the following conditions are met: + +// 3. You must not distribute modified copies of the work to third parties. This includes +// posting the work online, or hosting copies of the modified work for download. + +// 4. Any derivative version of this work is also covered by this license, including point 8. + +// 5. Neither the name of the copyright holders nor the names of the authors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. + +// 6. You agree that this licence is governed by and shall be construed in accordance +// with the laws of England and Wales. + +// 7. You agree to submit all disputes arising out of or in connection with this licence +// to the exclusive jurisdiction of the Courts of England and Wales. + +// Authors and copyright holders agree that: + +// 8. This licence expires and the work covered by it is released into the +// public domain on 1st of March 2021 + +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +let logSystem = "ZMQ" +require('./exceptionWriter.js')(logSystem); +let zmq = require("zeromq"), +dealer = zmq.socket("dealer"); +const { fromEvent } = require('rxjs'); + +function randomBetween(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} + +function randomString() { + var source = 'abcdefghijklmnopqrstuvwxyz' + var target = []; + for (var i = 0; i < 20; i++) { + target.push(source[randomBetween(0, source.length)]); + } + return target.join(''); +} + + +function startZMQ() { + dealer.identity = randomString(); + dealer.connect(`tcp://${config.zmq.host}:${config.zmq.port}`); + log('info', logSystem, 'Dealer connected to port %s:%s', [config.zmq.host, config.zmq.port]); + return fromEvent(dealer, "message"); +} + +exports.startZMQ = startZMQ + +function sendMessage(type, address) { + if (type === 'getinfo') { + let getinfo = {"jsonrpc": "2.0", + "id": "1", + "method": "get_info", + "params": {}} + dealer.send(["", JSON.stringify(getinfo)]); + } + if (type === 'get_block_template') { + let getblocktemplate = {"jsonrpc":"2.0", + "id":"0", + "method":"get_block_template", + "params":{"reserve_size":17, + "wallet_address":address} } + + dealer.send(["", JSON.stringify(getblocktemplate)]); + } + +} + +exports.sendMessage = sendMessage + +process.on('SIGINT', () => { + dealer.send(["", "EVICT"]); + dealer.close() + console.log('\nClosed') +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..137a105 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "cryptonote-nodejs-pool", + "version": "1.4.0", + "license": "GPL-2.0", + "Original author": "Daniel Vandal", + "Maintained by": "Musclesonvacation", + "repository": { + "type": "git", + "url": "https://github.com/muscleman/cryptonote-nodejs-pool.git" + }, + "dependencies": { + "async": "1", + "base58-native": "*", + "bignum": "*", + "cli-color": "*", + "cryptoforknote-util": "git+https://musclesonvacation@bitbucket.org/musclesonvacation/zano-node-util.git", + "cryptonight-hashing": "git://github.com/MoneroOcean/node-cryptonight-hashing.git", + "dateformat": "^4.5.1", + "mailgun.js": "*", + "node-telegram-bot-api": "*", + "nodemailer": "^6.4.11", + "nodemailer-sendmail-transport": "1.0.2", + "redis": "^3.1.2", + "socket.io": "^2.1.1", + "time-ago": "*", + "zeromq": "^5.2.0", + "rxjs": "^6.5.4", + "uint64be": "^3.0.0" + }, + "engines": { + "node": ">=8.11.3" + } +} diff --git a/website_example/admin.html b/website_example/admin.html new file mode 100644 index 0000000..f81826e --- /dev/null +++ b/website_example/admin.html @@ -0,0 +1,147 @@ + + + + + + + Arqma with PLE or XCY or TRTL Mining Pool - Admin Panel + + + + + + + + + + + + + + + + + + + + + + diff --git a/website_example/browserconfig.xml b/website_example/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/website_example/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/website_example/config.js b/website_example/config.js new file mode 100644 index 0000000..02a9b5b --- /dev/null +++ b/website_example/config.js @@ -0,0 +1,19 @@ +var api = "https://testv2.smartcoinpool.net/api"; +let parentCoin = "Zano" + +let byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH', 'EH', 'ZH', 'YH'] + +var email = "support@poolhost.com"; +var telegram = "https://t.me/YourPool"; +var discord = "https://discordapp.com/invite/YourPool"; +var facebook = "https://www.facebook.com/"; + +var marketCurrencies = ["{symbol}-BTC", "{symbol}-LTC", "{symbol}-DOGE", "{symbol}-USDT", "{symbol}-USD", "{symbol}-EUR", "{symbol}-CAD"]; + +var blockchainExplorer = "http://chainradar.com/{symbol}/block/{id}"; +var blockchainExplorerMerged = "http://explorer.ird.cash/?hash={id}#block"; +var transactionExplorer = "http://chainradar.com/{symbol}/transaction/{id}"; +var transactionExplorerMerged = "http://explorer.ird.cash/?hash={id}#transaction"; + +var themeCss = "themes/default.css"; +var defaultLang = 'en'; diff --git a/website_example/favicon.ico b/website_example/favicon.ico new file mode 100644 index 0000000..2f03f23 Binary files /dev/null and b/website_example/favicon.ico differ diff --git a/website_example/index.html b/website_example/index.html new file mode 100644 index 0000000..c100cd2 --- /dev/null +++ b/website_example/index.html @@ -0,0 +1,305 @@ + + + + + + + Zano Mining by Muscleman + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
Network: N/A
+
Prop Pool: N/A
+
Solo Pool: N/A
+
You: N/A
+
Stats Updated  
+
+
+ + +
+
+

+
+ +
+ + + + + + + + + + + + + diff --git a/website_example/js/common.js b/website_example/js/common.js new file mode 100644 index 0000000..1d0087a --- /dev/null +++ b/website_example/js/common.js @@ -0,0 +1,2310 @@ +/** + * Common javascript code for cryptonote-nodejs-pool + * Author: Daniel Vandal + * GitHub: https://github.com/dvandal/cryptonote-nodejs-pool + **/ + +/** + * Layout + **/ + +// Collapse menu on load for mobile devices +$('#menu-content').collapse('hide'); + +/** + * Cookies handler + **/ + +var docCookies = { + getItem: function (sKey) { + return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null; + }, + setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) { + if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; } + var sExpires = ""; + if (vEnd) { + switch (vEnd.constructor) { + case Number: + sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd; + break; + case String: + sExpires = "; expires=" + vEnd; + break; + case Date: + sExpires = "; expires=" + vEnd.toUTCString(); + break; + } + } + document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : ""); + return true; + }, + removeItem: function (sKey, sPath, sDomain) { + if (!sKey || !this.hasItem(sKey)) { return false; } + document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : ""); + return true; + }, + hasItem: function (sKey) { + return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie); + } +}; + +/** + * Pages routing + **/ + +// Current page +var currentPage; + +// Handle hash change +window.onhashchange = function(){ + routePage(); +}; + +// Route to page +var xhrPageLoading; +function routePage(loadedCallback) { + if (currentPage) currentPage.destroy(); + $('#page').html(''); + $('#loading').show(); + + if (xhrPageLoading) { + xhrPageLoading.abort(); + } + + $('.hot_link').parent().removeClass('active'); + var $link = $('a.hot_link[href="' + (window.location.hash || '#') + '"]'); + + $link.parent().addClass('active'); + var page = $link.data('page'); + + loadTranslations(); + + xhrPageLoading = $.ajax({ + url: 'pages/' + page, + cache: false, + success: function (data) { + $('#menu-content').collapse('hide'); + $('#loading').hide(); + $('#page').show().html(data); + loadTranslations(); + if (currentPage) currentPage.update(); + if (loadedCallback) loadedCallback(); + } + }); +} + +/** + * Strings + **/ + +// Add .update() custom jQuery function to update text content +$.fn.update = function(txt){ + var el = this[0]; + if (el && el.textContent !== txt) + el.textContent = txt; + return this; +}; + +// Update Text classes +function updateTextClasses(className, text){ + var els = document.getElementsByClassName(className); + if (els) { + for (var i = 0; i < els.length; i++){ + var el = els[i]; + if (el && el.textContent !== text) + el.textContent = text; + } + } +} + +// Update Text content +function updateText(elementId, text){ + var el = document.getElementById(elementId); + if (el && el.textContent !== text){ + el.textContent = text; + } + return el; +} + +// Convert float to string +function floatToString(float) { + return float.toFixed(6).replace(/[0\.]+$/, ''); +} + +// Format number +function formatNumber(number, delimiter){ + if(number != '') { + number = number.split(delimiter).join(''); + + var formatted = ''; + var sign = ''; + + if(number < 0){ + number = -number; + sign = '-'; + } + + while(number >= 1000){ + var mod = number % 1000; + + if(formatted != '') formatted = delimiter + formatted; + if(mod == 0) formatted = '000' + formatted; + else if(mod < 10) formatted = '00' + mod + formatted; + else if(mod < 100) formatted = '0' + mod + formatted; + else formatted = mod + formatted; + + number = parseInt(number / 1000); + } + + if(formatted != '') formatted = sign + number + delimiter + formatted; + else formatted = sign + number; + return formatted; + } + return ''; +} + +// Format date +function formatDate(time){ + if (!time) return ''; + return new Date(parseInt(time) * 1000).toLocaleString(); +} + +// Format percentage +function formatPercent(percent) { + if (!percent && percent !== 0) return ''; + return percent + '%'; +} + +// Get readable time +function getReadableTime(seconds){ + var units = [ [60, 'second'], [60, 'minute'], [24, 'hour'], + [7, 'day'], [4, 'week'], [12, 'month'], [1, 'year'] ]; + + function formatAmounts(amount, unit){ + var rounded = Math.round(amount); + var unit = unit + (rounded > 1 ? 's' : ''); + if (getTranslation(unit)) unit = getTranslation(unit); + return '' + rounded + ' ' + unit; + } + + var amount = seconds; + for (var i = 0; i < units.length; i++){ + if (amount < units[i][0]) { + return formatAmounts(amount, units[i][1]); + } + amount = amount / units[i][0]; + } + return formatAmounts(amount, units[units.length - 1][1]); +} + +// Get readable hashrate +function getReadableHashRateString(hashrate){ + var i = 0; + var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ]; + while (hashrate > 1000){ + hashrate = hashrate / 1000; + i++; + } + if (typeof hashrate != 'number') + hashrate = 0; + return hashrate.toFixed(2) + byteUnits[i]; +} + +function getCoinDecimalPlace(stats) +{ + if (typeof coinDecimalPlaces != "undefined") return coinDecimalPlaces; + else if (stats.config.coinDecimalPlaces) return stats.config.coinDecimalPlaces; + else stats.config.coinUnits.toString().length - 1; +} + +function getReadableCoin(stats, coins, digits, withoutSymbol) { + let coinDecimalPlaces = getCoinDecimalPlace(stats) + let amount = parseFloat((parseInt(coins || 0) / stats.config.coinUnits).toFixed(digits || coinDecimalPlaces)) + return amount.toString() + (withoutSymbol ? '' : (' ' + stats.config.symbol)); +} + + +// Format payment link +function formatPaymentLink(hash, merged){ + return '' + hash + ''; +} + +// Format difficulty +function formatDifficulty(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +// Format luck / current effort +function formatLuck(difficulty, shares, solo=false) { + // Only an approximation to reverse the calculations done in pool.js, because the shares with their respective times are not recorded in redis + // Approximation assumes equal pool hashrate for the whole round + // Could potentially be replaced by storing the sum of all job.difficulty in the redis db. + if (lastStats.config.slushMiningEnabled) { + // Uses integral calculus to calculate the average of a dynamic function + var accurateShares = 1/lastStats.config.blockTime * ( // 1/blockTime to get the average + shares * lastStats.config.weight * ( // Basically calculates the 'area below the graph' between 0 and blockTime + 1 - Math.pow( + Math.E, + ((- lastStats.config.blockTime) / lastStats.config.weight) // blockTime is equal to the highest possible result of (dateNowSeconds - scoreTime) + ) + ) + ); + } + else { + var accurateShares = shares; + } + + var percent = Math.round(accurateShares / difficulty * 100); + if(!percent){ + return `?` + (solo === true ? `` : ``); + } + else if(percent <= 100){ + return `${percent}% ` + (solo === true ? `` : ``); + } + else if(percent >= 101 && percent <= 150){ + return `${percent}% ` + (solo === true ? `` : ``); + } + else{ + return `${percent}% ` + (solo === true ? `` : ``); + } +} + +/** + * URLs + **/ + +// Return pool host +function getPoolHost() { + if (typeof poolHost != "undefined") return poolHost; + if (lastStats.config.poolHost) return lastStats.config.poolHost; + else return window.location.hostname; +} + +// Return transaction URL +function getTransactionUrl(id, stats) { + if (stats && blockExplorers){ + return blockExplorers[stats.config.coin].transactionExplorer.replace('{symbol}', stats.config.symbol.toLowerCase()).replace('{id}', id); + } +} + +// Return blockchain explorer URL +function getBlockchainUrl(id, stats) { + if (stats && blockExplorers){ + return blockExplorers[stats.config.coin].blockchainExplorer.replace('{symbol}', stats.config.symbol.toLowerCase()).replace('{id}', id); + } +} + +/** + * Tables + **/ + +// Sort table cells +function sortTable() { + var table = $(this).parents('table').eq(0), + rows = table.find('tr:gt(0)').toArray().sort(compareTableRows($(this).index())); + this.asc = !this.asc; + if(!this.asc) { + rows = rows.reverse() + } + for(var i = 0; i < rows.length; i++) { + table.append(rows[i]) + } +} + +// Compare table rows +function compareTableRows(index) { + return function(a, b) { + var valA = getCellValue(a, index), valB = getCellValue(b, index); + if (!valA) { valA = 0; } + if (!valB) { valB = 0; } + return $.isNumeric(valA) && $.isNumeric(valB) ? valA - valB : valA.toString().localeCompare(valB.toString()) + } +} + +// Get table cell value +function getCellValue(row, index) { + return $(row).children('td').eq(index).data("sort") +} + +/** + * Translations + **/ + +if (typeof langs == "undefined") { + var langs = { en: 'English' }; +} + +if (typeof defaultLang == "undefined") { + var defaultLang = 'en'; +} + +var langCode = defaultLang; +var langData = null; + +function getTranslation(key) { + if (!langData || !langData[key]) return null; + return langData[key]; +} + +var translate = function(data) { + $("html")[0].lang = langCode; + langData = data; + + $("[data-tkey]").each(function(index) { + var strTr = data[$(this).attr('data-tkey')]; + $(this).html(strTr); + }); + + $("[data-tplaceholder]").each(function(index) { + var strTr = data[$(this).attr('data-tplaceholder')]; + $(this).attr('placeholder', strTr) + }); + + $("[data-tvalue]").each(function(index) { + var strTr = data[$(this).attr('data-tvalue')]; + $(this).attr('value', strTr) + }); +} + +// Get language code from URL +const $_GET = {}; +const args = location.search.substr(1).split(/&/); +for (var i=0; i' + langs[lang] + ''; + numLangs ++; + } + html += ''; + } + if (html && numLangs > 1) { + $('#langSelector').html(html); + $('#newLang').each(function(){ + $(this).change(function() { + var newLang = $(this).val(); + var url = '?lang=' + newLang; + if (window.location.hash) url += window.location.hash; + window.location.href = url; + }); + }); + } + + // Mobile + var html = ''; + var numLangs = 0; + if (langs) { + html += ''; + } + if (html && numLangs > 1) { + $('#mLangSelector').html(html); + $('#mNewLang').each(function(){ + $(this).change(function() { + var newLang = $(this).val(); + var url = '?lang=' + newLang; + if (window.location.hash) url += window.location.hash; + window.location.href = url; + }); + }); + } +} + + +/* +*************************************************************** +pool_block methods +*************************************************************** +*/ + +function poolBlocks_GenerateChart(data, displayedChart) { + if (displayedChart[data.config.coin] || !data.charts.blocks || data.charts.blocks === "undefined" || !data.charts.blocksSolo || data.charts.blocksSolo === "undefined") return ; + let chartDays = data.config.blocksChartDays || null; + let title = getTranslation('poolBlocks') ? getTranslation('poolBlocks') : 'Blocks found'; + if (chartDays) { + if (chartDays === 1) title = getTranslation('blocksFoundLast24') ? getTranslation('blocksFoundLast24') : 'Blocks found in the last 24 hours'; + else title = getTranslation('blocksFoundLastDays') ? getTranslation('blocksFoundLastDays') : 'Blocks found in the last {DAYS} days'; + title = title.replace('{DAYS}', chartDays); + } + updateText(`blocksChartTitle${data.config.coin}`, title); + let labels = []; + let values = []; + let valuesSolo = []; + for (let key in data.charts.blocks) { + let label = key; + if (chartDays && chartDays === 1) { + let keyParts = key.split(' '); + label = keyParts[1].replace(':00', ''); + } + labels.push(label); + values.push(data.charts.blocks[key]); + } + for (let key in data.charts.blocksSolo) { + valuesSolo.push(data.charts.blocksSolo[key]); + } + + let $chart = $(`blocksChartObj${data.config.coin}`); + let bgcolor = null, bordercolor = null, borderwidth = null; + let colorelem = $chart.siblings('a.chart-style'); + if (colorelem.length == 1) { + bgcolor = colorelem.css('background-color'); + bordercolor = colorelem.css('border-left-color'); + borderwidth = parseFloat(colorelem.css('width')); + } + if (bgcolor === null) bgcolor = 'rgba(3, 169, 244, .4)'; + if (bordercolor === null) bordercolor = '#03a9f4'; + if (borderwidth === null || isNaN(borderwidth)) borderwidth = 1; + let chartElement = document.getElementById(`blocksChartObj${data.config.coin}`) + if (!chartElement) return + let chart = new Chart(chartElement, { + type: 'bar', + data: { + labels: labels, + datasets: [{ + label: 'Prop Blocks', + data: values, + fill: false, + backgroundColor: bgcolor, + borderColor: bordercolor, + borderWidth: borderwidth + }, + { + label: 'Solo Blocks', + data: valuesSolo, + fill: false, + backgroundColor: 'rgba(0, 230, 64, 1)', + borderColor: bordercolor, + borderWidth: borderwidth + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + legend: { display: false }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + userCallback: function(label, index, labels) { + if (Math.floor(label) === label) return label; + } + } + }], + }, + layout: { + padding: { top: 0, left: 0, right: 0, bottom: 0 } + } + } + }); + $(`#blocksChart${data.config.coin}`).show(); + displayedChart[data.config.coin] = true; +} + +// Parse block data +function poolBlocks_ParseBlock(height, serializedBlock, stats){ + var parts = serializedBlock.split(':'); + let block = {} + if (parts[0].includes('solo') || parts[0].includes('prop')){ + block = { + height: parseInt(height), + solo: parts[0] === 'solo', + address: parts[1], + hash: parts[2], + time: parts[3], + difficulty: parseInt(parts[4]), + shares: parseInt(parts[5]), + orphaned: parts[6], + reward: parts[7] + }; + }else{ + block = { + height: parseInt(height), + solo: false, + address: '', + hash: parts[0], + time: parts[1], + difficulty: parseInt(parts[2]), + shares: parseInt(parts[3]), + orphaned: parts[4], + reward: parts[5] + }; + } + + var toGo = stats.config.depth - (stats.network.height - block.height - 1); + if(toGo > 1){ + block.maturity = toGo + ' to go'; + } + else if(toGo == 1){ + block.maturity = ""; + } + else if(toGo <= 0){ + block.maturity = ""; + } + + switch (block.orphaned){ + case '0': + block.status = 'unlocked'; + block.maturity = ""; + break; + case '1': + block.status = 'orphaned'; + block.maturity = ""; + block.reward = 0; + break; + default: + block.status = 'pending'; + break; + } + return block; +} + +// Get block row element +function getBlockRowElement(block, jsonString, stats){ + function formatBlockLink(hash, stats){ + return '' + hash + ''; + } + + var blockStatusClasses = { + 'pending': 'pending', + 'unlocked': 'unlocked', + 'orphaned': 'orphaned' + }; + + var row = document.createElement('tr'); + row.setAttribute(`data-json`, jsonString); + row.setAttribute(`data-height`, block.height); + row.setAttribute('id', `blockRow${stats.config.coin}${block.height}`); + row.setAttribute('title', block.status); + row.className = blockStatusClasses[block.status]; + + var reward = ""; + if(typeof block.reward == "undefined"){ + reward = "Waiting..."; + } + else{ + reward = getReadableCoin(stats, block.reward, null, true); + } + + var columns = + '' + formatDate(block.time) + '' + + '' + reward + '' + + '' + block.height + '' + + '' + block.difficulty + '' + + '' + formatBlockLink(block.hash, stats) + '' + + '' + block.address + '' + + '' + formatLuck(block.difficulty, block.shares, block.solo) + '' + + '' + block.maturity + ''; + + row.innerHTML = columns; + + return row; +} + +// Render blocks +function poolBlocks_RenderBlocks(blocksResults, stats){ + var $blocksRows = $(`#blocksReport${stats.config.coin}_rows`); + + for (var i = 0; i < blocksResults.length; i += 2){ + var block = poolBlocks_ParseBlock(blocksResults[i + 1], blocksResults[i], stats); + var blockJson = JSON.stringify(block); + + var existingRow = document.getElementById(`blockRow${stats.config.coin}${block.height}`); + if (existingRow && existingRow.getAttribute(`data-json`) !== blockJson){ + $(existingRow).replaceWith(getBlockRowElement(block, blockJson, stats)); + } + else if (!existingRow){ + var blockElement = getBlockRowElement(block, blockJson, stats); + + var inserted = false; + var rows = $blocksRows.children().get(); + for (var f = 0; f < rows.length; f++) { + var bHeight = parseInt(rows[f].getAttribute(`data-height`)); + if (bHeight < block.height){ + inserted = true; + $(rows[f]).before(blockElement); + break; + } + } + if (!inserted){ + $blocksRows.append(blockElement); + } + } + } +} + +// Load more blocks button +function poolBlocks_Setup(api, stats, xhrGetBlocks) { + $(`#loadMoreBlocks${stats.config.coin}`).click(function(xhrGetBlocks){ + if (xhrGetBlocks[stats.config.coin]) xhrGetBlocks[stats.config.coin].abort(); + xhrGetBlocks[stats.config.coin] = $.ajax({ + url: api + '/get_blocks', + data: { + height: $(`#blocksReport${stats.config.coin}_rows`).children().last().data(`height`) + }, + dataType: 'json', + cache: 'false', + success: function(data){ + poolBlocks_RenderBlocks(data, stats); + } + }); + }); +} + +function poolBlocks_InitTemplate(ranOnce, displayedChart, xhrGetBlocks) { + let coin = lastStats.config.coin + if ($(`#blocksTabs li:contains(${coin})`).length == 0) { + let template1 = $('#siblingTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:lastStats.config.coin, active:'active'}) + $('#tab-content').append(rendered1) + + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active:'active'}); + $('#blocksTabs').append(rendered) + + poolBlocks_Setup(api, lastStats, xhrGetBlocks) + } + + + updateText(`blocksTotal${coin}`, lastStats.pool.totalBlocks.toString()); + if (lastStats.pool.lastBlockFound) { + var d = new Date(parseInt(lastStats.pool.lastBlockFound)).toISOString(); + $(`#lastBlockFound${coin}`).timeago('update', d); + } + else { + $(`#lastBlockFound${coin}`).removeAttr('title').data('ts', '').update('Never'); + } + + updateText(`blocksTotalSolo${coin}`, lastStats.pool.totalBlocksSolo.toString()); + if (lastStats.pool.lastBlockFoundSolo) { + var d = new Date(parseInt(lastStats.pool.lastBlockFoundSolo)).toISOString(); + $(`#lastBlockFoundSolo${coin}`).timeago('update', d); + } + else { + $(`#lastBlockFoundSolo${coin}`).removeAttr('title').data('ts', '').update('Never'); + } + + updateText(`blocksMaturityCount${coin}`, lastStats.config.depth.toString()); + + $(`#averageLuck${coin}`).html(formatLuck(lastStats.pool.totalDiff, lastStats.pool.totalShares)); + + displayedChart[lastStats.config.coin] = false + if (lastStats.charts.blocks) { + poolBlocks_GenerateChart(lastStats, displayedChart); + } + + poolBlocks_RenderBlocks(lastStats.pool.blocks, lastStats); + + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length == 0) { + let template1 = $('#siblingTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:key}) + $('#tab-content').append(rendered1) + + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + poolBlocks_Setup(mergedApis[key].api, mergedStats[key]) + } + + updateText(`blocksTotal${key}`, mergedStats[key].pool.totalBlocks.toString()); + if (mergedStats[key].pool.lastBlockFound) { + var d = new Date(parseInt(mergedStats[key].pool.lastBlockFound)).toISOString(); + $(`#lastBlockFound${key}`).timeago('update', d); + } + else { + $(`#lastBlockFound${key}`).removeAttr('title').data('ts', '').update('Never'); + } + + updateText(`blocksTotalSolo${key}`, mergedStats[key].pool.totalBlocksSolo.toString()); + if (mergedStats[key].pool.lastBlockFoundSolo) { + var d = new Date(parseInt(mergedStats[key].pool.lastBlockFoundSolo)).toISOString(); + $(`#lastBlockFoundSolo${key}`).timeago('update', d); + } + else { + $(`#lastBlockFoundSolo${key}`).removeAttr('title').data('ts', '').update('Never'); + } + + updateText(`blocksMaturityCount${key}`, mergedStats[key].config.depth.toString()); + + $(`#averageLuck${key}`).html(formatLuck(mergedStats[key].pool.totalDiff, mergedStats[key].pool.totalShares)); + displayedChart[key] = false + if (mergedStats[key].charts.blocks) { + poolBlocks_GenerateChart(mergedStats[key], displayedChart); + } + poolBlocks_RenderBlocks(mergedStats[key].pool.blocks, mergedStats[key]); + }) + sortElementList($(`#blocksTabs`), $(`#blocksTabs>div`), mergedStats) + if (!ranOnce) + ranOnce = RunOnce() +} + +/* +*************************************************************** +top10miners methods +*************************************************************** +*/ + +function top10Miners_GetMinerCells(position, data){ + var miner = data.miner; + var hashrate = data.hashrate ? data.hashrate : 0; + var lastShare = data.lastShare ? data.lastShare : 0; + var hashes = (data.hashes || 0).toString(); + + return '' + position + '' + + '' + miner + '' + + '' + getReadableHashRateString(hashrate) + '/sec' + + '' + (lastShare ? $.timeago(new Date(parseInt(lastShare) * 1000).toISOString()) : 'Never') + '' + + '' + hashes + ''; +} + +// Update top10 miners report +function top10Miners_UpdateTop10(xhrGetMiners, endPoint, key) { + if (xhrGetMiners[key]) + xhrGetMiners[key].abort() + + $( `#top10miners_rows${key}`).empty(); + + xhrGetMiners[key] = $.ajax({ + url: `${endPoint}/get_top10miners`, + data: { + time: $(`#top10_rows${key}`).children().last().data('time') + }, + dataType: 'json', + cache: 'false', + success: function(data){ + if (!data) return; + for (var i=0; i' + top10Miners_GetMinerCells(i+1, data[i]) + ''); + } + } + }); +} + +function top10Miners_InitTemplate(xhrGetMiners, ranOnce) { + let coin = lastStats.config.coin + if ($(`#blocksTabs li:contains(${coin})`).length === 0) { + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active: 'active'}); + $('#blocksTabs').append(rendered) + + template = $('#siblingTemplate').html() + Mustache.parse(template) + rendered = Mustache.render(template, {coin:coin, active: 'active'}) + $('#tab-content').append(rendered) + } + + top10Miners_UpdateTop10(xhrGetMiners, api, coin); + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length === 0) { + coin = key + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:mergedStats[key].config.coin, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + template = $('#siblingTemplate').html() + Mustache.parse(template) + rendered = Mustache.render(template, {coin:coin}) + $('#tab-content').append(rendered) + } + top10Miners_UpdateTop10(xhrGetMiners, mergedApis[key].api, key); + }) + sortElementList($(`#blocksTabs`), $(`#blocksTabs>li`), mergedStats) + if (!ranOnce) + ranOnce = RunOnce() +} + +/* +*************************************************************** +settings methods +*************************************************************** +*/ + +function settings_Setup(api, stats) { + + var address = getCurrentAddress(stats.config.coin); + if (address){ + $(`#yourAddress${stats.config.coin}`).val(address); + settings_GetPayoutLevel(api, address, stats); + settings_GetEmailAddress(api, address, stats); + } + + // Handle click on Set button + $(`#payoutSetButton${stats.config.coin}`).click(function(){ + var address = $(`#yourAddress${stats.config.coin}`).val().trim(); + if (!address || address == '') { + settings_ShowError('noMinerAddress', 'No miner address specified', '', false); + return; + } + + var ip = $(`#yourIP${stats.config.coin}`).val().trim(); + if (!ip || ip == '') { + settings_ShowError('noMinerIP', 'No miner IP address specified', '', false); + return; + } + + var level = $(`#yourPayoutRate${stats.config.coin}`).val().trim(); + if (!level || level < 0) { + settings_ShowError('noPayoutLevel', 'No payout level specified', '', false); + return; + } + settings_SetPayoutLevel(api, address, ip, level, stats); + }); + + // Handle click on Enable button + $(`#enableButton${stats.config.coin}`).click(function(){ + var address = $(`#yourAddress${stats.config.coin}`).val().trim(); + var ip = $(`#yourIP${stats.config.coin}`).val().trim(); + var email = $(`#yourEmail${stats.config.coin}`).val(); + settings_SetEmailNotifications(stats, api, email, address, ip, true); + }); + + // Handle click on Disable button + $(`#disableButton${stats.config.coin}`).click(function(){ + var address = $(`#yourAddress${stats.config.coin}`).val().trim(); + var ip = $(`#yourIP${stats.config.coin}`).val().trim(); + var email = $(`#yourEmail${stats.config.coin}`).val(); + settings_SetEmailNotifications(stats, api, email, address, ip, false); + }); +} + +/** + * Error Message + **/ +function settings_ShowError(id, message, extra, stats) { + if (getTranslation(id)) message = getTranslation(id); + message = message.trim(); + if (extra) message += ' ' + extra; + $(`#action_update_message${stats.config.coin}`).text(message); + $(`#action_update_message${stats.config.coin}`).removeClass().addClass('alert alert-danger'); +} + +/** + * Success Message + **/ +function settings_ShowSuccess(id, message, stats) { + if (getTranslation(id)) message = getTranslation(id); + $(`#action_update_message${stats.config.coin}`).text(message); + $(`#action_update_message${stats.config.coin}`).removeClass().addClass('alert alert-success'); +} + +/** + * Payout level + **/ + +// Get current payout level +function settings_GetPayoutLevel(api, address, stats) { + if (!address || address == '') + return; + $.ajax({ + url: `${api}/get_miner_payout_level`, + data: { + address: address + }, + dataType: 'json', + cache: 'false' + }).done(function(data){ + if (data.level != undefined) { + $(`#yourPayoutRate${stats.config.coin}`).val(data.level); + } + }); +} + +// Set payout level +function settings_SetPayoutLevel(api, address, ip, level, stats) { + let params = { + address: address, + ip: ip, + level: level + } + $.ajax({ + url: `${api}/set_miner_payout_level`, + data: params, + dataType: 'json', + cache: 'false' + }).done(function(data){ + if (data.status == 'done') { + settings_ShowSuccess('minerPayoutSet', 'Done! Your minimum payout level was set', stats); + } else { + settings_ShowError('Error:', data.status, null, stats); + } + }); +} + +/** + * Email Notifications + **/ + +// Check if specified value is a valid email +function settings_IsEmail(email) { + var regex = /^([a-zA-Z0-9_.+-])+\@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/; + return regex.test(email); +} + +// Get current email address for notifications +function settings_GetEmailAddress(endPoint, address, stats) { + if (!address || address == '') return; + + $.ajax({ + url: `${endPoint}/get_email_notifications`, + data: { + address: address + }, + dataType: 'json', + cache: 'false' + }).done(function(data){ + if (data.email != undefined) { + $(`#yourEmail${stats.config.coin}`).val(data.email); + } + }); +} + +// Set email address for notifications +function settings_SetEmailNotifications(stats, endPoint, email, address, ip, enable) { + var address = $(`#yourAddress${stats.config.coin}`).val().trim(); + if (!address || address == '') { + settings_ShowError('noMinerAddress', 'No miner address specified', null, stats); + return; + } + + var ip = $(`#yourIP${stats.config.coin}`).val().trim(); + if (!ip || ip == '') { + settings_ShowError('noMinerIP', 'No miner IP address specified', null, stats); + return; + } + + var email = $(`#yourEmail${stats.config.coin}`).val().trim(); + if (enable && !email) { + settings_ShowError('noEmail', 'No email address specified', null, stats); + return; + } + if (enable && !settings_IsEmail(email)) { + settings_ShowError('invalidEmail', 'Invalid email address specified', null, stats); + return; + } + + $.ajax({ + url: `${endPoint}/set_email_notifications`, + data: { + address: address, + ip: ip, + email: email, + action: enable ? 'enable' : 'disable' + }, + dataType: 'json', + cache: 'false' + }).done(function(data){ + if (data.status == "done") { + if (enable) { + settings_ShowSuccess('notificationEnabled', 'Done! Email notifications have been enabled', stats); + } else { + settings_ShowSuccess('notificationDisabled', 'Done! Email notifications have been disabled', stats); + } + } else { + settings_ShowError('error', 'Error:', data.status, stats); + } + }); +} + +function settings_InitTemplate(ranOnce) { + if (!lastStats.config.sendEmails) $(`#emailNotifications${lastStats.config.coin}`).hide(); + + let coin = lastStats.config.coin + let template = $('#siblingTemplate').html() + if ($(`#blocksTabs li:contains(${coin})`).length === 0) { + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:coin, active:'active'}) + $('#tab-content').append(rendered) + + template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active:'active'}); + $('#blocksTabs').append(rendered) + settings_Setup(api, lastStats) + } + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length === 0) { + if (!mergedStats[key].config.sendEmails) + $(`#emailNotifications${mergedStats[key].config.coin}`).hide(); + template = $('#siblingTemplate').html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key}) + $('#tab-content').append(rendered) + + template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + rendered = Mustache.render(template, {coin:key, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + settings_Setup(mergedApis[key].api, mergedStats[key]) + } + }) + sortElementList($(`#blocksTabs`), $(`#blocksTabs>li`), mergedStats) + if (!ranOnce) + ranOnce = RunOnce() +} + +/* +*************************************************************** +payments methods +*************************************************************** +*/ + +// Parse payment data +function payments_ParsePayment(time, serializedPayment){ + var parts = serializedPayment.split(':'); + return { + time: parseInt(time), + hash: parts[0], + amount: parts[1], + fee: parts[2], + mixin: parts[3], + recipients: parts[4] + }; +} + +// Get payment cells +function payments_GetPaymentCells(payment, stats){ + return '' + formatDate(payment.time) + '' + + '' + formatPaymentLink(payment.hash, stats) + '' + + '' + (getReadableCoin(stats, payment.amount)) + '' + + '' + (getReadableCoin(stats, payment.fee)) + '' + + '' + payment.mixin + '' + + '' + payment.recipients + ''; +} + +// Get payment row element +function payments_GetPaymentRowElement(payment, jsonString, stats){ + var row = document.createElement('tr'); + row.setAttribute(`data-json`, jsonString); + row.setAttribute(`data-time`, payment.time); + row.setAttribute('id', `paymentRow${stats.config.coin}${payment.time}`); + + row.innerHTML = payments_GetPaymentCells(payment, stats); + + return row; +} + +// Render payments data +function payments_renderPayments(paymentsResults, stats){ + var $paymentsRows = $(`#paymentsReport${stats.config.coin}_rows`); + for (var i = 0; i < paymentsResults.length; i += 2){ + var payment = payments_ParsePayment(paymentsResults[i + 1], paymentsResults[i]); + var paymentJson = JSON.stringify(payment); + var existingRow = document.getElementById(`paymentRow${stats.config.coin}${payment.time}`); + + if (existingRow && existingRow.getAttribute(`data-json`) !== paymentJson){ + $(existingRow).replaceWith(payments_GetPaymentRowElement(payment, paymentJson, stats)); + } + else if (!existingRow){ + var paymentElement = payments_GetPaymentRowElement(payment, paymentJson, stats); + + var inserted = false; + var rows = $paymentsRows.children().get(); + for (var f = 0; f < rows.length; f++) { + var pTime = parseInt(rows[f].getAttribute(`data-time`)); + if (pTime < payment.time){ + inserted = true; + $(rows[f]).before(paymentElement); + break; + } + } + if (!inserted) { + $paymentsRows.append(paymentElement); + } + } + } +} + +// Load more payments button +function payments_Setup(xhrGetPayments, api, stats) { + $(`#loadMorePayments${stats.config.coin}`).click(function(){ + if (xhrGetPayments[stats.config.coin]) xhrGetPayments[stats.config.coin].abort(); + xhrGetPayments[stats.config.coin] = $.ajax({ + url: api + '/get_payments', + data: { + time: $(`#paymentsReport${stats.config.coin}_rows`).children().last().data(`time`) + }, + dataType: 'json', + cache: 'false', + success: function(data){ + payments_renderPayments(data, stats); + } + }); + }); +} + +function payments_InitTemplate(xhrGetPayments, ranOnce) { + let coin = lastStats.config.coin + if ($(`#blocksTabs li:contains(${coin})`).length === 0) { + let template1 = $('#siblingTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:coin, active:'active'}) + $('#tab-content').append(rendered1) + + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active:'active'}); + $('#blocksTabs').append(rendered) + + payments_Setup(xhrGetPayments, api, lastStats) + } + updateText(`paymentsTotal${coin}`, lastStats.pool.totalPayments.toString()); + updateText(`paymentsTotalPaid${coin}`, lastStats.pool.totalMinersPaid.toString()); + updateText(`paymentsInterval${coin}`, getReadableTime(lastStats.config.paymentsInterval)); + updateText(`paymentsMinimum${coin}`, getReadableCoin(lastStats, lastStats.config.minPaymentThreshold)); + updateText(`paymentsDenomination${coin}`, getReadableCoin(lastStats, lastStats.config.denominationUnit, 3)); + payments_renderPayments(lastStats.pool.payments, lastStats); + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length === 0) { + + let template1 = $('#siblingTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:key}) + $('#tab-content').append(rendered1) + + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + payments_Setup(xhrGetPayments, mergedApis[key].api, mergedStats[key]) + } + + updateText(`paymentsTotal${key}`, mergedStats[key].pool.totalPayments.toString()); + updateText(`paymentsTotalPaid${key}`, mergedStats[key].pool.totalMinersPaid.toString()); + updateText(`paymentsInterval${key}`, getReadableTime(mergedStats[key].config.paymentsInterval)); + updateText(`paymentsMinimum${key}`, getReadableCoin(mergedStats[key], mergedStats[key].config.minPaymentThreshold)); + updateText(`paymentsDenomination${key}`, getReadableCoin(mergedStats[key], mergedStats[key].config.denominationUnit, 3)); + payments_renderPayments(mergedStats[key].pool.payments, mergedStats[key]); + }) + sortElementList($(`#blocksTabs`), $(`#blocksTabs>li`), mergedStats) + if (!ranOnce) + ranOnce = RunOnce() +} + +/* +*************************************************************** +market methods +*************************************************************** +*/ + +function market_LoadMarketData(api, stats, loadedData, currencyPairs, xhrMarketGets, marketPrices) { + if (loadedData[stats.config.coin]) return ; + + if (typeof marketCurrencies !== 'undefined' && marketCurrencies.length > 0){ + let intervalMarketPolling = setInterval(market_UpdateMarkets(api, stats, currencyPairs, xhrMarketGets, marketPrices), 300000); + market_UpdateMarkets(api, stats, currencyPairs, xhrMarketGets, marketPrices); + } else { + $(`#marketInfos${stats.config.coin}`).hide(); + } + + loadedData[stats.config.coin] = true; +} + +// Market data polling (poll data every 5 minutes) +function market_UpdateMarkets(api, stats, currencyPairs, xhrMarketGets, marketPrices){ + if (typeof marketCurrencies === 'undefined' || marketCurrencies.length === 0) return ; + + currencyPairs[stats.config.coin] = [] + + for (let i = 0; i < marketCurrencies.length; i++){ + currencyPairs[stats.config.coin].push(marketCurrencies[i].replace('{symbol}', stats.config.symbol).toUpperCase()); + } + + if (xhrMarketGets[stats.config.coin]) xhrMarketGets[stats.config.coin].abort() + + xhrMarketGets[stats.config.coin] = $.ajax({ + url: api + '/get_market', + data: { tickers: currencyPairs[stats.config.coin] }, + dataType: 'json', + cache: 'false', + success: function(data) { + if (!data || data.length === 0) { + $(`#marketInfos${stats.config.coin}`).hide(); + return; + } + + $(`#marketInfos${stats.config.coin}`).empty(); + for (let i in data) { + if (!data[i] || !data[i].ticker) continue; + let ticker = data[i].ticker; + let tickerParts = ticker.split('-'); + let tickerBase = tickerParts[0] || null; + let tickerTarget = tickerParts[1] || null; + + let price = data[i].price; + if (!price || price === 0) continue; + + let dataSource = data[i].source; + + market_RenderMarketPrice(tickerBase, tickerTarget, price, dataSource, stats, marketPrices); + } + $(`#marketInfos${stats.config.coin}`).show(); + }, + error: function() { + $(`#marketInfos${stats.config.coin}`).hide(); + } + }); +} + +// Render market price +function market_RenderMarketPrice(base, target, price, source, stats, marketPrices) { + let icon = 'fa-money'; + if (target == 'BTC') icon = 'fa-btc'; + if (target == 'BCH') icon = 'fa-btc'; + if (target == 'USD') icon = 'fa-dollar'; + if (target == 'CAD') icon = 'fa-dollar'; + if (target == 'EUR') icon = 'fa-eur'; + if (target == 'GBP') icon = 'fa-gbp'; + if (target == 'JPY') icon = 'fa-jpy'; + + if (base == stats.config.symbol.toUpperCase()) { + marketPrices[stats.config.coin][target] = price; + } + + if (target == 'USD' || target == 'CAD' || target == 'EUR' || target == 'GBP' || target == 'JPY') { + price = price.toFixed(4); + } else { + price = price.toFixed(8); + } + + let sourceURL = null; + if (source == 'cryptonator') sourceURL = 'https://www.cryptonator.com/'; + else if (source == 'altex') sourceURL = 'https://altex.exchange/'; + else if (source == 'crex24') sourceURL = 'https://crex24.com/'; + else if (source == 'cryptopia') sourceURL = 'https://www.cryptopia.co.nz/'; + else if (source == 'stocks.exchange') sourceURL = 'https://stocks.exchange/'; + else if (source == 'tradeogre') sourceURL = 'https://tradeogre.com/'; + + source = source.charAt(0).toUpperCase() + source.slice(1); + if (sourceURL) source = ''+source+''; + + $(`#marketInfos${stats.config.coin}`).append( + '
' + + '
' + + '
' + + '
' + base + ' to ' + target + '
' + + '
' + price + '
' + + '
Source: ' + source + '
' + + '
' + + '
' + ); +} + +/** + * Market Charts + **/ + +// Create charts +function market_CreateCharts(stats) { + if (!stats || !stats.charts) return ; + let data = stats.charts; + let graphData = { + price: market_GetGraphData(data.price), + profit: market_GetGraphData(data.profit) + }; + + for(let graphType in graphData) { + if (graphData[graphType].values.length > 1) { + let $chart = $(`#chart${stats.config.coin}_${graphType}`); + let bgcolor = null, bordercolor = null, borderwidth = null; + let colorelem = $chart.siblings('a.chart-style'); + if (colorelem.length == 1) { + bgcolor = colorelem.css('background-color'); + bordercolor = colorelem.css('border-left-color'); + borderwidth = parseFloat(colorelem.css('width')); + } + if (bgcolor === null) bgcolor = 'rgba(3, 169, 244, .4)'; + if (bordercolor === null) bordercolor = '#03a9f4'; + if (borderwidth === null || isNaN(borderwidth)) borderwidth = 1; + let chartObj = new Chart(document.getElementById(`chart${stats.config.coin}_${graphType}`), { + type: 'line', + data: { + labels: graphData[graphType].names, + datasets: [{ + data: graphData[graphType].values, + dataType: graphType, + fill: true, + backgroundColor: bgcolor, + borderColor: bordercolor, + borderWidth: borderwidth + }] + }, + options: { + animation: false, + responsive: true, + maintainAspectRatio: false, + legend: { display: false }, + elements: { point: { radius: 0, hitRadius: 10, hoverRadius: 5 } }, + scales: { + xAxes: [{ + display: false, + ticks: { display: false }, + gridLines: { display: false } + }], + yAxes: [{ + display: false, + ticks: { + display: false, + beginAtZero: true, + userCallback: function(label, index, labels) { + if (Math.floor(label) === label) return label; + } + }, + gridLines: { display: false } + }] + }, + layout: { + padding: { top: 5, left: 10, right: 10, bottom: 10 } + }, + tooltips: { + callbacks: { + label: function(tooltipItem, data) { + let dataType = data.datasets[tooltipItem.datasetIndex].dataType || ''; + let label = tooltipItem.yLabel; + if (dataType == 'price') label = parseFloat(tooltipItem.yLabel).toFixed(4); + else if (dataType == 'profit') label = parseFloat(tooltipItem.yLabel).toFixed(10); + return ' ' + label; + } + } + } + } + }); + $chart.closest('.marketChart').show(); + } + } +} + +// Get chart data +function market_GetGraphData(rawData) { + let graphData = { + names: [], + values: [] + }; + if(rawData) { + for (let i = 0, xy; xy = rawData[i]; i++) { + graphData.names.push(new Date(xy[0]*1000).toLocaleString()); + graphData.values.push(xy[1]); + } + } + return graphData; +} + + +// Calculate current estimation +function market_CalcEstimateProfit(marketPrices){ + let rateUnit = Math.pow(1000,parseInt($('#calcHashUnit').data('mul'))); + let hashRate = parseFloat($('#calcHashRate').val()) * rateUnit; + let coin = lastStats.config.coin + try { + if ($(`#calcHashAmount${coin}`).length == 0) { + let template = $(`#calcHashResultTemplate`).html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:coin}) + $(`#calcHashHolder`).append(rendered) + } + let profit = (hashRate * 86400 / lastStats.network.difficulty) * lastStats.lastblock.reward; + if (profit) { + updateText(`calcHashAmount${coin}1`, getReadableCoin(lastStats, profit)); + updateText(`calcHashAmount${coin}2`, market_GetCurrencyPriceText(lastStats, profit, marketPrices)); + //return; + } else { + updateText(`calcHashAmount${coin}1`, ''); + updateText(`calcHashAmount${coin}2`, ''); + } + } + catch (e){ + updateText(`calcHashAmount${coin}1`, ''); + updateText(`calcHashAmount${coin}2`, ''); + } + + + + Object.keys(mergedStats).forEach(key => { + try { + if ($(`#calcHashAmount${key}`).length == 0) { + let template = $(`#calcHashResultTemplate`).html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key}) + $(`#calcHashHolder`).append(rendered) + } + + let profit = (hashRate * 86400 / mergedStats[key].network.difficulty) * mergedStats[key].lastblock.reward; + if (profit) { + updateText(`calcHashAmount${key}1`, getReadableCoin(mergedStats[key], profit)); + updateText(`calcHashAmount${key}2`, market_GetCurrencyPriceText(mergedStats[key], profit, marketPrices)); + return; + } else { + updateText(`calcHashAmount${key}1`, ''); + updateText(`calcHashAmount${key}2`, ''); + } + } + catch(e) + { + updateText(`calcHashAmount${key}1`, ''); + updateText(`calcHashAmount${key}2`, ''); + } + }) +} + + +// Get price in specified currency +function market_GetCurrencyPriceText(stats, coinsRaw, marketPrices) { + if (!priceCurrency || !marketPrices[stats.config.coin] || !marketPrices[stats.config.coin][priceCurrency]) return ; + let priceInCurrency = (Math.trunc(getReadableCoin(stats, coinsRaw, 2, true) * marketPrices[stats.config.coin][priceCurrency] * 100) / 100); + return priceInCurrency + ' ' + priceCurrency; +} + +function market_InitTemplate(ranOnce, chartsInitialized, loadedData, marketPrices, intervalChartsUpdate, currencyPairs, xhrMarketGets) { + priceSource = lastStats.config.priceSource || 'cryptonator'; + priceCurrency = lastStats.config.priceCurrency || 'USD'; + + let coin = lastStats.config.coin + if ($(`#blocksTabs li:contains(${coin})`).length == 0) { + chartsInitialized[coin] = false + loadedData[coin] = false + marketPrices[coin] = {} + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active:'active'}); + $('#blocksTabs').append(rendered) + + let template1 = $('#siblingMarketTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:coin, active:'active'}) + $(`#tab-content`).append(rendered1) + + + let template2 = $('#siblingCalculatorTemplate').html() + Mustache.parse(template2) + let rendered2 = Mustache.render(template2, {coin:coin}) + $(`#calculator`).append(rendered2) + + updateText(`priceChartCurrency${lastStats.config.coin}`, priceCurrency); + updateText(`profitChartProfit${lastStats.config.coin}`, priceCurrency); + + if (lastStats.charts && !chartsInitialized[coin]) { + intervalChartsUpdate[coin] = setInterval(market_CreateCharts(lastStats), 60*1000); + market_CreateCharts(lastStats); + chartsInitialized[coin] = true; + } + + } + + market_LoadMarketData(api, lastStats, loadedData, currencyPairs, xhrMarketGets, marketPrices); + + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length === 0) { + chartsInitialized[key] = false; + loadedData[key] = false + marketPrices[key] = {} + let template1 = $('#siblingMarketTemplate').html() + Mustache.parse(template1) + let rendered1 = Mustache.render(template1, {coin:key}) + $('#tab-content').append(rendered1) + + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + } + + updateText(`priceChartCurrency${mergedStats[key].config.coin}`, priceCurrency); + updateText(`profitChartProfit${mergedStats[key].config.coin}`, priceCurrency); + + market_LoadMarketData(mergedApis[key].api, mergedStats[key], loadedData, currencyPairs, xhrMarketGets, marketPrices); + + if (mergedStats[key].charts && !chartsInitialized[key]) { + intervalChartsUpdate[key] = setInterval(market_CreateCharts(mergedStats[key]), 60*1000); + market_CreateCharts(mergedStats[key]); + chartsInitialized[key] = true; + } + + }) + + market_CalcEstimateProfit(marketPrices); + +sortElementList($(`#blocksTabs`), $(`#blocksTabs>li`), mergedStats) + + if (!ranOnce) + ranOnce = RunOnce() +} + +/* +*************************************************************** +workerstats methods +*************************************************************** +*/ + +function workerstats_Setup(stats, api, addressTimeout, xhrAddressPoll, xhrGetPayments ) { + + // Enable time ago on last submitted share + $(`#yourLastShare${stats.config.coin}`).timeago(); + + $(`#lookUp${stats.config.coin}`).click(function(){ + var address = $(`#yourStatsInput${stats.config.coin}`).val().trim(); + + if (getCurrentAddress(stats.config.coin) != address) { + docCookies.setItem(`mining_address_${stats.config.coin}`, address, Infinity); + + var urlWalletAddress = location.search.split('walletMerged=')[1] || 0; + if (urlWalletAddress){ + window.location.href = "/#worker_stats"; + return ; + } + else { + docCookies.setItem(`mining_address_${stats.config.coin}`, address, Infinity); + loadLiveStats(true, mergedStats); + } + } + + $(`#addressError${stats.config.coin}, .yourStats${stats.config.coin}, .yourWorkers${stats.config.coin}, .userChart${stats.config.coin}`).hide(); + $(`#workersReport_rows_${stats.config.coin}`).empty(); + $(`#paymentsReport_rows_${stats.config.coin}`).empty(); + + $(`#lookUp${stats.config.coin} > span:first-child`).hide(); + $(`#lookUp${stats.config.coin} > span:last-child`).show(); + + + if (addressTimeout[stats.config.coin]) clearTimeout(addressTimeout[stats.config.coin]); + + + if (xhrAddressPoll[stats.config.coin]) + xhrAddressPoll[stats.config.coin].abort(); + + $(`#lookUp${stats.config.coin} > span:last-child`).hide(); + $(`#lookUp${stats.config.coin} > span:first-child`).show(); + + if (!address){ + $(`#yourStatsInput${stats.config.coin}`).focus(); + return; + } + + workerstats_FetchAddressStats(false, stats, api, xhrAddressPoll); + + }); + + var address = getCurrentAddress(stats.config.coin); + if (address){ + $(`#yourStatsInput${stats.config.coin}`).val(address); + $(`#lookUp${stats.config.coin}`).click(); + } + else { + $(`#lookUp${stats.config.coin} > span:last-child`).hide(); + $(`#lookUp${stats.config.coin} > span:first-child`).show(); + $(`#addressError${stats.config.coin}, .yourStats${stats.config.coin}, .yourWorkers${stats.config.coin}, .userChart${stats.config.coin}`).hide(); + } + + $(`#yourStatsInput${stats.config.coin}`).keyup(function(e){ + if(e.keyCode === 13) + $(`#lookUp${stats.config.coin}`).click(); + }); + + + // Handle sort on workers table + //$(`#workersReport${stats.config.coin} th.sort`).on('click', sortTable); + $(`.workerStats th.sort`).on('click', sortTable); + + // Load more payments button + $(`#loadMorePayments${stats.config.coin}`).click(function(xhrGetPayments){ + if (xhrGetPayments[stats.config.coin]) + xhrGetPayments[stats.config.coin].abort(); + + xhrGetPayments[stats.config.coin] = $.ajax({ + url: `${api}/get_payments`, + data: { + time: $(`#paymentsReport_rows_${stats.config.coin}`).children().last().data('time'), + address: address + }, + dataType: 'json', + cache: 'false', + success: function(data){ + workerstats_RenderPayments(data, stats); + } + }); + }); +} + +/** + * Miner statistics + **/ + + +// Load current miner statistics + +function workerstats_FetchAddressStats(longpoll, stats, api, xhrAddressPoll){ + let address = getCurrentAddress(stats.config.coin) + xhrAddressPoll[stats.config.coin] = $.ajax({ + url: `${api}/stats_address`, + data: { + address: address, + longpoll: longpoll + }, + dataType: 'json', + cache: 'false', + success: function(data){ + if (!data.stats){ + $(`.yourStats${stats.config.coin}, .yourWorkers${stats.config.coin}, .userChart${stats.config.coin}`).hide(); + $(`#addressError${stats.config.coin}`).text(data.error).show(); + docCookies.setItem(`mining_address_${stats.config.coin}`, '', Infinity); + loadLiveStats(true); + return; + } + $(`#addressError${stats.config.coin}`).hide(); + + + if (data.stats.lastShare) { + $(`#yourLastShare${stats.config.coin}`).timeago('update', new Date(parseInt(data.stats.lastShare) * 1000).toISOString()); + } // AQUÍ + else { + updateText(`yourLastShare${stats.config.coin}`, 'Never'); + } + + + updateText(`yourHashrateHolder${stats.config.coin}`, (getReadableHashRateString(data.stats.hashrate) || '0 H') + '/sec'); + if ('hashrate_1h' in data.stats) { + $(`#minerAvgHR${stats.config.coin}`).show(); + updateText(`yourHR1h${stats.config.coin}`, (getReadableHashRateString(data.stats.hashrate_1h) || '0 H') + '/s'); + updateText(`yourHR6h${stats.config.coin}`, (getReadableHashRateString(data.stats.hashrate_6h) || '0 H') + '/s'); + updateText(`yourHR24h${stats.config.coin}`, (getReadableHashRateString(data.stats.hashrate_24h) || '0 H') + '/s'); + } else { + $(`#minerAvgHR${stats.config.coin}`).hide(); + } + + let totalCoins = data.stats.paid; + let last24hCoins = 0; + let last7dCoins = 0; + + for (let i = 0; i < data.payments.length; i += 2) { + let payment = workerstats_ParsePayment(data.payments[i + 1], data.payments[i]); + let paymentDate = new Date(parseInt(payment.time) * 1000); + let daysDiff = moment().diff(moment(paymentDate), 'days'); + + if (daysDiff < 1) { + last24hCoins = last24hCoins + parseInt(payment.amount); + } + + if (daysDiff < 7) { + last7dCoins = last7dCoins + parseInt(payment.amount); + } + } + + + // $.getJSON(`https://api.coingecko.com/api/v3/coins/${stats.config.coin.toLowerCase()}?sparkline=true`, function() {}) + // .done(data => { + // let paidTotalUSD = getReadableCoin(stats, totalCoins, 2, true) * data.market_data.current_price.usd; + // let paid24hUSD = getReadableCoin(stats, last24hCoins, 2, true) * data.market_data.current_price.usd; + // let paid7dUSD = getReadableCoin(stats, last7dCoins, 2, true) * data.market_data.current_price.usd; + + // updateText(`yourPaid${stats.config.coin}`, `${getReadableCoin(stats, totalCoins)} - $${paidTotalUSD.toFixed(2)}`); + // updateText(`paid24h${stats.config.coin}`, `${getReadableCoin(stats, last24hCoins)} - $${paid24hUSD.toFixed(2)}`); + // updateText(`paid7d${stats.config.coin}`, `${getReadableCoin(stats, last7dCoins)} - $${paid7dUSD.toFixed(2)}`); + // }) + // .fail(() => { + updateText(`yourPaid${stats.config.coin}`, getReadableCoin(stats, totalCoins)); + updateText(`paid24h${stats.config.coin}`, getReadableCoin(stats, last24hCoins)); + updateText(`paid7d${stats.config.coin}`, getReadableCoin(stats, last7dCoins)); + // }) + + + + + updateText(`yourHashes${stats.config.coin}`, (data.stats.hashes || 0).toString()); + //updateText(`yourPaid${stats.config.coin}`, getReadableCoin(stats, data.stats.paid)); + updateText(`yourPendingBalance${stats.config.coin}`, getReadableCoin(stats, data.stats.balance)); + + let userRoundHashes = parseInt(data.stats.roundHashes || 0); + let poolRoundHashes = parseInt(stats.pool.roundHashes || 0); + let userRoundScore = parseFloat(data.stats.roundScore || 0); + let poolRoundScore = parseFloat(stats.pool.roundScore || 0); + let lastReward = parseFloat(stats.lastblock.reward || 0); + + + let poolFee = stats.config.fee; + if (Object.keys((stats.config.donation)).length) { + let totalDonation = 0; + let ldon = stats.config.donation; + for(let i in ldon) { + totalDonation += ldon[i]; + } + poolFee += totalDonation; + } + let transferFee = stats.config.transferFee; + + let share_pct = userRoundHashes * 100 / poolRoundHashes; + let score_pct = userRoundScore * 100 / poolRoundScore; + updateText(`yourRoundShareProportion${stats.config.coin}`, isNaN(share_pct) ? 0.0 : Math.round(share_pct * 1000) / 1000); + updateText(`yourRoundScoreProportion${stats.config.coin}`, isNaN(score_pct) ? 0.0 : Math.round(score_pct * 1000) / 1000); + if (!lastStats.config.slushMiningEnabled) { + $(`#slush_round_info${stats.config.coin}`).hide(); + } + + let payoutEstimatePct = parseFloat(userRoundHashes * 100 / poolRoundHashes) + let payoutEstimate = Math.round(lastReward * (payoutEstimatePct / 100)); + if (transferFee) payoutEstimate = payoutEstimate - transferFee; + if (payoutEstimate < 0) + payoutEstimate = 0; + updateText(`yourPayoutEstimate${stats.config.coin}`, getReadableCoin(stats, payoutEstimate)); + + + workerstats_RenderPayments(data.payments, stats); + + if (data.workers && data.workers.length > 0) { + workerstats_RenderWorkers(data.workers, stats); + $(`.yourWorkers${stats.config.coin}`).show(); + } + + $(`.yourStats${stats.config.coin}`).show(); + workerstats_CreateCharts(data, stats); + + }, + error: function(e){ + if (e.statusText === 'abort') return; + $(`#addressError${stats.config.coin}`).text('Connection error').show(); + + if (addressTimeout[stats.config.coin]) + clearTimeout(addressTimeout[stats.config.coin]); + + addressTimeout[stats.config.coin] = setTimeout(function(){ + workerstats_FetchAddressStats(false, stats, mergedApis[stats.config.coin].api); + }, 2000); + } + }); +} + +/** + * Charts + **/ + +// Create charts +function workerstats_CreateCharts(data, stats) { + if (data.hasOwnProperty("charts")) { + var graphData = { + hashrate: workerstats_GetGraphData(stats, data.charts.hashrate), + payments: workerstats_GetGraphData(stats, data.charts.payments, true) + }; + + for(var graphType in graphData) { + if(graphData[graphType].values.length > 1) { + var settings = jQuery.extend({}, graphSettings); + settings.tooltipValueLookups = {names: graphData[graphType].names}; + var $chart = $(`[data-chart=user_${graphType}_${stats.config.coin}]`).show().find('.chart'); + $chart.sparkline(graphData[graphType].values, settings); + } + } + } +} + +// Get chart data +function workerstats_GetGraphData(stats, rawData, fixValueToCoins) { + var graphData = { + names: [], + values: [] + }; + + if(rawData) { + for (var i = 0, xy; xy = rawData[i]; i++) { + graphData.names.push(new Date(xy[0]*1000).toLocaleString()); + graphData.values.push(fixValueToCoins ? getReadableCoin(stats, xy[1], null, true) : xy[1]); + } + } + + return graphData; +} + +/** + * Workers report + **/ + +// Get worker row id +function workerstats_GetWorkerRowId(workerName){ + var id = btoa(workerName); + id = id.replace(/=/, ''); + return id; +} + +// Get worker row element +function workerstats_GetWorkerRowElement(worker, jsonString, stats){ + var row = document.createElement('tr'); + row.setAttribute('data-json', jsonString); + row.setAttribute('data-name', worker.name); + row.setAttribute('id', 'workerRow' + stats.config.coin + '_' + workerstats_GetWorkerRowId(worker.name)); + + row.innerHTML = workerstats_GetWorkerCells(worker); + + return row; +} + +// Get worker cells +function workerstats_GetWorkerCells(worker){ + let hashrate = worker.hashrate ? worker.hashrate : 0; + let hashrate1h = worker.hashrate_1h || 0; + let hashrate6h = worker.hashrate_6h || 0; + let hashrate24h = worker.hashrate_24h || 0; + let lastShare = worker.lastShare ? worker.lastShare : 0; + let hashes = (worker.hashes || 0).toString(); + let status = (hashrate <= 0) ? 'error' : 'ok'; + + return '' + + '' + (worker.name != 'undefined' ? worker.name : 'Undefined') + '' + + '' + getReadableHashRateString(hashrate) + '/s' + + '' + getReadableHashRateString(hashrate1h) + '/s' + + '' + getReadableHashRateString(hashrate6h) + '/s' + + '' + getReadableHashRateString(hashrate24h) + '/s' + + '' + (lastShare ? $.timeago(new Date(parseInt(lastShare) * 1000).toISOString()) : 'Never') + '' + + '' + hashes + ''; +} + +// Sort workers +function workerstats_SortWorkers(a, b){ + var aName = a.name.toLowerCase(); + var bName = b.name.toLowerCase(); + return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); +} + +// Render workers list +function workerstats_RenderWorkers(workersData, stats){ + workersData = workersData.sort(workerstats_SortWorkers); + + var $workersRows = $(`#workersReport_rows_${stats.config.coin}`); + + for (var i = 0; i < workersData.length; i++){ + var existingRow = document.getElementById(`workerRow${stats.config.coin}_${workerstats_GetWorkerRowId(workersData[i].name)}`); + if (!existingRow){ + $workersRows.empty(); + break; + } + } + + let have_avg_hr = false; + + for (var i = 0; i < workersData.length; i++){ + var worker = workersData[i]; + if (Date.now()/1000 - parseInt(worker.lastShare) > 2 * 86400) continue; + if (!have_avg_hr && 'hashrate_1h' in worker) have_avg_hr = true; + var workerJson = JSON.stringify(worker); + var existingRow = document.getElementById(`workerRow${stats.config.coin}_${workerstats_GetWorkerRowId(worker.name)}`); + if (existingRow && existingRow.getAttribute('data-json') !== workerJson){ + $(existingRow).replaceWith(workerstats_GetWorkerRowElement(worker, workerJson, stats)); + } + else if (!existingRow){ + var workerElement = workerstats_GetWorkerRowElement(worker, workerJson, stats); + $workersRows.append(workerElement); + } + } + if (!have_avg_hr) $(`#workersReport${stats.config.coin} .avghr`).hide(); + else $(`#workersReport${stats.config.coin} .avghr`).show(); +} + +/** + * Payments report + **/ + +// Parse payment data +function workerstats_ParsePayment(time, serializedPayment){ + var parts = serializedPayment.split(':'); + return { + time: parseInt(time), + hash: parts[0], + amount: parts[1], + fee: parts[2], + mixin: parts[3], + recipients: parts[4] + }; +} + +// Get payment row element +function workerstats_GetPaymentRowElement(payment, jsonString, stats){ + var row = document.createElement('tr'); + row.setAttribute('data-json', jsonString); + row.setAttribute('data-time', payment.time); + row.setAttribute('id', 'paymentRow' + stats.config.coin + payment.time); + + row.innerHTML = workerstats_GetPaymentCells(payment, stats); + + return row; +} + +// Get payment cells +function workerstats_GetPaymentCells(payment, stats){ + return '' + formatDate(payment.time) + '' + + '' + formatPaymentLink(payment.hash, stats) + '' + + '' + getReadableCoin(stats, payment.amount) + '' + + '' + payment.mixin + ''; +} + +// Get summary row element +function workerstats_GetSummaryRowElement(summary, jsonString, stats){ + var row = document.createElement('tr'); + row.setAttribute('data-json', jsonString); + row.setAttribute('data-date', summary.date); + row.setAttribute('id', 'summaryRow' + stats.config.coin + summary.date); + row.setAttribute('class', 'summary'); + + row.innerHTML = workerstats_GetSummaryCells(summary, stats); + + return row; +} + +// Get summary cells +function workerstats_GetSummaryCells(summary, stats){ + var text = getTranslation('paymentSummaryMulti') ? getTranslation('paymentSummaryMulti') : 'On %DATE% you have received %AMOUNT% in %COUNT% payments'; + if (summary.count <= 1) text = getTranslation('paymentSummarySingle') ? getTranslation('paymentSummarySingle') : 'On %DATE% you have received %AMOUNT%'; + text = text.replace(/%DATE%/g, summary.date); + text = text.replace(/%COUNT%/g, summary.count); + text = text.replace(/%AMOUNT%/g, getReadableCoin(stats, summary.amount)); + return '' + text + ''; +} + +// Render payments +function workerstats_RenderPayments(paymentsResults, stats){ + var $paymentsRows = $(`#paymentsReport_rows_${stats.config.coin}`); + var lastPaymentDate = null; + var summaryData = { date: null, time: null, count: 0, amount: 0 }; + for (var i = 0; i < paymentsResults.length; i += 2){ + var payment = workerstats_ParsePayment(paymentsResults[i + 1], paymentsResults[i]); + var paymentJson = JSON.stringify(payment); + var paymentElement = workerstats_GetPaymentRowElement(payment, paymentJson, stats); + + var paymentDate = new Date(parseInt(payment.time) * 1000).toLocaleDateString(); + if (!lastPaymentDate || lastPaymentDate && paymentDate != lastPaymentDate) { + summaryData = { date: paymentDate, time: payment.time, count: 0, amount: 0 }; + } + + var existingRow = document.getElementById(`paymentRow${stats.config.coin}${payment.time}`); + if (existingRow && existingRow.getAttribute('data-json') !== paymentJson){ + $(existingRow).replaceWith(workerstats_GetPaymentRowElement(payment, paymentJson, stats)); + } + else if (!existingRow){ + var inserted = false; + var rows = $paymentsRows.children().get(); + for (var f = 0; f < rows.length; f++) { + var pTime = parseInt(rows[f].getAttribute('data-time')); + if (pTime && pTime < payment.time){ + inserted = true; + $(rows[f]).before(paymentElement); + break; + } + } + if (!inserted) { + $paymentsRows.append(paymentElement); + } + } + + summaryData.count ++; + summaryData.amount += parseInt(payment.amount); + + var summaryJson = JSON.stringify(summaryData); + var summaryElement = workerstats_GetSummaryRowElement(summaryData, summaryJson, stats); + + var existingSummary = document.getElementById(`summaryRow${stats.config.coin}${summaryData.date}`); + if (existingSummary && existingSummary.getAttribute('data-json') !== summaryJson){ + $(existingSummary).replaceWith(summaryElement); + } + else if (!existingSummary){ + var inserted = false; + var rows = $paymentsRows.children().get(); + for (var f = 0; f < rows.length; f++) { + var pTime = parseInt(rows[f].getAttribute('data-time')); + if (pTime && pTime === summaryData.time){ + inserted = true; + $(rows[f]).before(summaryElement); + break; + } + } + if (!inserted) { + $paymentsRows.append(summaryElement); + } + } + lastPaymentDate = paymentDate; + } +} + +function workerstats_InitTemplate(ranOnce, addressTimeout, xhrAddressPoll, xhrGetPayments ) { + let coin = lastStats.config.coin + if ($(`#blocksTabs li:contains(${coin})`).length === 0) { + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:lastStats.config.coin, symbol:`(${lastStats.config.symbol})`, active: 'active'}); + $('#blocksTabs').append(rendered) + + template = $('#siblingTemplate').html() + Mustache.parse(template) + rendered = Mustache.render(template, {coin:coin, active: 'active'}) + $('#tab-content').append(rendered) + workerstats_Setup(lastStats, api, addressTimeout, xhrAddressPoll, xhrGetPayments) + } + + Object.keys(mergedStats).forEach(key => { + if ($(`#blocksTabs li:contains(${key})`).length === 0) { + coin = key + let template = $('#siblingTabTemplate').html(); + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:mergedStats[key].config.coin, symbol:`(${mergedStats[key].config.symbol})`}); + $('#blocksTabs').append(rendered) + + template = $('#siblingTemplate').html() + Mustache.parse(template) + rendered = Mustache.render(template, {coin:coin}) + $('#tab-content').append(rendered) + workerstats_Setup(mergedStats[key], mergedApis[key].api, addressTimeout, xhrAddressPoll, xhrGetPayments) + } + }) + + sortElementList($(`#blocksTabs`), $(`#blocksTabs>li`), mergedStats) + + if (!ranOnce) + ranOnce = RunOnce() +} + + +/* +*************************************************************** +workerstats methods +*************************************************************** +*/ + +let home_GraphSettings = { + type: 'line', + width: '100%', + height: '140', + lineColor: '#03a9f4', + fillColor: 'rgba(3, 169, 244, .4)', + spotColor: null, + minSpotColor: null, + maxSpotColor: null, + highlightLineColor: '#236d26', + spotRadius: 3, + chartRangeMin: 0, + drawNormalOnTop: false, + tooltipFormat: '{{y}} – {{offset:names}}' +}; + +function home_CreateCharts(data) { + if (data.hasOwnProperty("charts")) { + var graphData = { + hashrate: home_GetGraphData(data.charts.hashrate), + diff: home_GetGraphData(data.charts.difficulty), + miners: home_GetGraphData(data.charts.miners), + workers: home_GetGraphData(data.charts.workers) + }; + + for(var graphType in graphData) { + if(graphData[graphType].values.length > 1) { + var settings = jQuery.extend({}, home_GraphSettings); + settings.tooltipValueLookups = {names: graphData[graphType].names}; + var $chart = $('[data-chart=' + graphType + '] .chart'); + $chart.closest('.poolChart').show(); + $chart.sparkline(graphData[graphType].values, settings); + } + } + } +} + +// Get chart data +function home_GetGraphData(rawData, fixValueToCoins) { + var graphData = { + names: [], + values: [] + }; + if(rawData) { + for (var i = 0, xy; xy = rawData[i]; i++) { + graphData.names.push(new Date(xy[0]*1000).toLocaleString()); + graphData.values.push(fixValueToCoins ? getReadableCoin(lastStats, xy[1], null, true) : xy[1]); + } + } + + return graphData; +} + +function home_GenerateNetworkStats(key, symbol) { + if ($(`#networkStats${key}`).length == 0) { + let template = $('#siblingTemplate').html() + if (template) { + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:key, symbol: symbol}) + $(`#networkStats`).append(rendered) + } + } +} + +function sortElementList(container, siblings, stats) { + let sorted = (a,b) => { + return ((a.id.toLowerCase() < b.id.toLowerCase()) ? -1 : ((a.id.toLowerCase() > b.id.toLowerCase()) ? 1 : 0)) + } + if (stats && siblings.length -1 === Object.keys(stats).length) { + siblings.sort(sorted).appendTo(container) + } +} + + +function home_InitTemplate(parentStats, siblingStats) { + $('#networkLastBlockFound').timeago('update', new Date(parentStats.lastblock.timestamp * 1000).toISOString()); + + let coin = parentStats.config.coin + let minerInfo = [] + let efforts = [] + + if ($(`#networkStats${coin}`).length == 0) { + minerInfo.push({blocks: parentStats.pool.totalBlocks.toString(), + blocksSolo: parentStats.pool.totalBlocksSolo.toString(), + coin: coin, + symbol: parentStats.config.symbol, + miners: parentStats.pool.miners.toString(), + minersSolo: parentStats.pool.minersSolo.toString()}) + + efforts.push({coin: coin, effort: `${(parentStats.pool.roundHashes / parentStats.network.difficulty * 100).toFixed(1)}%`,symbol: parentStats.config.symbol}) + + let template = $('#siblingTemplate').html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin: coin, symbol: parentStats.config.symbol}) + $(`#networkStats`).append(rendered) + } + + let lastBlockFound = null + if (parentStats.pool.lastBlockFound) { + lastBlockFound = parseInt(parentStats.pool.lastBlockFound); + } + + + updateText(`networkHashrate${coin}`, getReadableHashRateString(parentStats.network.difficulty / parentStats.config.coinDifficultyTarget) + '/sec'); + updateText(`networkDifficulty${coin}`, formatNumber(parentStats.network.difficulty.toString(), ' ')); + updateText(`blockchainHeight${coin}`, formatNumber(parentStats.network.height.toString(), ' ')); + updateText(`networkLastReward${coin}`, getReadableCoin(parentStats, parentStats.lastblock.reward)); + + + + Object.keys(siblingStats).forEach(key => { + home_GenerateNetworkStats(key, siblingStats[key].config.symbol) + + minerInfo.push({blocks: siblingStats[key].pool.totalBlocks.toString(), + blocksSolo: siblingStats[key].pool.totalBlocksSolo.toString(), + coin: key, + symbol: siblingStats[key].config.symbol, + miners: siblingStats[key].pool.miners.toString(), + minersSolo: siblingStats[key].pool.minersSolo.toString()}) + + efforts.push({coin: key, effort: `${(siblingStats[key].pool.roundHashes / siblingStats[key].network.difficulty * 100).toFixed(1)}%`, symbol: siblingStats[key].config.symbol}); + + if (siblingStats[key].pool.lastBlockFound) { + let lastChildBlockFound = parseInt(siblingStats[key].pool.lastBlockFound) + if (lastChildBlockFound > lastBlockFound) + lastBlockFound = lastChildBlockFound + } + + updateText(`networkHashrate${key}`, getReadableHashRateString(siblingStats[key].network.difficulty / siblingStats[key].config.coinDifficultyTarget) + '/sec'); + updateText(`networkDifficulty${key}`, formatNumber(siblingStats[key].network.difficulty.toString(), ' ')); + updateText(`blockchainHeight${key}`, formatNumber(siblingStats[key].network.height.toString(), ' ')); + updateText(`networkLastReward${key}`, getReadableCoin(siblingStats[key], siblingStats[key].lastblock.reward)); + updateText(`poolMiners${key}`, `${siblingStats[key].pool.miners}/${siblingStats[key].pool.minersSolo}`); + updateText(`blocksTotal${key}`, `${siblingStats[key].pool.totalBlocks}/${siblingStats[key].pool.totalBlocksSolo}`); + updateText(`currentEffort${key}`, (siblingStats[key].pool.roundHashes / siblingStats[key].network.difficulty * 100).toFixed(1) + '%'); + }) + + sortElementList($(`#networkStats`), $(`#networkStats>div`), siblingStats) + + if ($(`#poolDetails > div`).length == 0) { + let template = $('#poolDetailTemplate').html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:parentStats.config.coin, symbol: parentStats.config.symbol, blocks: minerInfo}) + $(`#poolDetails`).append(rendered) + } + + if ($(`#mainPoolStats > div`).length == 0) { + let template = $('#mainPoolTemplate').html() + Mustache.parse(template) + let rendered = Mustache.render(template, {coin:parentStats.config.coin, blocks: minerInfo, efforts: efforts}) + $(`#mainPoolStats`).append(rendered) + } + + + if (lastBlockFound) { + $('#poolLastBlockFound').timeago('update', new Date(lastBlockFound).toISOString()); + } + else { + $('#poolLastBlockFound').removeAttr('title').data('ts', '').update('Never'); + } + + let lastHash = updateText('lastHash', parentStats.lastblock.hash) + if (lastHash) + lastHash.setAttribute('href', getBlockchainUrl(parentStats.lastblock.hash, parentStats)); + + + updateText('poolHashrate', `PROP: ${getReadableHashRateString(parentStats.pool.hashrate)}/sec`); + updateText('poolHashrateSolo', `SOLO: ${getReadableHashRateString(parentStats.pool.hashrateSolo)}/sec`); + + + var hashPowerSolo = parentStats.pool.hashrateSolo / (parentStats.network.difficulty / parentStats.config.coinDifficultyTarget) * 100; + updateText ('hashPowerSolo', hashPowerSolo.toFixed(2) + '%'); + + var hashPower = parentStats.pool.hashrate / (parentStats.network.difficulty / parentStats.config.coinDifficultyTarget) * 100; + updateText('hashPower', hashPower.toFixed(2) + '%'); + + + updateText(`poolMiners${coin}`, `${parentStats.pool.miners}/${parentStats.pool.minersSolo}`); + updateText('blocksTotal', `${parentStats.pool.totalBlocks}/${parentStats.pool.totalBlocksSolo}`); + + + var totalFee = parentStats.config.fee; + if (Object.keys(parentStats.config.donation).length) { + var totalDonation = 0; + for(var i in parentStats.config.donation) { + totalDonation += parentStats.config.donation[i]; + } + totalFee += totalDonation; + } + + updateText('poolFee', (totalFee > 0 && totalFee != 100 ? floatToString(totalFee) : (totalFee == 100 ? '100' : '0')) + '%'); + + updateText('paymentsInterval', getReadableTime(parentStats.config.paymentsInterval)); + updateText('paymentsMinimum', getReadableCoin(parentStats, parentStats.config.minPaymentThreshold)); + + updateText('blockSolvedTime', getReadableTime(parentStats.network.difficulty / parentStats.pool.hashrate)); + + updateText(`currentEffort${coin}`, (parentStats.pool.roundHashes / parentStats.network.difficulty * 100).toFixed(1) + '%'); +} diff --git a/website_example/js/custom.js b/website_example/js/custom.js new file mode 100644 index 0000000..4bdcd2b --- /dev/null +++ b/website_example/js/custom.js @@ -0,0 +1 @@ +/* Insert your pool's unique Javascript here */ \ No newline at end of file diff --git a/website_example/lang/ca.json b/website_example/lang/ca.json new file mode 100644 index 0000000..aa7f855 --- /dev/null +++ b/website_example/lang/ca.json @@ -0,0 +1,166 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "Tauler", + "gettingStarted": "Començar", + "yourStats": "Estadístiques personals", + "poolBlocks": "Blocs de la Pool", + "settings": "Ajustaments", + "faq": "FAQ", + "telegram": "Grup de Telegram", + "discord": "Discord", + "contactUs": "Contactar", + "network": "Xarxa", + "pool": "Pool", + "you": "La teva taxa", + "statsUpdated": "Estadístiques actualitzades", + + "poolHashrate": "Taxa de la Pool (hash)", + "currentEffort": "Esforç actual", + "networkHashrate": "Taxa de la Xarxa (hash)", + "networkDifficulty": "Dificultat", + "blockchainHeight": "Altura cadena de blocs", + "networkLastReward": "Última recompensa", + "poolMiners": "Miners connectats", + "poolFee": "Tarifa de la Pool", + + "minerStats": "Les teves estadístiques i historial de pagaments", + "workerStats": "Estadístiques personals", + "miner": "Miner", + "miners": "Miners", + "minersCount": "miners", + "workers": "Treballadors", + "workersCount": "treballadors", + "workerName": "Nom Treballador", + "lastHash": "Últim hash", + "hashRate": "Taxa (hash)", + "currentHashRate": "Taxa actual (hash)", + "lastShare": "Última acció enviada", + "totalHashes": "Total d'accions enviades", + "top10miners": "Top 10 Miners", + + "blocksTotal": "Blocs trobats", + "blockSolvedTime": "Bloc trobat cada", + "blocksMaturityCount": "Maduresa requerida", + "efficiency": "Eficiència", + "averageLuck": "Promitg de sort", + "timeFound": "Data i hora", + "reward": "Recompensa", + "height": "Altura", + "difficulty": "Dificultat", + "blockHash": "Bloc Hash", + "effort": "Esforç", + "blocksFoundLast24": "Blocks found in the last 24 hours", + "blocksFoundLastDays": "Blocks found in the last {DAYS} days", + + "payments": "Pagaments", + "paymentsHistory": "Historial de pagaments", + "paymentsTotal": "Total de pagaments", + "paymentsMinimum": "Pagament mínim", + "paymentsInterval": "Interval de pagaments", + "paymentsDenomination": "Unitat de denominació", + "timeSent": "Data", + "transactionHash": "Hash transacció", + "amount": "Import", + "fee": "Tarifa", + "mixin": "Mixin", + "payees": "Beneficiàris", + "pendingBalance": "Balanç pendent", + "totalPaid": "Total pagat", + "payoutEstimate": "Pagament estimat", + "paymentSummarySingle": "El %DATE% has rebut %AMOUNT%", + "paymentSummaryMulti": "El %DATE% has rebut %AMOUNT% en %COUNT% pagaments", + + "connectionDetails": "Detalls de connexió", + "cnAlgorithm": "Algoritme", + "miningPoolHost": "Adreça de la Pool", + "username": "Usuari", + "usernameDesc": "La teva adreça del wallet", + "paymentId": "ID de pagament (Exchanges)", + "fixedDiff": "Dificultat fixa", + "address": "adreça", + "addrPaymentId": "IDPagament", + "addrDiff": "diff", + "password": "Contrassenya", + "passwordDesc": "Nom del seu treballador", + "emailNotifications": "Notificacions per E-Mail", + "miningPorts": "Ports de mineria", + "port": "Port", + "portDiff": "Dificultat inicial", + "description": "Descripció", + "miningApps": "Aplicacions de mineria", + "configGeneratorDesc": "Genera la teva configuració personalitzada per minar a la nostra Pool", + "addressField": "Adreça Wallet", + "paymentIdField": "ID de Pagament per a Exchanges (opcional)", + "fixedDiffField": "Dificultat fixa (opcional)", + "workerNameField": "Nom_Treballador", + "emailNotificationsField": "Notificacions per E-Mail (opcional))", + "generateConfig": "Generar configuració", + "appName": "Aplicació", + "appArch": "Arquitectura", + "appDesc": "Característiques", + "download": "Descarregar", + "showConfig": "Veure més", + + "market": "Mercat / Calculadora", + "loadingMarket": "Carregant preus del mercat", + "priceIn": "Preu en", + "hashPer": "Hash/", + "estimateProfit": "Beneficis estimats", + "enterYourHashrate": "Entra la teva taxa (hash)", + "perDay": "per dia", + + "verificationFields": "Camps de verificació", + "minerVerification": "Per tenir una mica més de seguretat que l'adreça de la cartera és vostra, us demanem que proporcioneu una de les adreces IP que utilitza el vostre miner.", + "minerAddress": "Adreça del moneder", + "minerIP": "Adreça IP del Miner", + "setMinimumPayout": "Estableix el nivell mínim de pagament", + "minerMinPayout": "Si preferiu un nivell de pagament més alt que el predeterminat del grup, aquí podeu canviar-lo per als vostres miners. L'import que indiqueu aquí es convertirà en l'import mínim dels pagaments de la vostra adreça.", + "minimumPayout": "Pagament mínim", + "enableEmailNotifications": "Activar notificacions per de correu electrònic", + "minerEmailNotify": "Aquesta Pool enviarà una notificació per correu electrònic quan es trobi un bloc i quan es produeixi un pagament.", + "emailAddress": "Adreça de correu electrònic", + "noMinerAddress": "No s'ha especificat cap adreça del moneder", + "noMinerIP": "No s'especificat cap adreça IP", + "noPayoutLevel": "No s'ha especificat cap nivell de pagament", + "noEmail": "No s'ha especificat cap adreça de correu electrònic", + "invalidEmail": "S'ha especificat una adreça de correu electrònic no vàlida", + "minerPayoutSet": "Fet! S'ha establert el nivell mínim de pagament", + "notificationEnabled": "Fet! S'han activat les notificacions per correu electrònic", + "notificationDisabled": "Fet! S'han desactivat les notificacions per correu electrònic", + + "enterYourAddress": "Introduïu la vostra adreça", + "enterYourMinerIP": "Una adreça IP que utilitzin els miners (qualsevol)", + "enterYourEmail": "Introduïu la vostra adreça de correu electrònic (opcional)", + "lookup": "Cercar", + "searching": "Cercant...", + "loadMore": "Carregar més", + "set": "Establir", + "enable": "Activar", + "disable": "Desactivar", + "status": "Estat", + "updated": "Actualitzat:", + "source": "Origen:", + "error": "Error:", + + "na": "N/A", + "estimated": "estimat", + "never": "Mai", + "second": "segon", + "seconds": "segons", + "minute": "minut", + "minutes": "minuts", + "hour": "hora", + "hours": "hores", + "day": "dia", + "days": "dies", + "week": "setmana", + "weeks": "setmanes", + "month": "mes", + "months": "mesos", + "year": "any", + "years": "anys", + + "poweredBy": "Powered by", + "openSource": "open sourced under the" + +} diff --git a/website_example/lang/en.json b/website_example/lang/en.json new file mode 100644 index 0000000..ff5f742 --- /dev/null +++ b/website_example/lang/en.json @@ -0,0 +1,168 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "Dashboard", + "gettingStarted": "Getting Started", + "yourStats": "Worker Statistics", + "poolBlocks": "Pool Blocks", + "settings": "Settings", + "faq": "FAQ", + "telegram": "Telegram group", + "discord": "Discord", + "contactUs": "Contact Us", + + "network": "Network", + "pool": "Pool", + "you": "You", + "statsUpdated": "Stats Updated", + + "poolHashrate": "Pool Hash Rate", + "currentEffort": "Current Effort", + "networkHashrate": "Network Hash Rate", + "networkDifficulty": "Difficulty", + "blockchainHeight": "Blockchain Height", + "networkLastReward": "Last Reward", + "poolMiners": "Connected Miners", + "poolFee": "Pool Fee", + + "minerStats": "Your Stats & Payment History", + "workerStats": "Workers Statistics", + "miner": "Miner", + "miners": "Miners", + "minersCount": "miners", + "workers": "Workers", + "workersCount": "workers", + "workerName": "Worker Name", + "lastHash": "Last Hash", + "hashRate": "Hash Rate", + "currentHashRate": "Current Hash Rate", + "lastShare": "Last Share Submitted", + "totalHashes": "Total Hashes Submitted", + "top10miners": "Top 10 miners", + + "blocksTotal": "Blocks Found", + "blockSolvedTime": "Blocks Found Every", + "blocksMaturityCount": "Maturity Requirement", + "efficiency": "Efficiency", + "averageLuck": "Average Luck", + "timeFound": "Time Found", + "reward": "Reward", + "height": "Height", + "difficulty": "Difficulty", + "blockHash": "Block Hash", + "effort": "Effort", + "blocksFoundLast24": "Blocks found in the last 24 hours", + "blocksFoundLastDays": "Blocks found in the last {DAYS} days", + + "payments": "Payments", + "paymentsHistory": "Payments History", + "paymentsTotal": "Total Payments", + "paymentsMinimum": "Minimum Payout", + "paymentsInterval": "Payment Interval", + "paymentsDenomination": "Denomination Unit", + "timeSent": "Time Sent", + "transactionHash": "Transaction Hash", + "amount": "Amount", + "fee": "Fee", + "mixin": "Mixin", + "payees": "Payees", + "pendingBalance": "Pending Balance", + "totalPaid": "Total Paid", + "payoutEstimate": "Current Payout Estimate", + "paymentSummarySingle": "On %DATE% you have received %AMOUNT%", + "paymentSummaryMulti": "On %DATE% you have received %AMOUNT% in %COUNT% payments", + + "connectionDetails": "Connection Details", + "miningPoolHost": "Mining Pool Address", + "cnAlgorithm": "Algorithm", + "username": "Username", + "usernameDesc": "This is your wallet address", + "paymentId": "Exchange Payment ID", + "fixedDiff": "Difficulty locking", + "address": "address", + "addrPaymentId": "paymentID", + "addrDiff": "diff", + "password": "Password", + "passwordDesc": "This is your worker name", + "emailNotifications": "Email Notifications", + "miningPorts": "Mining Ports", + "port": "Port", + "portDiff": "Starting Difficulty", + "description": "Description", + "miningApps": "Mining Applications", + "configGeneratorDesc": "Generate your custom configuration to mine on our pool", + "addressField": "Wallet Address", + "paymentIdField": "Payment ID for exchanges (optional)", + "fixedDiffField": "Fixed difficulty (optional)", + "workerNameField": "Worker_Name", + "emailNotificationsField": "Email Notifications (optional)", + "generateConfig": "Generate configuration", + "appName": "App Name", + "appArch": "Architecture", + "appDesc": "Features", + "download": "Download", + "showConfig": "See more", + + "market": "Market / Calculator", + "loadingMarket": "Loading market prices", + "priceIn": "Price in", + "hashPer": "Hash/", + "estimateProfit": "Estimate Mining Profits", + "enterYourHashrate": "Enter Your Hash Rate", + "perDay": "per day", + + "verificationFields": "Verification fields", + "minerVerification": "In order to get a little more confidence that the wallet address is yours we ask you to give one of the IP addresses that is used by your miner.", + "minerAddress": "Miner Address", + "minerIP": "Miner IP address", + "setMinimumPayout": "Set your minimal payout level", + "minerMinPayout": "If you prefer a higher payout level than the pool's default then this is where you can change it for your miners. The amount you indicate here will become the minimum amount for pool payments to your address.", + "minimumPayout": "Minimum payout", + "enableEmailNotifications": "Enable email notifications", + "minerEmailNotify": "This pool will send out email notification when a block is found and whenever a payout happens.", + "emailAddress": "Email address", + "noMinerAddress": "No miner address specified", + "noMinerIP": "No miner IP address specified", + "noPayoutLevel": "No payout level specified", + "noEmail": "No email address specified", + "invalidEmail": "Invalid email address specified", + "minerPayoutSet": "Done! Your minimum payout level was set", + "notificationEnabled": "Done! Email notifications have been enabled", + "notificationDisabled": "Done! Email notifications have been disabled", + + "enterYourAddress": "Enter Your Address", + "enterYourMinerIP": "An IP address your miners use (any)", + "enterYourEmail": "Enter Your E-Mail Address (optional)", + + "lookup": "Lookup", + "searching": "Searching...", + "loadMore": "Load more", + "set": "Set", + "enable": "Enable", + "disable": "Disable", + + "status": "Status", + "updated": "Updated:", + "source": "Source:", + "error": "Error:", + + "na": "N/A", + "estimated": "estimated", + "never": "Never", + "second": "second", + "seconds": "seconds", + "minute": "minute", + "minutes": "minutes", + "hour": "hour", + "hours": "hours", + "day": "day", + "days": "days", + "week": "week", + "weeks": "weeks", + "month": "month", + "months": "months", + "year": "year", + "years": "years", + + "poweredBy": "Powered by", + "openSource": "open sourced under the" +} diff --git a/website_example/lang/es.json b/website_example/lang/es.json new file mode 100644 index 0000000..b667f6f --- /dev/null +++ b/website_example/lang/es.json @@ -0,0 +1,168 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "Panel", + "gettingStarted": "Empezar", + "yourStats": "Estadísticas personales", + "poolBlocks": "Bloques de la Pool", + "settings": "Ajustes", + "faq": "FAQ", + "telegram": "Grupo de Telegram", + "discord": "Discord", + "contactUs": "Contacto", + + "network": "Red", + "pool": "Pool", + "you": "Tú Tasa", + "statsUpdated": "Estadísticas actualizadas", + + "poolHashrate": "Tasa Pool (hash)", + "currentEffort": "Ronda actual", + "networkHashrate": "Tasa Red (hash)", + "networkDifficulty": "Dificultad", + "blockchainHeight": "Altura", + "networkLastReward": "Última recompensa", + "poolMiners": "Mineros conectados", + "poolFee": "Tarifa Pool", + + "minerStats": "Tus estadísticas e historial de pagos", + "workerStats": "Estadísticas personales", + "miner": "Minero", + "miners": "Mineros", + "minersCount": "mineros", + "workers": "Trabajadores", + "workersCount": "trabajadores", + "workerName": "Nombre Trabajador", + "lastHash": "Último hash", + "hashRate": "Tasa (hash)", + "currentHashRate": "Tasa actual (hash)", + "lastShare": "Última acción enviada", + "totalHashes": "Total de acciones enviadas", + "top10miners": "Top 10 Mineros", + + "blocksTotal": "Bloques encontrados", + "blockSolvedTime": "Bloque encontrado cada", + "blocksMaturityCount": "Madurez requerida", + "efficiency": "Eficiencia", + "averageLuck": "Promedio de suerte", + "timeFound": "Fecha y hora", + "reward": "Recompensa", + "height": "Altura", + "difficulty": "Dificultad", + "blockHash": "Bloque Hash", + "effort": "Esfuerzo", + "blocksFoundLast24": "Bloques encontrados en las últimas 24 horas", + "blocksFoundLastDays": "Bloques encontrados en los últimos {DAYS} días", + + "payments": "Pagos", + "paymentsHistory": "Historial de pagos", + "paymentsTotal": "Total de pagos", + "paymentsMinimum": "Pago mínimo", + "paymentsInterval": "Intervalo de pagos", + "paymentsDenomination": "Unidad de denominación", + "timeSent": "Fecha y hora", + "transactionHash": "Hash transacción", + "amount": "Importe", + "fee": "Tarifa", + "mixin": "Mixin", + "payees": "Beneficiarios", + "pendingBalance": "Balance pendiente", + "totalPaid": "Total pagado", + "payoutEstimate": "Pago estimado", + "paymentSummarySingle": "El %DATE% has recibido %AMOUNT%", + "paymentSummaryMulti": "El %DATE% has recibido %AMOUNT% en %COUNT% pagos", + + "connectionDetails": "Detalles de conexión", + "miningPoolHost": "Dirección de la Pool", + "cnAlgorithm": "Algoritmo", + "username": "Usuario", + "usernameDesc": "Tu dirección del wallet", + "paymentId": "ID de pago (Exchanges)", + "fixedDiff": "Dificultad fija", + "address": "dirección", + "addrPaymentId": "IDPago", + "addrDiff": "diff", + "password": "Contraseña", + "passwordDesc": "Nombre de su trabajador", + "emailNotifications": "Notificaciones por E-Mail", + "miningPorts": "Puertos de minería", + "port": "Puerto", + "portDiff": "Dificultad inicial", + "description": "Descripción", + "miningApps": "Aplicaciones de minería", + "configGeneratorDesc": "Genera tu configuración personalizada para minar a nuestra Pool", + "addressField": "Dirección Wallet", + "paymentIdField": "ID de Pago para Exchanges (opcional)", + "fixedDiffField": "Dificultad fija (opcional)", + "workerNameField": "Nombre_Trabajador", + "emailNotificationsField": "Notificaciones por E-Mail (opcional)", + "generateConfig": "Generar configuración", + "appName": "Aplicación", + "appArch": "Arquitectura", + "appDesc": "Características", + "download": "Descargar", + "showConfig": "Ver más", + + "market": "Mercado / Calculadora", + "loadingMarket": "Cargando precios del mercado", + "priceIn": "Precio en", + "hashPer": "Hash/", + "estimateProfit": "Beneficios estimados", + "enterYourHashrate": "Entra tu tasa (hash)", + "perDay": "por día", + + "verificationFields": "Campos de verificación", + "minerVerification": "Para tener un poco más de seguridad que la dirección de la cartera es vuestra, le pedimos que proporcione una de las direcciones IP que utiliza su minero.", + "minerAddress": "Dirección del monedero", + "minerIP": "Dirección IP del Minero", + "setMinimumPayout": "Establece el nivel mínimo de pago", + "minerMinPayout": "Si prefiere un nivel de pago más alto que el predeterminado del grupo, aquí puede cambiarlo para sus mineros. El importe que indique aquí se convertirá en el importe mínimo de los pagos de su dirección.", + "minimumPayout": "Pago mínimo", + "enableEmailNotifications": "Activar notificaciones por correo electrónico", + "minerEmailNotify": "Esta Pool enviará una notificación por correo electrónico cuando se encuentre un bloque y cuando se produzca un pago.", + "emailAddress": "Dirección de correo electrónico", + "noMinerAddress": "No se ha especificado ninguna dirección del monedero", + "noMinerIP": "No se especificado ninguna dirección IP", + "noPayoutLevel": "No se ha especificado ningún nivel de pago", + "noEmail": "No se ha especificado ninguna dirección de correo electrónico", + "invalidEmail": "Se ha especificado una dirección de correo electrónico no válida", + "minerPayoutSet": "Hecho! Se ha establecido el nivel mínimo de pago", + "notificationEnabled": "Hecho! Se han activado las notificaciones por correo electrónico", + "notificationDisabled": "Hecho! Se han desactivado las notificaciones por correo electrónico", + + "enterYourAddress": "Introduzca su dirección", + "enterYourMinerIP": "Una dirección IP que utilicen los mineros (cualquiera)", + "enterYourEmail": "Introduzca su dirección de correo electrónico (opcional)", + + "lookup": "Buscar", + "searching": "Buscando...", + "loadMore": "Cargar más", + "set": "Establecer", + "enable": "Activar", + "disable": "Desactivar", + + "status": "Estado", + "updated": "Actualizado:", + "source": "Origen:", + "error": "Error:", + + "na": "N/A", + "estimated": "estimado", + "never": "Nunca", + "second": "segundo", + "seconds": "segundos", + "minute": "minuto", + "minutes": "minutos", + "hour": "hora", + "hours": "horas", + "day": "día", + "days": "dias", + "week": "semana", + "weeks": "semanas", + "month": "mes", + "months": "meses", + "year": "año", + "years": "años", + + "poweredBy": "Powered by", + "openSource": "open sourced bajo " +} diff --git a/website_example/lang/fr.json b/website_example/lang/fr.json new file mode 100644 index 0000000..84ce291 --- /dev/null +++ b/website_example/lang/fr.json @@ -0,0 +1,168 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "Tableau de bord", + "gettingStarted": "Comment démarrer", + "yourStats": "Vos statistiques", + "poolBlocks": "Blocs trouvés", + "settings": "Paramètres", + "faq": "FAQ", + "telegram": "Telegram", + "discord": "Discord", + "contactUs": "Nous contacter", + + "network": "Réseau", + "pool": "Pool", + "you": "Vous", + "statsUpdated": "Statistiques mises à jour", + + "poolHashrate": "Taux de Hash du Pool", + "currentEffort": "Effort actuel", + "networkHashrate": "Taux de Hash du réseau", + "networkDifficulty": "Difficulté", + "blockchainHeight": "Hauteur de la BlockChain", + "networkLastReward": "Dernière récompense", + "poolMiners": "Mineurs connectés", + "poolFee": "Frais du pool", + + "minerStats": "Vos statistiques et Historique des paiements", + "workerStats": "Statistiques des travailleurs", + "miner": "Mineur", + "miners": "Mineurs", + "minersCount": "mineurs", + "workers": "Travailleurs", + "workersCount": "travailleurs", + "workerName": "Nom du travailleur", + "lastHash": "Dernier Hash", + "hashRate": "Taux de Hash", + "currentHashRate": "Taux de Hash actuel", + "lastShare": "Dernière transmission", + "totalHashes": "Hashes transmis", + "top10miners": "Top 10 mineurs", + + "blocksTotal": "Blocs trouvés", + "blockSolvedTime": "Bloc trouvé chaque", + "blocksMaturityCount": "Maturité requise", + "efficiency": "Efficacité", + "averageLuck": "Chance moyenne", + "timeFound": "Trouvé le", + "reward": "Récompense", + "height": "Hauteur", + "difficulty": "Difficulté", + "blockHash": "Hash du bloc", + "effort": "Effort", + "blocksFoundLast24": "Blocs trouvés dans les 24 dernières heures", + "blocksFoundLastDays": "Blocs trouvés dans les derniers {DAYS} jours", + + "payments": "Paiements", + "paymentsHistory": "Historique des paiements", + "paymentsTotal": "Nombre de paiements", + "paymentsMinimum": "Minimum avant paiement", + "paymentsInterval": "Intervale de paiement", + "paymentsDenomination": "Unité de dénomination", + "timeSent": "Envoyé le", + "transactionHash": "Hash de transaction", + "amount": "Montant", + "fee": "Frais", + "mixin": "Mixin", + "payees": "Payés", + "pendingBalance": "Balance en attente", + "totalPaid": "Total payé", + "payoutEstimate": "Estimation de paiement", + "paymentSummarySingle": "Le %DATE% vous avez reçu %AMOUNT%", + "paymentSummaryMulti": "Le %DATE% vous avez reçu %AMOUNT% en %COUNT% paiements", + + "connectionDetails": "Détails de connexion", + "miningPoolHost": "Adresse du pool", + "cnAlgorithm": "Algorithme", + "username": "Nom d'utilisateur", + "usernameDesc": "C'est l'adresse de votre wallet", + "paymentId": "ID de paiement de l'exchange", + "fixedDiff": "Difficulté fixe", + "address": "adresse", + "addrPaymentId": "idPaiement", + "addrDiff": "diff", + "password": "Mot de passe", + "passwordDesc": "C'est l'identifiant de votre travailleur", + "emailNotifications": "Notifications par email", + "miningPorts": "Ports de minage", + "port": "Port", + "portDiff": "Difficulté de départ", + "description": "Description", + "miningApps": "Applications de minage", + "configGeneratorDesc": "Générer votre configuration personalisée pour miner sur notre pool", + "addressField": "Adresse de votre wallet", + "paymentIdField": "ID de paiement pour l'exchange (optionnel)", + "fixedDiffField": "Difficulté fixe (optionnel)", + "workerNameField": "Nom_du_Travailleur", + "emailNotificationsField": "Notifications par Email (optionnel)", + "generateConfig": "Générer la configuration", + "appName": "Nom de l'App", + "appArch": "Architecture", + "appDesc": "Fonctionalités", + "download": "Télécharger", + "showConfig": "Afficher", + + "market": "Marché et calculateur", + "loadingMarket": "Chargement des prix du marché", + "priceIn": "Prix en", + "hashPer": "Hash/", + "estimateProfit": "Estimation des profits de minage", + "enterYourHashrate": "Entrez votre taux de Hash", + "perDay": "par jour", + + "verificationFields": "Champs de vérification", + "minerVerification": "Afin de nous assurer que l'adresse du mineur est bien la vôtre, nous vous demandons d'entrer une adresse IP utilisée par votre mineur.", + "minerAddress": "Adresse du mineur", + "minerIP": "Adresse IP du mineur", + "setMinimumPayout": "Configurer votre niveau de paiement minimum", + "minerMinPayout": "Si vous préférez un montant de paiement minimum plus élevé que celui du pool c'est ici que vous pouvez le changer pour vos mineurs. Le montant que vous indiquerez ici deviendra le montant minimum pour les paiements à votre adresse.", + "minimumPayout": "Paiement minimum", + "enableEmailNotifications": "Activer les notifications par email", + "minerEmailNotify": "Ce pool peut vous envoyer une notification par email lorsqu'un bloc est trouvé ou bien lorsqu'un paiement vous est transmis.", + "emailAddress": "Adresse email", + "noMinerAddress": "Aucune adresse de mineur spécifiée", + "noMinerIP": "Aucune adresse IP pour votre mineur spécifiée", + "noPayoutLevel": "Aucun niveau de paiement spécifié", + "noEmail": "Aucune adresse email spécifiée", + "invalidEmail": "L'adresse email spécifiée est invalide", + "minerPayoutSet": "Fait! Votre niveau de paiement minimum a été configuré", + "notificationEnabled": "Fait! Les notifications par email ont été activées", + "notificationDisabled": "Fait! Les notifications par email ont été désactivées", + + "enterYourAddress": "Entrez votre adresse", + "enterYourMinerIP": "Une adresse IP utilisée par votre mineur (peu importe)", + "enterYourEmail": "Votre adresse email", + + "lookup": "Chercher", + "searching": "Recherche...", + "loadMore": "Charger plus", + "set": "Configurer", + "enable": "Activer", + "disable": "Désactiver", + + "status": "Statut", + "updated": "Mis à jour:", + "source": "Source:", + "error": "Erreur:", + + "na": "N/D", + "estimated": "estimé", + "never": "Jamais", + "second": "seconde", + "seconds": "secondes", + "minute": "minute", + "minutes": "minutes", + "hour": "heure", + "hours": "heures", + "day": "jour", + "days": "jours", + "week": "semaine", + "weeks": "weeks", + "month": "mois", + "months": "mois", + "year": "année", + "years": "années", + + "poweredBy": "Propulsé par", + "openSource": "et libre de droits sous licence" +} \ No newline at end of file diff --git a/website_example/lang/it.json b/website_example/lang/it.json new file mode 100644 index 0000000..cbe084f --- /dev/null +++ b/website_example/lang/it.json @@ -0,0 +1,168 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "Dashboard", + "gettingStarted": "Come Iniziare", + "yourStats": "Statistiche del Worker", + "poolBlocks": "Pool Blocks", + "settings": "Impostazioni", + "faq": "FAQ", + "telegram": "Gruppo Telegram", + "discord": "Discord", + "contactUs": "Contattaci", + + "network": "Rete", + "pool": "Pool", + "you": "Tu", + "statsUpdated": "Stats Aggiornati", + + "poolHashrate": "Pool Hash Rate", + "currentEffort": "Current Effort", + "networkHashrate": "Network Hash Rate", + "networkDifficulty": "Difficoltà", + "blockchainHeight": "Blockchain Height", + "networkLastReward": "Ultimo Reward", + "poolMiners": "Miners Connessi", + "poolFee": "Pool Fee", + + "minerStats": "Stats Personali & Storia dei tuoi pagamenti", + "workerStats": "Statistiche del Worker", + "miner": "Miner", + "miners": "Miners", + "minersCount": "miners", + "workers": "Workers", + "workersCount": "workers", + "workerName": "Worker Name", + "lastHash": "Ultimo Hash", + "hashRate": "Hash Rate", + "currentHashRate": "Attuale Hash Rate", + "lastShare": "Ultimo Share Trasmesso", + "totalHashes": "Totale Hashes Trasmessi", + "top10miners": "Top 10 miners", + + "blocksTotal": "Blocchi trovati", + "blockSolvedTime": "Blocco trovato ogni", + "blocksMaturityCount": "Maturità richiesta", + "efficiency": "Efficenza", + "averageLuck": "Fortuna", + "timeFound": "Orario trovato", + "reward": "Ricompensa", + "height": "Height", + "difficulty": "Difficoltà", + "blockHash": "Block Hash", + "effort": "Effort", + "blocksFoundLast24": "Blocks found in the last 24 hours", + "blocksFoundLastDays": "Blocks found in the last {DAYS} days", + + "payments": "Pagamenti", + "paymentsHistory": "Storia Pagamenti", + "paymentsTotal": "Pagamenti totali", + "paymentsMinimum": "Minimo pagamento", + "paymentsInterval": "intervallo pagamento", + "paymentsDenomination": "Unità di denominazione", + "timeSent": "Orario Inviato", + "transactionHash": "Transazione Hash", + "amount": "Quantità", + "fee": "Fee", + "mixin": "Mixin", + "payees": "Pagati", + "pendingBalance": "In sospeso", + "totalPaid": "Totale pagato", + "payoutEstimate": "Stima del pagamento corrente", + "paymentSummarySingle": "In %DATE% hai ricevuto %AMOUNT%", + "paymentSummaryMulti": "In %DATE% hai ricevuto %AMOUNT% in %COUNT% pagamenti", + + "connectionDetails": "Detagli Connessione", + "miningPoolHost": "Mining Pool Address", + "cnAlgorithm": "Algorithmo", + "username": "Username", + "usernameDesc": "questo è L'indrizzo del wallet", + "paymentId": "Exchange Payment ID", + "fixedDiff": "Difficoltà di blocco", + "address": "indrizzo", + "addrPaymentId": "paymentID", + "addrDiff": "diff", + "password": "Password", + "passwordDesc": "Questo è il nome del Worker", + "emailNotifications": "Notifiche Email", + "miningPorts": "Mining Ports", + "port": "Port", + "portDiff": "Difficoltà avvio", + "description": "Descrizione", + "miningApps": "Applicazioni Mining", + "configGeneratorDesc": "Genera La tua configurazione per minare sulla nostra pool", + "addressField": "Indrizzo wallet", + "paymentIdField": "Pagamento ID per exchanges (optionale)", + "fixedDiffField": "Fixed difficulty (optionale)", + "workerNameField": "Worker_Name", + "emailNotificationsField": "Notifiche email (optionale)", + "generateConfig": "Genera configuratione", + "appName": "App Name", + "appArch": "Architettura", + "appDesc": "Features", + "download": "Download", + "showConfig": "Vedi", + + "market": "Market / Calculatoe", + "loadingMarket": "Loading market prices", + "priceIn": "Price in", + "hashPer": "Hash/", + "estimateProfit": "Stima dei profitti", + "enterYourHashrate": "Inserisci il tuo Hashrate", + "perDay": "al giorno", + + "verificationFields": "Campi di verifica", + "minerVerification": "Per avere un po 'più di fiducia che l'indirizzo del tuo wallet è tuo ti chiediamo di dare uno degli indirizzi IP che viene utilizzato dal tuo miners.", + "minerAddress": "Miner Address", + "minerIP": "Miner IP address", + "setMinimumPayout": "Inserisci il pagamento minimo ", + "minerMinPayout": "Se preferisci un livello di pagamento più alto rispetto al valore predefinito del pool, è qui che puoi cambiarlo per i tuoi miner. L'importo indicato qui diventerà l'importo minimo per i pagamenti del pool al tuo indirizzo.", + "minimumPayout": "Payout Minimo", + "enableEmailNotifications": "Abilitare notifiche email", + "minerEmailNotify": "Questa pool invierà una notifica via email quando viene trovato un blocco e ogni volta che si verifica un pagamento.", + "emailAddress": "Email address", + "noMinerAddress": "Nessun indrizzo wallet indicato", + "noMinerIP": "Nessun indrizzo ip indicato del miner", + "noPayoutLevel": "Nessun livello specificato", + "noEmail": "Nessun indirizzo email specificato", + "invalidEmail": "Indrizzo email invalido", + "minerPayoutSet": "Fatto! Il tuo livello di pagamento minimo è stato impostato", + "notificationEnabled": "Fatto! Le notifiche email sono state abilitate", + "notificationDisabled": "Fatto! Le notifiche email sono state disabilitate", + + "enterYourAddress": "Inserisci il tuo indrizzo", + "enterYourMinerIP": "Un indrizzo ip di qualsiasi tuo miner", + "enterYourEmail": "Inserisci il tuo indrizzo email (optionale)", + + "lookup": "Consulto..", + "searching": "Cerco...", + "loadMore": "Carica Di più", + "set": "imposta", + "enable": "abilita", + "disable": "Disabilita", + + "status": "Stato", + "updated": "Aggiornato:", + "source": "Fonte:", + "error": "Errore:", + + "na": "N/A", + "estimated": "stimato", + "never": "mai", + "second": "secondo", + "seconds": "secondi", + "minute": "minuto", + "minutes": "minuti", + "hour": "ora", + "hours": "ore", + "day": "giorno", + "days": "giorni", + "week": "settimana", + "weeks": "settimane", + "month": "mese", + "months": "mesi", + "year": "anno", + "years": "anni", + + "poweredBy": "Powered by", + "openSource": "open sourced under the" +} diff --git a/website_example/lang/ko.json b/website_example/lang/ko.json new file mode 100644 index 0000000..229d66d --- /dev/null +++ b/website_example/lang/ko.json @@ -0,0 +1,167 @@ +{ + "miningPool": "Mining Pool", + "dashboard": "풀상황", + "gettingStarted": "도움말:시작", + "yourStats": "마이너(워커) 상황", + "poolBlocks": "풀블럭상태", + "settings": "설정", + "telegram": "텔레그램연결", + "discord": "Discord", + "contactUs": "문의하기", + + "network": "네트워크", + "pool": "풀", + "you": "you", + "statsUpdated": "업데이트", + + "poolHashrate": "풀의 해시레이트", + "currentEffort": "Current Effort", + "networkHashrate": "네크워크 해시레이트", + "networkDifficulty": "난이도", + "blockchainHeight": "블록체인 높이", + "networkLastReward": "최근 보상량", + "poolMiners": "연결된 마이너수", + "poolFee": "풀 수수료", + + "minerStats": "마이너 상태와 지급상황", + "workerStats": "워커 상태", + "miner": "Miner", + "miners": "Miners", + "minersCount": "miners", + "workers": "Workers", + "workersCount": "workers", + "workerName": "Worker Name", + "lastHash": "Last Hash", + "hashRate": "Hash Rate", + "currentHashRate": "Current Hash Rate", + "lastShare": "Last Share Submitted", + "totalHashes": "Total Hashes Submitted", + "top10miners": "상위 10 채굴자", + + "blocksTotal": "블록 발견", + "blockSolvedTime": "Blocks Found Every", + "blocksMaturityCount": "적립되기전 블럭수량", + "efficiency": "Efficiency", + "averageLuck": "Average Luck", + "timeFound": "블럭 발견시작", + "reward": "보상", + "height": "블럭번호", + "difficulty": "난이도", + "blockHash": "블럭 해시", + "effort": "풀의노력", + "blocksFoundLast24": "Blocks found in the last 24 hours", + "blocksFoundLastDays": "Blocks found in the last {DAYS} days", + + "payments": "출금상황", + "paymentsHistory": "출금 내역", + "paymentsTotal": "전체 출금 수", + "paymentsMinimum": "최소 출금 수량", + "paymentsInterval": "출금 주기", + "paymentsDenomination": "최소 출금 단위", + "timeSent": "출금 시작", + "transactionHash": "트랜잭션 아이디", + "amount": "수량", + "fee": "수수료", + "mixin": "Mixin", + "payees": "출금대상수", + "pendingBalance": "적립 대기", + "totalPaid": "전체 출금", + "payoutEstimate": "Current Payout Estimate", + "paymentSummarySingle": "On %DATE% you have received %AMOUNT%", + "paymentSummaryMulti": "On %DATE% you have received %AMOUNT% in %COUNT% payments", + + "connectionDetails": "연결정보", + "miningPoolHost": "풀 접속주소", + "cnAlgorithm": "채굴 알고리즘", + "username": "Username", + "usernameDesc": "This is your wallet address", + "paymentId": "거래소 Payment ID", + "fixedDiff": "고정 난이도 설정", + "address": "지갑주소", + "addrPaymentId": "paymentID", + "addrDiff": "난이도", + "password": "암호", + "passwordDesc": "워커 이름 설정", + "emailNotifications": "Email Notifications", + "miningPorts": "채굴 포트", + "port": "접속포트", + "portDiff": "시작 난이도", + "description": "설명", + "miningApps": "마이닝 프로그램", + "configGeneratorDesc": "Generate your custom configuration to mine on our pool", + "addressField": "지갑 주소", + "paymentIdField": "거래소용 Payment ID (optional)", + "fixedDiffField": "고정난이도 (optional)", + "workerNameField": "워커 이름", + "emailNotificationsField": "Email Notifications (optional)", + "generateConfig": "설정 예제 생성", + "appName": "프로그램 이름", + "appArch": "Architecture", + "appDesc": "Features", + "download": "Download", + "showConfig": "See more", + + "market": "시장가격 / 채굴량계산기", + "loadingMarket": "Loading market prices", + "priceIn": "Price in", + "hashPer": "Hash/", + "estimateProfit": "Estimate Mining Profits", + "enterYourHashrate": "Enter Your Hash Rate", + "perDay": "per day", + + "verificationFields": "Verification fields", + "minerVerification": "In order to get a little more confidence that the wallet address is yours we ask you to give one of the IP addresses that is used by your miner.", + "minerAddress": "Miner Address", + "minerIP": "Miner IP address", + "setMinimumPayout": "Set your minimal payout level", + "minerMinPayout": "If you prefer a higher payout level than the pool's default then this is where you can change it for your miners. The amount you indicate here will become the minimum amount for pool payments to your address.", + "minimumPayout": "Minimum payout", + "enableEmailNotifications": "Enable email notifications", + "minerEmailNotify": "This pool will send out email notification when a block is found and whenever a payout happens.", + "emailAddress": "Email address", + "noMinerAddress": "No miner address specified", + "noMinerIP": "No miner IP address specified", + "noPayoutLevel": "No payout level specified", + "noEmail": "No email address specified", + "invalidEmail": "Invalid email address specified", + "minerPayoutSet": "Done! Your minimum payout level was set", + "notificationEnabled": "Done! Email notifications have been enabled", + "notificationDisabled": "Done! Email notifications have been disabled", + + "enterYourAddress": "Enter Your Address", + "enterYourMinerIP": "An IP address your miners use (any)", + "enterYourEmail": "Enter Your E-Mail Address (optional)", + + "lookup": "Lookup", + "searching": "Searching...", + "loadMore": "Load more", + "set": "Set", + "enable": "Enable", + "disable": "Disable", + + "status": "Status", + "updated": "Updated:", + "source": "Source:", + "error": "Error:", + + "na": "N/A", + "estimated": "estimated", + "never": "Never", + "second": "second", + "seconds": "seconds", + "minute": "minute", + "minutes": "minutes", + "hour": "hour", + "hours": "hours", + "day": "day", + "days": "days", + "week": "week", + "weeks": "weeks", + "month": "month", + "months": "months", + "year": "year", + "years": "years", + + "poweredBy": "Powered by", + "openSource": "open sourced under the" +} diff --git a/website_example/lang/languages.js b/website_example/lang/languages.js new file mode 100644 index 0000000..b73a776 --- /dev/null +++ b/website_example/lang/languages.js @@ -0,0 +1 @@ +var langs = { 'en': 'English', 'es': 'Español', 'fr': 'Français', 'it': 'Italiano', 'ru': 'Русский', 'ca': 'Català', 'ko': '한국어', 'zh-CN': '简体中文' }; \ No newline at end of file diff --git a/website_example/lang/ru.json b/website_example/lang/ru.json new file mode 100644 index 0000000..4536f8e --- /dev/null +++ b/website_example/lang/ru.json @@ -0,0 +1,182 @@ +{ + "miningPool": "Майнинг пул", + "dashboard": "Главная", + "gettingStarted": "Присоединиться", + "yourStats": "Статистика", + "poolBlocks": "Блоки пула", + "settings": "Настройки", + "faq": "FAQ", + "telegram": "Группа Telegram", + "discord": "Discord", + "contactUs": "Почта", + + "network": "Сеть", + "pool": "Пул", + "you": "Вы", + "statsUpdated": "Статистика обновлена", + + "poolHashrate": "Скорость пула", + "currentEffort": "Сложность раунда", + "networkHashrate": "Скорость сети", + "networkDifficulty": "Сложность", + "blockchainHeight": "№ последнего блока", + "networkLastReward": "Последнее вознаграждение", + "poolMiners": "Пользователи пула", + "poolFee": "Комиссия пула", + + "minerStats": "Ваша статистика и история платежей", + "workerStats": "Статистика ферм", + "miner": "Miner", + "miners": "Пользователи", + "minersCount": "miners", + "workers": "Фермы", + "workersCount": "фермы", + "workerName": "Имя фермы", + "lastHash": "Последний хеш", + "hashRate": "Скорость", + "currentHashRate": "Текущая скорость", + "lastShare": "Последняя шара принята", + "totalHashes": "Всего принято хешей", + "top10miners": "Лучшие 10 майнеров", + "blocksTotal": "Найдено блоков", + "blockSolvedTime": "Время нахождения блока", + "blocksMaturityCount": "Требование подтверждения", + "efficiency": "Эффективность", + "averageLuck": "Средняя удача", + "timeFound": "Время нахождения", + "reward": "Выплата", + "height": "№ блока", + "difficulty": "Сложность", + "blockHash": "Хеш блока", + "effort": "Усилие", + "blocksFoundLast24": "Блоки найдены за последние 24 часа", + "blocksFoundLastDays": "Блоки найдены за последние {DAYS} дней", + "propSoloConnectedMiners": "PROP / SOLO Подключенные шахтеры", + + "payments": "Платежи", + "paymentsHistory": "История платежей", + "paymentsTotal": "Всего платежей", + "paymentsMinimum": "Минимальный платёж", + "paymentsInterval": "Интервал платежей", + "paymentsDenomination": "Единица измерения", + "timeSent": "Время отправления", + "transactionHash": "Хеш транзакции", + "amount": "Сумма", + "fee": "Комиссия", + "mixin": "Mixin", + "payees": "Получателей", + "pendingBalance": "Ожидающий баланс", + "totalPaid": "Всего выплачено", + "payoutEstimate": "Текущая оценка выплат", + + "connectionDetails": "Детали подключения", + "miningPoolHost": "Адрес майнинг пула", + "cnAlgorithm": "Алгоритм", + "username": "Имя пользователя", + "usernameDesc": "Это адрес вашего кошелька", + "paymentId": "Биржевой Payment ID", + "fixedDiff": "Фиксированная сложность", + "address": "address", + "addrPaymentId": "paymentID", + "addrDiff": "diff", + "password": "Пароль", + "passwordDesc": "Это имя вашей фермы в статистике", + "emailNotifications": "Уведомление по почте", + "miningPorts": "Порты для майнинга", + "port": "Порт", + "portDiff": "Стартовая сложность", + "description": "Описание", + "miningApps": "Программы для майнинга", + "configGeneratorDesc": "Создайте свою собственную конфигурацию для этого пула", + "addressField": "Адрес вашего кошелька", + "paymentIdField": "Payment ID для биржи (опция)", + "fixedDiffField": "Фиксированная сложность (опция)", + "workerNameField": "Имя_Фермы", + "emailNotificationsField": "Уведомление по почте (опция)", + "generateConfig": "Создать конфигурацию", + "appName": "Программа", + "appArch": "Архитектура", + "appDesc": "Особенности", + "download": "Скачать", + "showConfig": "Посмотреть", + + "market": "Рынок / Калькулятор", + "loadingMarket": "Загрузка стоимости", + "priceIn": "Стоимость в", + "hashPer": "Стоимость хеша/", + "estimateProfit": "Рассчёт прибыли", + "enterYourHashrate": "Введите вашу скорость", + "perDay": "/в день", + + "verificationFields": "Проверочные данные", + "minerVerification": "Чтобы быть уверенным в том, что адрес кошелька принадлежит вам, мы просим вас указать один из IP-адресов, который используется вашими фермами.", + "minerAddress": "Ваш кошелёк", + "minerIP": "IP адрес фермы", + "setMinimumPayout": "Установите минимальный уровень выплат", + "minerMinPayout": "Вы можете установить минимальный порог оплаты, если предпочитаете более высокий уровень выплат, чем значение по умолчанию. Сумма, которую вы здесь укажете, станет минимальной суммой для платежей пула на ваш адрес.", + "minimumPayout": "Минимальная выплата", + "enableEmailNotifications": "Включить уведомление по почте", + "minerEmailNotify": "Пул отправит уведомление по электронной почте, когда будет найден блок или когда произведёт выплату.", + "emailAddress": "E-mail адрес", + "noMinerAddress": "Вы не ввели свой кошелёк", + "noMinerIP": "Вы не ввели IP адрес фермы", + "noPayoutLevel": "Вы не ввели уровень оплаты", + "noEmail": "Вы не ввели e-mail адрес", + "invalidEmail": "Не правильный e-mail адрес", + "minerPayoutSet": "Минимальный уровень оплаты успешно установлен !", + "notificationEnabled": "Уведомления по электронной почте успешно включены !", + "notificationDisabled": "Уведомления по электронной почте успешно выключены !", + + "enterYourAddress": "Введите адрес своего кошелька", + "enterYourMinerIP": "IP-адрес, который используют ваши фермы", + "enterYourEmail": "Введите свой E-Mail адрес (опция)", + + "lookup": "Посмотреть", + "searching": "Поиск ...", + "loadMore": "Загрузить ещё", + "set": "Установить", + "enable": "Включить", + "disable": "Выключить", + + "status": "Статус", + "updated": "Обновлено:", + "source": "Source:", + "error": "Ошибка:", + + "na": "N/A", + "estimated": "примерно", + "never": "Не найден", + "second": "секунда", + "seconds": "секунд", + "minute": "минута", + "minutes": "минут", + "hour": "час", + "hours": "часов", + "day": "день", + "days": "дней", + "week": "неделя", + "weeks": "недель", + "month": "месяц", + "months": "месяцев", + "year": "год", + "years": "года", + + "timeagoPrefixAgo": null, + "timeagoPrefixFromNow": null, + "timeagoSuffixAgo": "назад", + "timeagoSuffixFromNow": "from now", + "timeagoSeconds": "меньше минуты", + "timeagoMinute": "около минуты", + "timeagoMinutes": "%d минут", + "timeagoHour": "about an hour", + "timeagoHours": "about %d hours", + "timeagoDay": " день", + "timeagoDays": "%d дней", + "timeagoMonth": "about месяц", + "timeagoMonths": "%d месяцев", + "timeagoYear": "about год", + "timeagoYears": "%d лет", + + "poweredBy": "Powered by", + "openSource": "open sourced under the" +} \ No newline at end of file diff --git a/website_example/lang/timeago/jquery.timeago.af.js b/website_example/lang/timeago/jquery.timeago.af.js new file mode 100644 index 0000000..817a7fa --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.af.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Afrikaans + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "gelede", + suffixFromNow: "van nou af", + seconds: "%d sekondes", + minute: "1 minuut", + minutes: "%d minute", + hour: "1 uur", + hours: "%d ure", + day: "1 dag", + days: "%d dae", + month: "1 maand", + months: "%d maande", + year: "1 jaar", + years: "%d jaar", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.am.js b/website_example/lang/timeago/jquery.timeago.am.js new file mode 100644 index 0000000..65502c3 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.am.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Amharic + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "በፊት", + suffixFromNow: "በኋላ", + seconds: "ከአንድ ደቂቃ በታች", + minute: "ከአንድ ደቂቃ ገደማ", + minutes: "ከ%d ደቂቃ", + hour: "ከአንድ ሰዓት ገደማ", + hours: "ከ%d ሰዓት ገደማ", + day: "ከአንድ ቀን", + days: "ከ%d ቀን", + month: "ከአንድ ወር ገደማ", + months: "ከ%d ወር", + year: "ከአንድ ዓመት ገደማ", + years: "ከ%d ዓመት", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ar.js b/website_example/lang/timeago/jquery.timeago.ar.js new file mode 100644 index 0000000..14cd18f --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ar.js @@ -0,0 +1,104 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + function numpf(n, a) { + return a[plural=n===0 ? 0 : n===1 ? 1 : n===2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5]; + } + + jQuery.timeago.settings.strings = { + prefixAgo: "منذ", + prefixFromNow: "بعد", + suffixAgo: null, + suffixFromNow: null, // null OR "من الآن" + second: function(value) { return numpf(value, [ + 'أقل من ثانية', + 'ثانية واحدة', + 'ثانيتين', + '%d ثوانٍ', + '%d ثانية', + '%d ثانية']); }, + seconds: function(value) { return numpf(value, [ + 'أقل من ثانية', + 'ثانية واحدة', + 'ثانيتين', + '%d ثوانٍ', + '%d ثانية', + '%d ثانية']); }, + minute: function(value) { return numpf(value, [ + 'أقل من دقيقة', + 'دقيقة واحدة', + 'دقيقتين', + '%d دقائق', + '%d دقيقة', + 'دقيقة']); }, + minutes: function(value) { return numpf(value, [ + 'أقل من دقيقة', + 'دقيقة واحدة', + 'دقيقتين', + '%d دقائق', + '%d دقيقة', + 'دقيقة']); }, + hour: function(value) { return numpf(value, [ + 'أقل من ساعة', + 'ساعة واحدة', + 'ساعتين', + '%d ساعات', + '%d ساعة', + '%d ساعة']); }, + hours: function(value) { return numpf(value, [ + 'أقل من ساعة', + 'ساعة واحدة', + 'ساعتين', + '%d ساعات', + '%d ساعة', + '%d ساعة']); }, + day: function(value) { return numpf(value, [ + 'أقل من يوم', + 'يوم واحد', + 'يومين', + '%d أيام', + '%d يومًا', + '%d يوم']); }, + days: function(value) { return numpf(value, [ + 'أقل من يوم', + 'يوم واحد', + 'يومين', + '%d أيام', + '%d يومًا', + '%d يوم']); }, + month: function(value) { return numpf(value, [ + 'أقل من شهر', + 'شهر واحد', + 'شهرين', + '%d أشهر', + '%d شهرًا', + '%d شهر']); }, + months: function(value) { return numpf(value, [ + 'أقل من شهر', + 'شهر واحد', + 'شهرين', + '%d أشهر', + '%d شهرًا', + '%d شهر']); }, + year: function(value) { return numpf(value, [ + 'أقل من عام', + 'عام واحد', + '%d عامين', + '%d أعوام', + '%d عامًا']); + }, + years: function(value) { return numpf(value, [ + 'أقل من عام', + 'عام واحد', + 'عامين', + '%d أعوام', + '%d عامًا', + '%d عام']);} + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.az.js b/website_example/lang/timeago/jquery.timeago.az.js new file mode 100644 index 0000000..8332c41 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.az.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Azerbaijani + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'əvvəl', + suffixFromNow: 'sonra', + seconds: 'saniyələr', + minute: '1 dəqiqə', + minutes: '%d dəqiqə', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 il', + years: '%d il', + wordSeparator: '', + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.bg.js b/website_example/lang/timeago/jquery.timeago.bg.js new file mode 100644 index 0000000..a3bd343 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.bg.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Bulgarian + jQuery.timeago.settings.strings = { + prefixAgo: "преди", + prefixFromNow: "след", + suffixAgo: null, + suffixFromNow: null, + seconds: "по-малко от минута", + minute: "една минута", + minutes: "%d минути", + hour: "един час", + hours: "%d часа", + day: "един ден", + days: "%d дни", + month: "един месец", + months: "%d месеца", + year: "една година", + years: "%d години" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.bs.js b/website_example/lang/timeago/jquery.timeago.bs.js new file mode 100644 index 0000000..cbb1780 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.bs.js @@ -0,0 +1,55 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Bosnian + var numpf = function(n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "prije", + prefixFromNow: "za", + suffixAgo: null, + suffixFromNow: null, + second: "sekund", + seconds: function(value) { + return numpf(value, "%d sekund", "%d sekunde", "%d sekundi"); + }, + minute: "oko minut", + minutes: function(value) { + return numpf(value, "%d minut", "%d minute", "%d minuta"); + }, + hour: "oko sat", + hours: function(value) { + return numpf(value, "%d sat", "%d sata", "%d sati"); + }, + day: "oko jednog dana", + days: function(value) { + return numpf(value, "%d dan", "%d dana", "%d dana"); + }, + month: "mjesec dana", + months: function(value) { + return numpf(value, "%d mjesec", "%d mjeseca", "%d mjeseci"); + }, + year: "prije godinu dana ", + years: function(value) { + return numpf(value, "%d godinu", "%d godine", "%d godina"); + }, + wordSeparator: " " + }; + +})); diff --git a/website_example/lang/timeago/jquery.timeago.ca.js b/website_example/lang/timeago/jquery.timeago.ca.js new file mode 100644 index 0000000..e4cb5ca --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ca.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Catalan + jQuery.timeago.settings.strings = { + prefixAgo: "fa", + prefixFromNow: "d'aquí", + suffixAgo: null, + suffixFromNow: null, + seconds: "menys d'un minut", + minute: "un minut", + minutes: "%d minuts", + hour: "una hora", + hours: "%d hores", + day: "un dia", + days: "%d dies", + month: "un mes", + months: "%d mesos", + year: "un any", + years: "%d anys", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.cs.js b/website_example/lang/timeago/jquery.timeago.cs.js new file mode 100644 index 0000000..b940f69 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.cs.js @@ -0,0 +1,34 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Czech + (function() { + function f(n, d, a) { + return a[d>=0 ? 0 : a.length===2 || n<5 ? 1 : 2]; + } + + jQuery.timeago.settings.strings = { + prefixAgo: 'před', + prefixFromNow: 'za', + suffixAgo: null, + suffixFromNow: null, + seconds: function(n, d) {return f(n, d, ['méně než minutou', 'méně než minutu']);}, + minute: function(n, d) {return f(n, d, ['minutou', 'minutu']);}, + minutes: function(n, d) {return f(n, d, ['%d minutami', '%d minuty', '%d minut']);}, + hour: function(n, d) {return f(n, d, ['hodinou', 'hodinu']);}, + hours: function(n, d) {return f(n, d, ['%d hodinami', '%d hodiny', '%d hodin']);}, + day: function(n, d) {return f(n, d, ['%d dnem', '%d den']);}, + days: function(n, d) {return f(n, d, ['%d dny', '%d dny', '%d dní']);}, + month: function(n, d) {return f(n, d, ['%d měsícem', '%d měsíc']);}, + months: function(n, d) {return f(n, d, ['%d měsíci', '%d měsíce', '%d měsíců']);}, + year: function(n, d) {return f(n, d, ['%d rokem', '%d rok']);}, + years: function(n, d) {return f(n, d, ['%d lety', '%d roky', '%d let']);} + }; + })(); +})); diff --git a/website_example/lang/timeago/jquery.timeago.cy.js b/website_example/lang/timeago/jquery.timeago.cy.js new file mode 100644 index 0000000..4c514a8 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.cy.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Welsh + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "yn ôl", + suffixFromNow: "o hyn", + seconds: "llai na munud", + minute: "am funud", + minutes: "%d munud", + hour: "tua awr", + hours: "am %d awr", + day: "y dydd", + days: "%d diwrnod", + month: "tua mis", + months: "%d mis", + year: "am y flwyddyn", + years: "%d blynedd", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.da.js b/website_example/lang/timeago/jquery.timeago.da.js new file mode 100644 index 0000000..236c34c --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.da.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Danish + jQuery.timeago.settings.strings = { + prefixAgo: "for", + prefixFromNow: "om", + suffixAgo: "siden", + suffixFromNow: "", + seconds: "mindre end et minut", + minute: "ca. et minut", + minutes: "%d minutter", + hour: "ca. en time", + hours: "ca. %d timer", + day: "en dag", + days: "%d dage", + month: "ca. en måned", + months: "%d måneder", + year: "ca. et år", + years: "%d år" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.de.js b/website_example/lang/timeago/jquery.timeago.de.js new file mode 100644 index 0000000..6a877a2 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.de.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // German + jQuery.timeago.settings.strings = { + prefixAgo: "vor", + prefixFromNow: "in", + suffixAgo: "", + suffixFromNow: "", + seconds: "wenigen Sekunden", + minute: "etwa einer Minute", + minutes: "%d Minuten", + hour: "etwa einer Stunde", + hours: "%d Stunden", + day: "etwa einem Tag", + days: "%d Tagen", + month: "etwa einem Monat", + months: "%d Monaten", + year: "etwa einem Jahr", + years: "%d Jahren" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.dv.js b/website_example/lang/timeago/jquery.timeago.dv.js new file mode 100644 index 0000000..0d70a49 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.dv.js @@ -0,0 +1,32 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + /** + * Dhivehi time in Thaana for timeago.js + **/ + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ކުރިން", + suffixFromNow: "ފަހުން", + seconds: "ސިކުންތުކޮޅެއް", + minute: "މިނިޓެއްވަރު", + minutes: "%d މިނިޓު", + hour: "ގަޑިއެއްވަރު", + hours: "ގާތްގަނޑަކަށް %d ގަޑިއިރު", + day: "އެއް ދުވަސް", + days: "މީގެ %d ދުވަސް", + month: "މަހެއްވަރު", + months: "މީގެ %d މަސް", + year: "އަހަރެއްވަރު", + years: "މީގެ %d އަހަރު", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.el.js b/website_example/lang/timeago/jquery.timeago.el.js new file mode 100644 index 0000000..2db9ebe --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.el.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Greek + jQuery.timeago.settings.strings = { + prefixAgo: "πριν", + prefixFromNow: "σε", + suffixAgo: "", + suffixFromNow: "", + seconds: "λιγότερο από ένα λεπτό", + minute: "περίπου ένα λεπτό", + minutes: "%d λεπτά", + hour: "περίπου μία ώρα", + hours: "περίπου %d ώρες", + day: "μία μέρα", + days: "%d μέρες", + month: "περίπου ένα μήνα", + months: "%d μήνες", + year: "περίπου ένα χρόνο", + years: "%d χρόνια" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.en.js b/website_example/lang/timeago/jquery.timeago.en.js new file mode 100644 index 0000000..8ca50af --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.en.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // English (Template) + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.es.js b/website_example/lang/timeago/jquery.timeago.es.js new file mode 100644 index 0000000..0785b3f --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.es.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Spanish + jQuery.timeago.settings.strings = { + prefixAgo: "hace", + prefixFromNow: "dentro de", + suffixAgo: "", + suffixFromNow: "", + seconds: "menos de un minuto", + minute: "un minuto", + minutes: "unos %d minutos", + hour: "una hora", + hours: "%d horas", + day: "un día", + days: "%d días", + month: "un mes", + months: "%d meses", + year: "un año", + years: "%d años" + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.et.js b/website_example/lang/timeago/jquery.timeago.et.js new file mode 100644 index 0000000..ac2461e --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.et.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Estonian + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "tagasi", + suffixFromNow: "pärast", + seconds: function(n, d) { return d < 0 ? "vähem kui minuti aja" : "vähem kui minut aega"; }, + minute: function(n, d) { return d < 0 ? "umbes minuti aja" : "umbes minut aega"; }, + minutes: function(n, d) { return d < 0 ? "%d minuti" : "%d minutit"; }, + hour: function(n, d) { return d < 0 ? "umbes tunni aja" : "umbes tund aega"; }, + hours: function(n, d) { return d < 0 ? "%d tunni" : "%d tundi"; }, + day: function(n, d) { return d < 0 ? "umbes päeva" : "umbes päev"; }, + days: "%d päeva", + month: function(n, d) { return d < 0 ? "umbes kuu aja" : "umbes kuu aega"; }, + months: function(n, d) { return d < 0 ? "%d kuu" : "%d kuud"; }, + year: function(n, d) { return d < 0 ? "umbes aasta aja" : "umbes aasta aega"; }, + years: function(n, d) { return d < 0 ? "%d aasta" : "%d aastat"; } + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.eu.js b/website_example/lang/timeago/jquery.timeago.eu.js new file mode 100644 index 0000000..5c2c32c --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.eu.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + jQuery.timeago.settings.strings = { + prefixAgo: "duela", + prefixFromNow: "hemendik", + suffixAgo: "", + suffixFromNow: "barru", + seconds: "minutu bat bainu gutxiago", + minute: "minutu bat", + minutes: "%d minutu inguru", + hour: "ordu bat", + hours: "%d ordu", + day: "egun bat", + days: "%d egun", + month: "hilabete bat", + months: "%d hilabete", + year: "urte bat", + years: "%d urte" + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.fa.js b/website_example/lang/timeago/jquery.timeago.fa.js new file mode 100644 index 0000000..ec8ccb9 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.fa.js @@ -0,0 +1,32 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Persian + // Use DIR attribute for RTL text in Persian Language for ABBR tag . + // By MB.seifollahi@gmail.com + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "پیش", + suffixFromNow: "از حال", + seconds: "کمتر از یک دقیقه", + minute: "حدود یک دقیقه", + minutes: "%d دقیقه", + hour: "حدود یک ساعت", + hours: "حدود %d ساعت", + day: "یک روز", + days: "%d روز", + month: "حدود یک ماه", + months: "%d ماه", + year: "حدود یک سال", + years: "%d سال", + wordSeparator: " ", + numbers: ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.fi.js b/website_example/lang/timeago/jquery.timeago.fi.js new file mode 100644 index 0000000..b5f7e69 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.fi.js @@ -0,0 +1,38 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Finnish + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "sitten", + suffixFromNow: "tulevaisuudessa", + seconds: "alle minuutti", + minute: "minuutti", + minutes: "%d minuuttia", + hour: "tunti", + hours: "%d tuntia", + day: "päivä", + days: "%d päivää", + month: "kuukausi", + months: "%d kuukautta", + year: "vuosi", + years: "%d vuotta" + }; + + // The above is not a great localization because one would usually + // write "2 days ago" in Finnish as "2 päivää sitten", however + // one would write "2 days into the future" as "2:n päivän päästä" + // which cannot be achieved with localization support this simple. + // This is because Finnish has word suffixes (attached directly + // to the end of the word). The word "day" is "päivä" in Finnish. + // As workaround, the above localizations will say + // "2 päivää tulevaisuudessa" which is understandable but + // not as fluent. +})); diff --git a/website_example/lang/timeago/jquery.timeago.fr.js b/website_example/lang/timeago/jquery.timeago.fr.js new file mode 100644 index 0000000..1bb052a --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.fr.js @@ -0,0 +1,27 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // French + jQuery.timeago.settings.strings = { + // environ ~= about, it's optional + prefixAgo: "il y a", + prefixFromNow: "d'ici", + seconds: "moins d'une minute", + minute: "environ une minute", + minutes: "environ %d minutes", + hour: "environ une heure", + hours: "environ %d heures", + day: "environ un jour", + days: "environ %d jours", + month: "environ un mois", + months: "environ %d mois", + year: "un an", + years: "%d ans" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.gl.js b/website_example/lang/timeago/jquery.timeago.gl.js new file mode 100644 index 0000000..277bbf7 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.gl.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Galician + jQuery.timeago.settings.strings = { + prefixAgo: "hai", + prefixFromNow: "dentro de", + suffixAgo: "", + suffixFromNow: "", + seconds: "menos dun minuto", + minute: "un minuto", + minutes: "uns %d minutos", + hour: "unha hora", + hours: "%d horas", + day: "un día", + days: "%d días", + month: "un mes", + months: "%d meses", + year: "un ano", + years: "%d anos" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.he.js b/website_example/lang/timeago/jquery.timeago.he.js new file mode 100644 index 0000000..2cd31ab --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.he.js @@ -0,0 +1,26 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Hebrew + jQuery.timeago.settings.strings = { + prefixAgo: "לפני", + prefixFromNow: "עוד", + seconds: "פחות מדקה", + minute: "דקה", + minutes: "%d דקות", + hour: "שעה", + hours: function(number){return (number===2) ? "שעתיים" : "%d שעות";}, + day: "יום", + days: function(number){return (number===2) ? "יומיים" : "%d ימים";}, + month: "חודש", + months: function(number){return (number===2) ? "חודשיים" : "%d חודשים";}, + year: "שנה", + years: function(number){return (number===2) ? "שנתיים" : "%d שנים";} + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.hr.js b/website_example/lang/timeago/jquery.timeago.hr.js new file mode 100644 index 0000000..bd14297 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.hr.js @@ -0,0 +1,54 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Croatian + var numpf = function (n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "prije", + prefixFromNow: "za", + suffixAgo: null, + suffixFromNow: null, + second: "sekundu", + seconds: function (value) { + return numpf(value, "%d sekundu", "%d sekunde", "%d sekundi"); + }, + minute: "oko minutu", + minutes: function (value) { + return numpf(value, "%d minutu", "%d minute", "%d minuta"); + }, + hour: "oko jedan sat", + hours: function (value) { + return numpf(value, "%d sat", "%d sata", "%d sati"); + }, + day: "jedan dan", + days: function (value) { + return numpf(value, "%d dan", "%d dana", "%d dana"); + }, + month: "mjesec dana", + months: function (value) { + return numpf(value, "%d mjesec", "%d mjeseca", "%d mjeseci"); + }, + year: "prije godinu dana", + years: function (value) { + return numpf(value, "%d godinu", "%d godine", "%d godina"); + }, + wordSeparator: " " + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.hu.js b/website_example/lang/timeago/jquery.timeago.hu.js new file mode 100644 index 0000000..0009de9 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.hu.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Hungarian + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: null, + seconds: "kevesebb mint egy perce", + minute: "körülbelül egy perce", + minutes: "%d perce", + hour: "körülbelül egy órája", + hours: "körülbelül %d órája", + day: "körülbelül egy napja", + days: "%d napja", + month: "körülbelül egy hónapja", + months: "%d hónapja", + year: "körülbelül egy éve", + years: "%d éve" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.hy.js b/website_example/lang/timeago/jquery.timeago.hy.js new file mode 100644 index 0000000..3f0de6e --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.hy.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Armenian + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "առաջ", + suffixFromNow: "հետո", + seconds: "վայրկյաններ", + minute: "մեկ րոպե", + minutes: "%d րոպե", + hour: "մեկ ժամ", + hours: "%d ժամ", + day: "մեկ օր", + days: "%d օր", + month: "մեկ ամիս", + months: "%d ամիս", + year: "մեկ տարի", + years: "%d տարի" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.id.js b/website_example/lang/timeago/jquery.timeago.id.js new file mode 100644 index 0000000..ca530cc --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.id.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Indonesian + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "yang lalu", + suffixFromNow: "dari sekarang", + seconds: "kurang dari semenit", + minute: "sekitar satu menit", + minutes: "%d menit", + hour: "sekitar sejam", + hours: "sekitar %d jam", + day: "sehari", + days: "%d hari", + month: "sekitar sebulan", + months: "%d bulan", + year: "sekitar setahun", + years: "%d tahun" + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.is.js b/website_example/lang/timeago/jquery.timeago.is.js new file mode 100644 index 0000000..e3d4b1f --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.is.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + jQuery.timeago.settings.strings = { + prefixAgo: "fyrir", + prefixFromNow: "eftir", + suffixAgo: "síðan", + suffixFromNow: null, + seconds: "minna en mínútu", + minute: "mínútu", + minutes: "%d mínútum", + hour: "klukkutíma", + hours: "um %d klukkutímum", + day: "degi", + days: "%d dögum", + month: "mánuði", + months: "%d mánuðum", + year: "ári", + years: "%d árum", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.it.js b/website_example/lang/timeago/jquery.timeago.it.js new file mode 100644 index 0000000..e1cac84 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.it.js @@ -0,0 +1,26 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Italian + jQuery.timeago.settings.strings = { + suffixAgo: "fa", + suffixFromNow: "da ora", + seconds: "meno di un minuto", + minute: "circa un minuto", + minutes: "%d minuti", + hour: "circa un'ora", + hours: "circa %d ore", + day: "un giorno", + days: "%d giorni", + month: "circa un mese", + months: "%d mesi", + year: "circa un anno", + years: "%d anni" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ja.js b/website_example/lang/timeago/jquery.timeago.ja.js new file mode 100644 index 0000000..30f3308 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ja.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Japanese + jQuery.timeago.settings.strings = { + prefixAgo: "", + prefixFromNow: "今から", + suffixAgo: "前", + suffixFromNow: "後", + seconds: "1 分未満", + minute: "約 1 分", + minutes: "%d 分", + hour: "約 1 時間", + hours: "約 %d 時間", + day: "約 1 日", + days: "約 %d 日", + month: "約 1 ヶ月", + months: "約 %d ヶ月", + year: "約 1 年", + years: "約 %d 年", + wordSeparator: "" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.jv.js b/website_example/lang/timeago/jquery.timeago.jv.js new file mode 100644 index 0000000..0344053 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.jv.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Javanesse (Boso Jowo) + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "kepungkur", + suffixFromNow: "seko saiki", + seconds: "kurang seko sakmenit", + minute: "kurang luwih sakmenit", + minutes: "%d menit", + hour: "kurang luwih sakjam", + hours: "kurang luwih %d jam", + day: "sedina", + days: "%d dina", + month: "kurang luwih sewulan", + months: "%d wulan", + year: "kurang luwih setahun", + years: "%d tahun" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ko.js b/website_example/lang/timeago/jquery.timeago.ko.js new file mode 100644 index 0000000..23d1d37 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ko.js @@ -0,0 +1,31 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Korean + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "전", + suffixFromNow: "후", + seconds: "1분", + minute: "약 1분", + minutes: "%d분", + hour: "약 1시간", + hours: "약 %d시간", + day: "하루", + days: "%d일", + month: "약 1개월", + months: "%d개월", + year: "약 1년", + years: "%d년", + wordSeparator: " ", + numbers: [] + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.ky.js b/website_example/lang/timeago/jquery.timeago.ky.js new file mode 100644 index 0000000..58dba29 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ky.js @@ -0,0 +1,42 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Russian + function numpf(n, f, s, t) { + // f - 1, 21, 31, ... + // s - 2-4, 22-24, 32-34 ... + // t - 5-20, 25-30, ... + var n10 = n % 10; + if ( (n10 === 1) && ( (n === 1) || (n > 20) ) ) { + return f; + } else if ( (n10 > 1) && (n10 < 5) && ( (n > 20) || (n < 10) ) ) { + return s; + } else { + return t; + } + } + + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "через", + suffixAgo: "мурун", + suffixFromNow: null, + seconds: "1 минуттан аз", + minute: "минута", + minutes: function(value) { return numpf(value, "%d минута", "%d минута", "%d минут"); }, + hour: "саат", + hours: function(value) { return numpf(value, "%d саат", "%d саат", "%d саат"); }, + day: "күн", + days: function(value) { return numpf(value, "%d күн", "%d күн", "%d күн"); }, + month: "ай", + months: function(value) { return numpf(value, "%d ай", "%d ай", "%d ай"); }, + year: "жыл", + years: function(value) { return numpf(value, "%d жыл", "%d жыл", "%d жыл"); } + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.lt.js b/website_example/lang/timeago/jquery.timeago.lt.js new file mode 100644 index 0000000..2079fcc --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.lt.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + //Lithuanian + jQuery.timeago.settings.strings = { + prefixAgo: "prieš", + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: "nuo dabar", + seconds: "%d sek.", + minute: "min.", + minutes: "%d min.", + hour: "val.", + hours: "%d val.", + day: "1 d.", + days: "%d d.", + month: "mėn.", + months: "%d mėn.", + year: "metus", + years: "%d metus", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.lv.js b/website_example/lang/timeago/jquery.timeago.lv.js new file mode 100644 index 0000000..855d1a4 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.lv.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + //Latvian + jQuery.timeago.settings.strings = { + prefixAgo: "pirms", + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: "no šī brīža", + seconds: "%d sek.", + minute: "min.", + minutes: "%d min.", + hour: "st.", + hours: "%d st.", + day: "1 d.", + days: "%d d.", + month: "mēnesis.", + months: "%d mēnesis.", + year: "gads", + years: "%d gads", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.mk.js b/website_example/lang/timeago/jquery.timeago.mk.js new file mode 100644 index 0000000..301a5da --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.mk.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Macedonian + (function() { + jQuery.timeago.settings.strings={ + prefixAgo: "пред", + prefixFromNow: "за", + suffixAgo: null, + suffixFromNow: null, + seconds: "%d секунди", + minute: "%d минута", + minutes: "%d минути", + hour: "%d час", + hours: "%d часа", + day: "%d ден", + days: "%d денови" , + month: "%d месец", + months: "%d месеци", + year: "%d година", + years: "%d години" + }; + })(); +})); diff --git a/website_example/lang/timeago/jquery.timeago.nl.js b/website_example/lang/timeago/jquery.timeago.nl.js new file mode 100644 index 0000000..2c5de89 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.nl.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Dutch + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "over", + suffixAgo: "geleden", + suffixFromNow: null, + seconds: "minder dan een minuut", + minute: "ongeveer een minuut", + minutes: "%d minuten", + hour: "ongeveer een uur", + hours: "ongeveer %d uur", + day: "een dag", + days: "%d dagen", + month: "ongeveer een maand", + months: "%d maanden", + year: "ongeveer een jaar", + years: "%d jaar", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.no.js b/website_example/lang/timeago/jquery.timeago.no.js new file mode 100644 index 0000000..c896337 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.no.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Norwegian + jQuery.timeago.settings.strings = { + prefixAgo: "for", + prefixFromNow: "om", + suffixAgo: "siden", + suffixFromNow: "", + seconds: "mindre enn et minutt", + minute: "ca. et minutt", + minutes: "%d minutter", + hour: "ca. en time", + hours: "ca. %d timer", + day: "en dag", + days: "%d dager", + month: "ca. en måned", + months: "%d måneder", + year: "ca. et år", + years: "%d år" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.pl.js b/website_example/lang/timeago/jquery.timeago.pl.js new file mode 100644 index 0000000..4842784 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.pl.js @@ -0,0 +1,39 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Polish + function numpf(n, s, t) { + // s - 2-4, 22-24, 32-34 ... + // t - 5-21, 25-31, ... + var n10 = n % 10; + if ( (n10 > 1) && (n10 < 5) && ( (n > 20) || (n < 10) ) ) { + return s; + } else { + return t; + } + } + + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "za", + suffixAgo: "temu", + suffixFromNow: null, + seconds: "mniej niż minutę", + minute: "minutę", + minutes: function(value) { return numpf(value, "%d minuty", "%d minut"); }, + hour: "godzinę", + hours: function(value) { return numpf(value, "%d godziny", "%d godzin"); }, + day: "dzień", + days: "%d dni", + month: "miesiąc", + months: function(value) { return numpf(value, "%d miesiące", "%d miesięcy"); }, + year: "rok", + years: function(value) { return numpf(value, "%d lata", "%d lat"); } + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.pt-br.js b/website_example/lang/timeago/jquery.timeago.pt-br.js new file mode 100644 index 0000000..a8701a8 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.pt-br.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Brazilian Portuguese + jQuery.timeago.settings.strings = { + prefixAgo: "há", + prefixFromNow: "em", + suffixAgo: null, + suffixFromNow: null, + seconds: "alguns segundos", + minute: "um minuto", + minutes: "%d minutos", + hour: "uma hora", + hours: "%d horas", + day: "um dia", + days: "%d dias", + month: "um mês", + months: "%d meses", + year: "um ano", + years: "%d anos" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.pt.js b/website_example/lang/timeago/jquery.timeago.pt.js new file mode 100644 index 0000000..13791a0 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.pt.js @@ -0,0 +1,26 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Portuguese + jQuery.timeago.settings.strings = { + suffixAgo: "atrás", + suffixFromNow: "a partir de agora", + seconds: "menos de um minuto", + minute: "cerca de um minuto", + minutes: "%d minutos", + hour: "cerca de uma hora", + hours: "cerca de %d horas", + day: "um dia", + days: "%d dias", + month: "cerca de um mês", + months: "%d meses", + year: "cerca de um ano", + years: "%d anos" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ro.js b/website_example/lang/timeago/jquery.timeago.ro.js new file mode 100644 index 0000000..fe59db9 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ro.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Romanian + jQuery.timeago.settings.strings = { + prefixAgo: "acum", + prefixFromNow: "in timp de", + suffixAgo: "", + suffixFromNow: "", + seconds: "mai putin de un minut", + minute: "un minut", + minutes: "%d minute", + hour: "o ora", + hours: "%d ore", + day: "o zi", + days: "%d zile", + month: "o luna", + months: "%d luni", + year: "un an", + years: "%d ani" + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.rs.js b/website_example/lang/timeago/jquery.timeago.rs.js new file mode 100644 index 0000000..b9e5188 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.rs.js @@ -0,0 +1,54 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Serbian + var numpf = function (n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "pre", + prefixFromNow: "za", + suffixAgo: null, + suffixFromNow: null, + second: "sekund", + seconds: function (value) { + return numpf(value, "%d sekund", "%d sekunde", "%d sekundi"); + }, + minute: "oko minut", + minutes: function (value) { + return numpf(value, "%d minut", "%d minuta", "%d minuta"); + }, + hour: "oko jedan sat", + hours: function (value) { + return numpf(value, "%d sat", "%d sata", "%d sati"); + }, + day: "jedan dan", + days: function (value) { + return numpf(value, "%d dan", "%d dana", "%d dana"); + }, + month: "mesec dana", + months: function (value) { + return numpf(value, "%d mesec", "%d meseca", "%d meseci"); + }, + year: "godinu dana", + years: function (value) { + return numpf(value, "%d godinu", "%d godine", "%d godina"); + }, + wordSeparator: " " + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ru.js b/website_example/lang/timeago/jquery.timeago.ru.js new file mode 100644 index 0000000..4ff3f8d --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ru.js @@ -0,0 +1,43 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Russian + function numpf(n, f, s, t) { + // f - 1, 21, 31, ... + // s - 2-4, 22-24, 32-34 ... + // t - 5-20, 25-30, ... + n = n % 100; + var n10 = n % 10; + if ( (n10 === 1) && ( (n === 1) || (n > 20) ) ) { + return f; + } else if ( (n10 > 1) && (n10 < 5) && ( (n > 20) || (n < 10) ) ) { + return s; + } else { + return t; + } + } + + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "через", + suffixAgo: "назад", + suffixFromNow: null, + seconds: "меньше минуты", + minute: "минуту", + minutes: function(value) { return numpf(value, "%d минуту", "%d минуты", "%d минут"); }, + hour: "час", + hours: function(value) { return numpf(value, "%d час", "%d часа", "%d часов"); }, + day: "день", + days: function(value) { return numpf(value, "%d день", "%d дня", "%d дней"); }, + month: "месяц", + months: function(value) { return numpf(value, "%d месяц", "%d месяца", "%d месяцев"); }, + year: "год", + years: function(value) { return numpf(value, "%d год", "%d года", "%d лет"); } + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.rw.js b/website_example/lang/timeago/jquery.timeago.rw.js new file mode 100644 index 0000000..50119e1 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.rw.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Kinyarwanda + jQuery.timeago.settings.strings = { + prefixAgo: "hashize", + prefixFromNow: "mu", + suffixAgo: null, + suffixFromNow: null, + seconds: "amasegonda macye", + minute: "umunota", + minutes: "iminota %d", + hour: "isaha", + hours: "amasaha %d", + day: "umunsi", + days: "iminsi %d", + month: "ukwezi", + months: "amezi %d", + year: "umwaka", + years: "imyaka %d", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.si.js b/website_example/lang/timeago/jquery.timeago.si.js new file mode 100644 index 0000000..6fa215e --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.si.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Sinhalese (SI) + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "පෙර", + suffixFromNow: "පසුව", + seconds: "තත්පර කිහිපයකට", + minute: "මිනිත්තුවකට පමණ", + minutes: "මිනිත්තු %d කට", + hour: "පැයක් පමණ ", + hours: "පැය %d කට පමණ", + day: "දවසක ට", + days: "දවස් %d කට ", + month: "මාසයක් පමණ", + months: "මාස %d කට", + year: "වසරක් පමණ", + years: "වසරක් %d කට පමණ" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.sk.js b/website_example/lang/timeago/jquery.timeago.sk.js new file mode 100644 index 0000000..e28ab7c --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.sk.js @@ -0,0 +1,34 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Slovak + (function() { + function f(n, d, a) { + return a[d>=0 ? 0 : a.length===2 || n<5 ? 1 : 2]; + } + + jQuery.timeago.settings.strings = { + prefixAgo: 'pred', + prefixFromNow: 'o', + suffixAgo: null, + suffixFromNow: null, + seconds: function(n, d) {return f(n, d, ['menej ako minútou', 'menej ako minútu']);}, + minute: function(n, d) {return f(n, d, ['minútou', 'minútu']);}, + minutes: function(n, d) {return f(n, d, ['%d minútami', '%d minúty', '%d minút']);}, + hour: function(n, d) {return f(n, d, ['hodinou', 'hodinu']);}, + hours: function(n, d) {return f(n, d, ['%d hodinami', '%d hodiny', '%d hodín']);}, + day: function(n, d) {return f(n, d, ['%d dňom', '%d deň']);}, + days: function(n, d) {return f(n, d, ['%d dňami', '%d dni', '%d dní']);}, + month: function(n, d) {return f(n, d, ['%d mesiacom', '%d mesiac']);}, + months: function(n, d) {return f(n, d, ['%d mesiacmi', '%d mesiace', '%d mesiacov']);}, + year: function(n, d) {return f(n, d, ['%d rokom', '%d rok']);}, + years: function(n, d) {return f(n, d, ['%d rokmi', '%d roky', '%d rokov']);} + }; + })(); +})); diff --git a/website_example/lang/timeago/jquery.timeago.sl.js b/website_example/lang/timeago/jquery.timeago.sl.js new file mode 100644 index 0000000..9f0329a --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.sl.js @@ -0,0 +1,46 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Slovenian with support for dual + var numpf = function (n, a) { + return a[n%100===1 ? 1 : n%100===2 ? 2 : n%100===3 || n%100===4 ? 3 : 0]; + }; + + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "čez", + suffixAgo: "nazaj", + suffixFromNow: null, + second: "sekundo", + seconds: function (value) { + return numpf(value, ["%d sekund", "%d sekundo", "%d sekundi", "%d sekunde"]); + }, + minute: "minuto", + minutes: function (value) { + return numpf(value, ["%d minut", "%d minuto", "%d minuti", "%d minute"]); + }, + hour: "eno uro", + hours: function (value) { + return numpf(value, ["%d ur", "%d uro", "%d uri", "%d ure"]); + }, + day: "en dan", + days: function (value) { + return numpf(value, ["%d dni", "%d dan", "%d dneva", "%d dni"]); + }, + month: "en mesec", + months: function (value) { + return numpf(value, ["%d mesecev", "%d mesec", "%d meseca", "%d mesece"]); + }, + year: "eno leto", + years: function (value) { + return numpf(value, ["%d let", "%d leto", "%d leti", "%d leta"]); + }, + wordSeparator: " " + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.sq.js b/website_example/lang/timeago/jquery.timeago.sq.js new file mode 100644 index 0000000..cb8ae70 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.sq.js @@ -0,0 +1,26 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Albanian SQ + jQuery.timeago.settings.strings = { + suffixAgo: "më parë", + suffixFromNow: "tani", + seconds: "më pak se një minutë", + minute: "rreth një minutë", + minutes: "%d minuta", + hour: "rreth një orë", + hours: "rreth %d orë", + day: "një ditë", + days: "%d ditë", + month: "rreth një muaj", + months: "%d muaj", + year: "rreth një vit", + years: "%d vjet" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.sr.js b/website_example/lang/timeago/jquery.timeago.sr.js new file mode 100644 index 0000000..bd1efe7 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.sr.js @@ -0,0 +1,54 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Serbian + var numpf = function (n, f, s, t) { + var n10; + n10 = n % 10; + if (n10 === 1 && (n === 1 || n > 20)) { + return f; + } else if (n10 > 1 && n10 < 5 && (n > 20 || n < 10)) { + return s; + } else { + return t; + } + }; + + jQuery.timeago.settings.strings = { + prefixAgo: "пре", + prefixFromNow: "за", + suffixAgo: null, + suffixFromNow: null, + second: "секунд", + seconds: function (value) { + return numpf(value, "%d секунд", "%d секунде", "%d секунди"); + }, + minute: "један минут", + minutes: function (value) { + return numpf(value, "%d минут", "%d минута", "%d минута"); + }, + hour: "један сат", + hours: function (value) { + return numpf(value, "%d сат", "%d сата", "%d сати"); + }, + day: "један дан", + days: function (value) { + return numpf(value, "%d дан", "%d дана", "%d дана"); + }, + month: "месец дана", + months: function (value) { + return numpf(value, "%d месец", "%d месеца", "%d месеци"); + }, + year: "годину дана", + years: function (value) { + return numpf(value, "%d годину", "%d године", "%d година"); + }, + wordSeparator: " " + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.sv.js b/website_example/lang/timeago/jquery.timeago.sv.js new file mode 100644 index 0000000..caf09db --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.sv.js @@ -0,0 +1,28 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Swedish + jQuery.timeago.settings.strings = { + prefixAgo: "för", + prefixFromNow: "om", + suffixAgo: "sedan", + suffixFromNow: "", + seconds: "mindre än en minut", + minute: "ungefär en minut", + minutes: "%d minuter", + hour: "ungefär en timme", + hours: "ungefär %d timmar", + day: "en dag", + days: "%d dagar", + month: "ungefär en månad", + months: "%d månader", + year: "ungefär ett år", + years: "%d år" + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.th.js b/website_example/lang/timeago/jquery.timeago.th.js new file mode 100644 index 0000000..23d59d4 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.th.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Thai + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ที่แล้ว", + suffixFromNow: "จากตอนนี้", + seconds: "น้อยกว่าหนึ่งนาที", + minute: "ประมาณหนึ่งนาที", + minutes: "%d นาที", + hour: "ประมาณหนึ่งชั่วโมง", + hours: "ประมาณ %d ชั่วโมง", + day: "หนึ่งวัน", + days: "%d วัน", + month: "ประมาณหนึ่งเดือน", + months: "%d เดือน", + year: "ประมาณหนึ่งปี", + years: "%d ปี", + wordSeparator: "", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.tr.js b/website_example/lang/timeago/jquery.timeago.tr.js new file mode 100644 index 0000000..8e0d2d4 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.tr.js @@ -0,0 +1,26 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Turkish + jQuery.timeago.settings.strings = { + suffixAgo: 'önce', + suffixFromNow: null, + seconds: 'birkaç saniye', + minute: '1 dakika', + minutes: '%d dakika', + hour: '1 saat', + hours: '%d saat', + day: '1 gün', + days: '%d gün', + month: '1 ay', + months: '%d ay', + year: '1 yıl', + years: '%d yıl' + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.uk.js b/website_example/lang/timeago/jquery.timeago.uk.js new file mode 100644 index 0000000..489963b --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.uk.js @@ -0,0 +1,42 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Ukrainian + function numpf(n, f, s, t) { + // f - 1, 21, 31, ... + // s - 2-4, 22-24, 32-34 ... + // t - 5-20, 25-30, ... + var n10 = n % 10; + if ( (n10 === 1) && ( (n === 1) || (n > 20) ) ) { + return f; + } else if ( (n10 > 1) && (n10 < 5) && ( (n > 20) || (n < 10) ) ) { + return s; + } else { + return t; + } + } + + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "через", + suffixAgo: "тому", + suffixFromNow: null, + seconds: "менше хвилини", + minute: "хвилина", + minutes: function(value) { return numpf(value, "%d хвилина", "%d хвилини", "%d хвилин"); }, + hour: "година", + hours: function(value) { return numpf(value, "%d година", "%d години", "%d годин"); }, + day: "день", + days: function(value) { return numpf(value, "%d день", "%d дні", "%d днів"); }, + month: "місяць", + months: function(value) { return numpf(value, "%d місяць", "%d місяці", "%d місяців"); }, + year: "рік", + years: function(value) { return numpf(value, "%d рік", "%d роки", "%d років"); } + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.ur.js b/website_example/lang/timeago/jquery.timeago.ur.js new file mode 100644 index 0000000..9d0cd40 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.ur.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Urdu + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "پہلے", + suffixFromNow: "اب سے", + seconds: "کچھ سیکنڈز", + minute: "تقریباً ایک منٹ", + minutes: "%d منٹ", + hour: "تقریباً ایک گھنٹہ", + hours: "تقریباً %d گھنٹے", + day: "ایک دن", + days: "%d دن", + month: "تقریباً ایک مہینہ", + months: "%d مہینے", + year: "تقریباً ایک سال", + years: "%d سال", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.uz.js b/website_example/lang/timeago/jquery.timeago.uz.js new file mode 100644 index 0000000..f4ce8b3 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.uz.js @@ -0,0 +1,29 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + //Uzbek + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: "keyin", + suffixAgo: "avval", + suffixFromNow: null, + seconds: "bir necha soniya", + minute: "1 daqiqa", + minutes: function(value) { return "%d daqiqa"; }, + hour: "1 soat", + hours: function(value) { return "%d soat"; }, + day: "1 kun", + days: function(value) { return "%d kun"; }, + month: "1 oy", + months: function(value) { return "%d oy"; }, + year: "1 yil", + years: function(value) { return "%d yil"; }, + wordSeparator: " " + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.vi.js b/website_example/lang/timeago/jquery.timeago.vi.js new file mode 100644 index 0000000..30f592a --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.vi.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Vietnamese + jQuery.timeago.settings.strings = { + prefixAgo: 'cách đây', + prefixFromNow: null, + suffixAgo: null, + suffixFromNow: "trước", + seconds: "chưa đến một phút", + minute: "khoảng một phút", + minutes: "%d phút", + hour: "khoảng một tiếng", + hours: "khoảng %d tiếng", + day: "một ngày", + days: "%d ngày", + month: "khoảng một tháng", + months: "%d tháng", + year: "khoảng một năm", + years: "%d năm", + wordSeparator: " ", + numbers: [] + }; +})); diff --git a/website_example/lang/timeago/jquery.timeago.zh-CN.js b/website_example/lang/timeago/jquery.timeago.zh-CN.js new file mode 100644 index 0000000..c21a287 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.zh-CN.js @@ -0,0 +1,31 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Simplified Chinese + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "之前", + suffixFromNow: "之后", + seconds: "不到1分钟", + minute: "大约1分钟", + minutes: "%d分钟", + hour: "大约1小时", + hours: "大约%d小时", + day: "1天", + days: "%d天", + month: "大约1个月", + months: "%d月", + year: "大约1年", + years: "%d年", + numbers: [], + wordSeparator: "" + }; +})); + diff --git a/website_example/lang/timeago/jquery.timeago.zh-TW.js b/website_example/lang/timeago/jquery.timeago.zh-TW.js new file mode 100644 index 0000000..15f5626 --- /dev/null +++ b/website_example/lang/timeago/jquery.timeago.zh-TW.js @@ -0,0 +1,30 @@ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + factory(jQuery); + } +}(function (jQuery) { + // Traditional Chinese, zh-tw + jQuery.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "之前", + suffixFromNow: "之後", + seconds: "不到1分鐘", + minute: "大約1分鐘", + minutes: "%d分鐘", + hour: "大約1小時", + hours: "%d小時", + day: "大約1天", + days: "%d天", + month: "大約1個月", + months: "%d個月", + year: "大約1年", + years: "%d年", + numbers: [], + wordSeparator: "" + }; +})); diff --git a/website_example/lang/zh-CN.json b/website_example/lang/zh-CN.json new file mode 100644 index 0000000..4a6a00a --- /dev/null +++ b/website_example/lang/zh-CN.json @@ -0,0 +1,169 @@ +{ + "miningPool": "矿池", + "dashboard": "仪表盘", + "gettingStarted": "挖矿指导", + "yourStats": "挖矿数据", + "poolBlocks": "池中区块", + "settings": "设置", + "faq": "常见问题", + "telegram": "Telegram群组", + "discord": "Discord", + "contactUs": "联系方式", + + "network": "全网", + "pool": "矿池", + "you": "用户", + "statsUpdated": "数据更新", + + "poolHashrate": "矿池算力", + "currentEffort": "当前效益", + "networkHashrate": "全网算力", + "networkDifficulty": "难度", + "blockchainHeight": "区块高度", + "networkLastReward": "上次收益", + "poolMiners": "在线矿工", + "poolFee": "矿池税", + + "minerStats": "用户数据及支付历史", + "workerStats": "设备数据", + "miner": "矿工", + "miners": "矿工", + "minersCount": "矿工数", + "workers": "挖矿设备", + "workersCount": "设备数", + "workerName": "设备名", + "lastHash": "上次提交", + "hashRate": "算力", + "currentHashRate": "当前算力", + "lastShare": "上次提交", + "totalHashes": "总提交", + "top10miners": "前十矿工", + + "blocksTotal": "已发现区块", + "blockSolvedTime": "区块发现用时", + "blocksMaturityCount": "成熟要求", + "efficiency": "效率", + "averageLuck": "平均幸运值", + "timeFound": "发现时间", + "reward": "收益", + "height": "高度", + "difficulty": "难度", + "blockHash": "区块Hash", + "effort": "功夫", + "blocksFoundLast24": "过去24小时内发现区块", + "blocksFoundLastDays": "过去{DAYS}天中发现区块", + "propSoloConnectedMiners": "成比例的 / 独奏 连接的 矿工", + + "payments": "支付", + "paymentsHistory": "支付历史", + "paymentsTotal": "总支付", + "paymentsMinimum": "最小支付额", + "paymentsInterval": "支付周期", + "paymentsDenomination": "面额单位", + "timeSent": "Time Sent", + "transactionHash": "交易Hash", + "amount": "数额", + "fee": "费用", + "mixin": "Mixin", + "payees": "收款人", + "pendingBalance": "处理中余额", + "totalPaid": "总计支付", + "payoutEstimate": "当前支付估计", + "paymentSummarySingle": "在%DATE%您收到%AMOUNT%", + "paymentSummaryMulti": "在%DATE%,通过%COUNT%笔支付,您收到%AMOUNT%", + + "connectionDetails": "连接信息", + "miningPoolHost": "矿池地址", + "cnAlgorithm": "算法", + "username": "用户名", + "usernameDesc": "这是你的钱包地址", + "paymentId": "交易所支付ID", + "fixedDiff": "固定难度", + "address": "地址", + "addrPaymentId": "交易ID", + "addrDiff": "难度", + "password": "密码", + "passwordDesc": "这是你的设备名", + "emailNotifications": "邮件提醒", + "miningPorts": "挖矿端口", + "port": "端口", + "portDiff": "初始难度", + "description": "介绍", + "miningApps": "挖矿应用", + "configGeneratorDesc": "生成您定制的配置", + "addressField": "钱包地址", + "paymentIdField": "为交易所的交易ID (可选)", + "fixedDiffField": "固定难度 (可选)", + "workerNameField": "设备名", + "emailNotificationsField": "邮件提醒 (可选)", + "generateConfig": "生成配置", + "appName": "应用名", + "appArch": "架构平台", + "appDesc": "特征", + "download": "下载", + "showConfig": "了解更多", + + "market": "市场计算器", + "loadingMarket": "加载市场价格", + "priceIn": "价格", + "hashPer": "Hash/", + "estimateProfit": "预计挖矿收益", + "enterYourHashrate": "确认您的算力", + "perDay": "每天", + + "verificationFields": "验证字段", + "minerVerification": "为了确保钱包地址是您的,我们要求您提供您的矿工使用的IP地址之一.", + "minerAddress": "矿工地址", + "minerIP": "矿工IP地址", + "setMinimumPayout": "设置您的最低支付水平", + "minerMinPayout": "如果你喜欢比池的默认值更高的支付水平,那么这是你可以为你的矿工改变它。您在此处显示的金额将成为您的地址的最低付款金额.", + "minimumPayout": "最低付款金额", + "enableEmailNotifications": "启用电子邮件通知", + "minerEmailNotify": "当发现一个区块并且每当支付发生时,该池将发送电子邮件通知.", + "emailAddress": "邮箱地址", + "noMinerAddress": "没有指定矿工地址", + "noMinerIP": "没有指定矿工IP地址", + "noPayoutLevel": "没有指定最小付款金额", + "noEmail": "没有指定邮箱地址", + "invalidEmail": "指定的电子邮件地址无效", + "minerPayoutSet": "完成! 您的最低支出水平已设定", + "notificationEnabled": "完成! 电子邮件通知已启用", + "notificationDisabled": "完成! 电子邮件通知已被禁用", + + "enterYourAddress": "输入您的地址", + "enterYourMinerIP": "您的矿工使用的IP地址 (任一)", + "enterYourEmail": "输入你的电子邮箱地址 (可选)", + + "lookup": "查找", + "searching": "搜索中...", + "loadMore": "加载更多", + "set": "设置", + "enable": "允许", + "disable": "禁止", + + "status": "状态", + "updated": "已更新:", + "source": "源:", + "error": "错误:", + + "na": "无", + "estimated": "预计", + "never": "从不", + "second": "秒", + "seconds": "秒", + "minute": "分", + "minutes": "分", + "hour": "小时", + "hours": "小时", + "day": "天", + "days": "天", + "week": "周", + "weeks": "周", + "month": "月", + "months": "月", + "year": "年", + "years": "年", + + "poweredBy": "驱动源于", + "openSource": "开源于" +} diff --git a/website_example/pages/admin/monitoring.html b/website_example/pages/admin/monitoring.html new file mode 100644 index 0000000..f949c3e --- /dev/null +++ b/website_example/pages/admin/monitoring.html @@ -0,0 +1,177 @@ + + + +
+
+ + + + + + + + \ No newline at end of file diff --git a/website_example/pages/admin/ports.html b/website_example/pages/admin/ports.html new file mode 100644 index 0000000..72857fa --- /dev/null +++ b/website_example/pages/admin/ports.html @@ -0,0 +1,85 @@ +

Current Ports Usage (0 users)

+ + + + + +
+
+ + + + + + + + + + +
PortConnected Users 
+
+
+ + + + diff --git a/website_example/pages/admin/statistics.html b/website_example/pages/admin/statistics.html new file mode 100644 index 0000000..d683aaa --- /dev/null +++ b/website_example/pages/admin/statistics.html @@ -0,0 +1,204 @@ +

Pool Statistics

+ +
+
+ + + + + diff --git a/website_example/pages/admin/tools.html b/website_example/pages/admin/tools.html new file mode 100644 index 0000000..7ebd623 --- /dev/null +++ b/website_example/pages/admin/tools.html @@ -0,0 +1,124 @@ + +

Test E-Mail notifications

+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ + +

Test Telegram Channel notifications

+ +
+
+ +
+
+
+ +
+
+
+ + diff --git a/website_example/pages/admin/userslist.html b/website_example/pages/admin/userslist.html new file mode 100644 index 0000000..b76edfc --- /dev/null +++ b/website_example/pages/admin/userslist.html @@ -0,0 +1,131 @@ + + +
+
+ + + + + + diff --git a/website_example/pages/faq.html b/website_example/pages/faq.html new file mode 100644 index 0000000..d56dc47 --- /dev/null +++ b/website_example/pages/faq.html @@ -0,0 +1,140 @@ +

Frequently Asked Questions (FAQ)

+ +

How do I pick which coins to merge mine?

+

The hash work of your miner application to the parent coin is also applied to the child coin you specify in the mining software pool password field.

+ +

You must mine at one parent coin, which in this case is ARQ for ArQmA. +Specify your ArQmA wallet address and static difficulty per the Getting Started page. +Using the pool password field in your miner to specify one of the child coin wallet addresses. See which child coins are listed on the pool's Dashboard page. +You will need at least a wallet address from an exchange or a wallet/paper wallet application to get an coin address for the parent coin and each child coin you wish to merge mine.

+ +

You can also connect multiple rigs with the same wallet address for the parent coin, and different child coin wallet address to apply hash to that child coin also.

+ +

Specify your rig_id in your miner software.

+

Put your child coin wallet address in the pool password field and put "@rig_id" at the end. + It can be a direct to exchange address also.

+

For example in XMR-STAK, the pools.txt config file looks something like this: +"rig_id" : "Johnny5", +An example wallet for Turtle child coin is "TRTL2aedfsr23blahlongaddressfield+paymentIDsomething" +so make it formatted for the pool password as +"pool_password" : "TRTL2aedfsr23blahlongaddressfield+paymentIDsomething@Johnny5"

+ +

You can also use the configuration generator within the Getting Started section of the pool.

+

Also, you can segment your cpu mining on one mining application to be applied to +the parent coin and specify your hash work one of the several child coins. +There is no limit on how many workers you can specify. You can also customize your miner application to mine only with certain gpus to a particular parent/child combination. Probably you can run several mining applications in tandem as long as they use only resources you specify.

+ +
+ +

Where are the stats for the pool? Why hasn't anything changed?

+
The dashboard lists the coins being mined, reward for each block, current effort on each block for the merged mining. Sometimes the Pool Blocks section and Worker Statistics needs a refresh by your browser to get updated stats. Typically that is pressing CTRL+F5 to flush the cache and ask your brower for fresh data.
+ + +

What is difficulty?

+
Difficulty is a measure of how difficult it is to find a hash below a given target. The target difficulty changes from block to block based on the network hashrate attempted by all nodes on a coin network. However, the difficulty for child coins can be dissimlar to the parent coin you start mining with
+ +

What is luck?

+
Mining is probabilistic in nature: if you find a block earlier than you statistically should on average you are lucky if it takes longer, you are unlucky. In a perfect World pool would find a block on 100% luck value. Less then 100% means the pool was lucky. More then 100% means the pool was unlucky. +
+ +

What is share?

+
Share is a possible valid hash for the block. Shares are beings sent by your rigs to the pool to prove their work. You should set your fixed difficulty to be somewhere around your hash rate average on your miner multiplied by 28-30. Aim for your average number of accepted shares to be within the block time (30 seconds typically for child coins, 2 minutes for ARQ) +If your shares are accepted faster, but with lower difficulty each share is worth less, but you should get at least one share in per average block time. It doesn't help the pool by spamming a large amount of low difficulty shares. It tends to tie up more resources and might get you banned.
+ +

What is block?

+
Transaction data is recorded in blocks. New transactions are being processes by miners into new blocks which are added to the end of the blockchain.
+ +

How long does it take to find a block?

+
It depends on amount of active miners. The more miners work on pool → the more hashrate pool has → the more blocks are found by the pool. However the more miners are active → the less reward you get from each block found. +Probablistically, even smaller pools will less miners or hash rate will eventually get a block. +Decentralizing the hash proof of work across several pools on any network is important for security of the coin network, for both the parent coin and the child coins. +The block time is how long it can take on average amount effort . The average for ArQmA is 120 seconds. However, some of the child coins have block timeframes on the average of 15 to 30 seconds. Check your child coins network specification. Some blocks are solved faster than the average block time, some blocks can have a higher than average block effort time. +Also some other pools might solve the block first, and there is a slim possibility of an orphan block on a child coin being mined.
+ +

What is an orphan block?

+
An orphan is a block that didn't meet the requirement as a solution to the block found. Also, there can be a situation where another pool or solo miner found the block solution first, and narrowly won the race. Blocks are confirmed by hash verfication by all the nodes on the coin's network. If there is a consensus of which node(pool,solo) found it first, then that block is added to the block chain permanenty with credit applying to that pool/solo/node. +A few orphan blocks on child coins can happen when the parent coin's hash work doesn't meet the requirements.
+ + +

Which payouts scheme is used?

+
Proportional (Share-based): Every time a block is found, its reward is split between miners according to the number of shares they submitted.
+ +

How current payout estimate is calculated?

+
The estimated payout is a calculated using your percentage of valid shares on the total for current round in proportion to the total shares submitted by all miners/workers in that round. This percentage is then applied to the reward of the last block found by the network and subtracting the small pool fee.. See the Worker Statistics page on the pool web page. +
+ +

I have been mining on this pool for 1 hour but still have not received any payouts. WTF?

+

As soon as the block is found you will get your reward. Please wait a little bit more time. In the case of ArQmA, a block has at least 18 confirmations before the pool starts its payout cycle. Typically every 36 minutes for a completed block plus the time interval set by the pool operator on the pool's Dashboard screeen. Payments can be halted for various reasons within the automated pool software, but highly unlikely. There is usually a minimum payout for each parent coin and child coins. Payments for matured blocks that meet the minimum can also be triggered manually by a pool operator/administrator. Ask nicely in Telegram or Discord support. Please have your information all in order before you ask such as miner application, version number, gpu and cpu information, wallet addresses, and error messages reported. +

The merge mined child coins have a different number of confirmation blocks needed, but at least the pool checks for fully verified blocks set at the interval on the Dashboard page. Not every parent block found generates a child block. +Check the Pool Blocks page on the pool. Or configure your settings to get an email alert if you like on the Settings pool page. +

+ +

My hashrate is wrong! Why?

+
Since you start to mine your hashrate grows gradually. Please wait. The pool determines your hashrate based on the amount of shares sent by your mining rigs (workers). This value could be a little bit different from reported hasrate (in your mining software). +
+ +

What is solo mining?

+

Solo mining is attempting to find a block by yourself separate from the pool of proportional miners collectively solving the current block.You get almost the entire block reward minus a small fee for the pool. You can specfy your mining software by changing the wallet address with a prefix of "solo:" added to your wallet specification. +However, if the coin network has a high hash in total (see Dashboard), the difficulty is also higher. Typically only very large mining rigs with at least 5-10% of the coin network total is needed to get timely solo blocks. However, you could get a string of blocks on lucky streak, then not find a block solution for a very long time. The more hash rate, the higher the probability(luck). You can generate a solo configuration on the Getting Started page on the pool. Tick the checkbox "solo mining". However, as solo you are also competing against the pool proportional share miners, and evvery single node on the parent coin network also. +

+

In the case of solo merged mining specified in the pool password field, you are also competing against the main pool users on proportional shares, and the entire child coin network and the other pools on the parent network that are also merge mining. However the reward is much higher since you might get two coin blocks(or more) for the same effort. +

+
+ +

I've been banned by the pool itself. What do I do now?

+

Disconnect your miner for five minutes at least. Then try again. Please pay close attention to error messages reported by the pool software to your miner client software. Typical errors are invalid parent wallet address format, or bad child coin wallet address specified in the pool password field. Check your miner logs if you have them turned on. +

+

Did you forget to turn off nicehash option?

+

For persistent banning, check with the pool operator or Admininstrator to see if there is something else going wrong outside of your control. Links for Telegram and Discord support are on the left side of the pool web page front end.

+
+ + +
+

How to use the Telegram Bot?

+
+
Telegram bot will report via Telegram, when your worker is connected, disconnected or banned, and payments have been made to your wallet. Also you can check pool stats directly from him.
+
+ + Bot name: @Pool_bot
+
+ + Commands:
+ /stats - Pool statistics
+ /blocks - Blocks notifications (enable or disable)
+ /report address - Miner statistics
+ /notify address - Miner notifications (enable or disable)
+
+ + Multiple addresses monitoring available.
+
+
+ + + \ No newline at end of file diff --git a/website_example/pages/getting_started.html b/website_example/pages/getting_started.html new file mode 100644 index 0000000..b96b8ee --- /dev/null +++ b/website_example/pages/getting_started.html @@ -0,0 +1,476 @@ + +
+
+

Connection Details

+
+
+
Mining Pool Address:
+
Algorithm:
+
+
+ +

Username

+
+
+
This is your wallet address
+
Exchange Payment ID: parent address.paymentID
+
Difficulty locking: parent address+diff
+
+
+ +

Password

+
+
+
Use your Iridium OR Nibble wallet address in the password field for merged mining.
+
Exchange Payment ID: child address+paymentID
+
Worker Name: child-address@workerName
+
Payment ID and Worker Name: child-address+paymentID@workerName
+
+
+
+ + +
+

Mining Ports

+
+
+ + + + + + + + + + + + + + + +
PortStarting DifficultyDescription
+
+
+
+ +
+ + +

Mining Applications

+ + +
+

Generate your custom configuration to mine on our pool

+
+
+
+ + +
+
+
+
+ Solo Mining + + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
App Name Architecture Features Download Configuration
XMR StakCPU & GPU (AMD/NVIDIA)Easy to use CPU + GPU Mining AppDownloadSee more
+
+ +
"pool_list": [
+    {
+        "pool_address": "POOL_HOST:PORT",
+        "wallet_address": "YOUR_WALLET_ADDRESS",
+        "rig_id": "",
+        "pool_password": /* Solo Mining set [solo:] */"YOUR_WORKER_NAME",
+        "use_nicehash": false,
+        "use_tls": false, /* Set to true if you are using an SSL port */
+        "tls_fingerprint": "",
+        "pool_weight": 1
+    },
+],
+"currency": "",
+ +
+
XMRigCPULigthweight but powerful CPU Mining AppDownloadSee more
+
+ +
"pools": [
+    {
+        "url": "POOL_HOST:PORT",
+        "user": "YOUR_WALLET_ADDRESS",
+        "pass": "YOUR_WORKER_NAME",
+        "keepalive": true,
+        "nicehash": false,
+        "variant": "trtl",
+        "algo": "cryptonight-pico/trtl"
+    }
+],
+ +
+
XMRig-AMDOpenCL (AMD)XMRIG version for AMD GPUDownloadSee more
+
+ +
"pools": [
+    {
+        "url": "POOL_HOST:PORT",
+        "user": "YOUR_WALLET_ADDRESS",
+        "pass": "YOUR_WORKER_NAME"
+        "keepalive": true,
+        "nicehash": false,
+        "variant": "trtl",
+        "algo": "cryptonight-pico/trtl"
+    }
+],
+ +
+
XMRig-NVIDIACuda (Nvidia)XMRIG version for Nvidia GPUDownloadSee more
+
+ +
"pools": [
+    {
+        "url": "POOL_HOST:PORT",
+        "user": "YOUR_WALLET_ADDRESS",
+        "pass": "YOUR_WORKER_NAME"
+        "keepalive": true,
+        "nicehash": false
+        "variant": "trtl",
+        "algo": "cryptonight-pico/trtl"
+    }
+],
+ +
+
XMRigCCCPUXMRIG Fork, optimized with remote controlDownloadSee more
+
+ +
"pools": [
+    {
+        "url": "POOL_HOST:PORT",
+        "user": "YOUR_WALLET_ADDRESS",
+        "pass": "YOUR_WORKER_NAME",
+        "keepalive": true,
+        "nicehash": false,
+        "algo": "cryptonight-ultralite",
+        "pow-variant": "2"
+    },
+],
+ +
+
Team Red MinerGPUAMD GPU Optimized Cryptocurrency MinerDownloadSee more
+
+
teamredminer.ext -a cnv8 trtle -o stratum+tcp//POOL_HOST:PORT -u "YOUR_WALLET_ADDRESS" -p "YOUR_WORKER_NAME" 
+
+ +
+
+
+
+ + + + diff --git a/website_example/pages/home.html b/website_example/pages/home.html new file mode 100644 index 0000000..5337c7c --- /dev/null +++ b/website_example/pages/home.html @@ -0,0 +1,303 @@ + +
+
+
+
+
+ + + + + + + + +
+
+

Hash Rate

+
+
+
+
+
+

Difficulty

+
+
+
+
+
+

Miners

+
+
+
+
+
+

Workers

+
+
+
+
+
+ + + diff --git a/website_example/pages/market.html b/website_example/pages/market.html new file mode 100644 index 0000000..0fbd597 --- /dev/null +++ b/website_example/pages/market.html @@ -0,0 +1,151 @@ + + +
+
+ +
+
+ + + + + + + + + + + + + diff --git a/website_example/pages/payments.html b/website_example/pages/payments.html new file mode 100644 index 0000000..b3327be --- /dev/null +++ b/website_example/pages/payments.html @@ -0,0 +1,138 @@ + + +
+
+ + + + + + + + diff --git a/website_example/pages/pool_blocks.html b/website_example/pages/pool_blocks.html new file mode 100644 index 0000000..2317af4 --- /dev/null +++ b/website_example/pages/pool_blocks.html @@ -0,0 +1,147 @@ + + +
+
+ + + + + + + + diff --git a/website_example/pages/settings.html b/website_example/pages/settings.html new file mode 100644 index 0000000..ade35e0 --- /dev/null +++ b/website_example/pages/settings.html @@ -0,0 +1,82 @@ + + +
+
+ + + + + + diff --git a/website_example/pages/top10miners.html b/website_example/pages/top10miners.html new file mode 100644 index 0000000..70f7231 --- /dev/null +++ b/website_example/pages/top10miners.html @@ -0,0 +1,70 @@ + +

Top 10 miners

+ + + +
+
+ + + + + + + \ No newline at end of file diff --git a/website_example/pages/worker_stats.html b/website_example/pages/worker_stats.html new file mode 100644 index 0000000..dc55217 --- /dev/null +++ b/website_example/pages/worker_stats.html @@ -0,0 +1,207 @@ + +

Your Stats & Payment History

+ + + +
+
+ + + + + + + diff --git a/website_example/safari-pinned-tab.svg b/website_example/safari-pinned-tab.svg new file mode 100644 index 0000000..5b362c2 --- /dev/null +++ b/website_example/safari-pinned-tab.svg @@ -0,0 +1,15 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/website_example/site.webmanifest b/website_example/site.webmanifest new file mode 100644 index 0000000..a1553eb --- /dev/null +++ b/website_example/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/website_example/themes/admin.css b/website_example/themes/admin.css new file mode 100644 index 0000000..4d03fbb --- /dev/null +++ b/website_example/themes/admin.css @@ -0,0 +1,103 @@ +#page-wrapper { + padding: 0 15px 0; + min-height: 568px; +} +@media (min-width: 992px) { + #page-wrapper { + padding: 0 30px; + } +} + +.nav-side-menu li.sign-out { + border-left: 3px solid darkred; + background-color: rgba(255,0,0,.2); +} + +.nav-side-menu li.sign-out:hover { + border-left: 3px solid darkred; + background-color: darkred; +} + +/* Pool Statistics */ +#poolStats .luckGood { + color: #5eff5e; +} +#poolStats .luckBad { + color: red; +} +#poolStats .luckMid { + color: #FFF500; +} + +/* Users list */ +.usersList { + word-wrap: break-word; +} +.usersList .tooltip-inner { + max-width: 100%; +} +@media (min-width: 768px) { + .usersList { + table-layout: fixed; + } + .usersList tr > th, + .usersList tr > td { + text-align: center; + white-space: nowrap; + } + .usersList .col1 { + white-space: normal; + } + .usersList .col2 { + width: 100px; + } + .usersList .col3 { + width: 100px; + } + .usersList .col4 { + width: 100px; + } + .usersList .col5 { + width: 100px; + } + .usersList .col6 { + width: 120px; + white-space: normal; + } +} + +/* Monitoring */ +.adminMonitor code { + white-space: normal; +} +.adminMonitor .tab-pane li { + margin-bottom: 10px; +} +.adminMonitor .infos { + margin-bottom: 20px; +} +.adminMonitor #logTable th { + white-space: nowrap; +} + +/* Ports Usage */ +#portsUsage tr > td { + vertical-align: middle; + font-size: 0.95em; +} +#portsUsage table .col1, +#portsUsage table .col2 { + text-align: center; +} +@media (min-width: 768px) { + #portsUsage table th { + white-space: nowrap; + } + #portsUsage table .col1 { + width:60px; + } + #portsUsage table .col2 { + width:160px; + text-align: center; + } +} diff --git a/website_example/themes/custom.css b/website_example/themes/custom.css new file mode 100644 index 0000000..1ebdecc --- /dev/null +++ b/website_example/themes/custom.css @@ -0,0 +1,133 @@ +/* Insert your pool's unique CSS here */ + +.nav-side-menu { + background: #164450; +} + +.nav-side-menu .brand { + background: #164450; + color: #fff; + padding: 10px; +} +.nav-side-menu .brand a { + color: #fff; + font-size: 16px; +} + +.brand { padding-top: 5px } +.brand img.img-responsive { + margin: 0 auto; +} + +.nav-side-menu ul .active, .nav-side-menu li .active { + border-left: 3px solid #5fbcd3; + background-color: #2c89a0; +} + +.infoBox { + /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#2c89a0+0,5fbcd3+100 */ +background: #2c89a0; /* Old browsers */ +background: -moz-linear-gradient(45deg, #2c89a0 0%, #5fbcd3 100%); /* FF3.6-15 */ +background: -webkit-linear-gradient(45deg, #2c89a0 0%,#5fbcd3 100%); /* Chrome10-25,Safari5.1-6 */ +background: linear-gradient(45deg, #2c89a0 0%,#5fbcd3 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2c89a0', endColorstr='#5fbcd3',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ +} + +.mergedBg { + background: #3b5ca3; +} + +#top-bar { + background-color: #164450; +} + +.infoBox .content .value sup { + top: -.9em; + text-decoration: none; + text-shadow: none; + font-size: 45%; + right: -3px; + font-weight: normal; +} + +#infoBlocksTotal .content .value .smallText { + font-size: 15px; + font-weight: normal; + /*display: block;*/ + /*text-shadow: none;*/ + margin-top: -3px; +} + +#infoBlocksFound .content .value .smallText { + font-size: 15px; + font-weight: normal; + /*display: block;*/ + /*text-shadow: none;*/ + margin-top: -3px; +/* Insert your pool's unique CSS here */ + +.nav-side-menu { + background: #164450; +} + +.nav-side-menu .brand { + background: #164450; + color: #fff; + padding: 10px; +} +.nav-side-menu .brand a { + color: #fff; + font-size: 16px; +} + +.brand { padding-top: 5px } +.brand img.img-responsive { + margin: 0 auto; +} + +.nav-side-menu ul .active, .nav-side-menu li .active { + border-left: 3px solid #5fbcd3; + background-color: #2c89a0; +} + +.infoBox { + /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#2c89a0+0,5fbcd3+100 */ +background: #2c89a0; /* Old browsers */ +background: -moz-linear-gradient(45deg, #2c89a0 0%, #5fbcd3 100%); /* FF3.6-15 */ +background: -webkit-linear-gradient(45deg, #2c89a0 0%,#5fbcd3 100%); /* Chrome10-25,Safari5.1-6 */ +background: linear-gradient(45deg, #2c89a0 0%,#5fbcd3 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ +filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2c89a0', endColorstr='#5fbcd3',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ +} + +.mergedBg { + background: #3b5ca3; +} + +#top-bar { + background-color: #164450; +} + +.infoBox .content .value sup { + top: -.9em; + text-decoration: none; + text-shadow: none; + font-size: 45%; + right: -3px; + font-weight: normal; +} + +#infoBlocksTotal .content .value .smallText { + font-size: 15px; + font-weight: normal; + /*display: block;*/ + /*text-shadow: none;*/ + margin-top: -3px; +} + +#infoBlocksFound .content .value .smallText { + font-size: 15px; + font-weight: normal; + /*display: block;*/ + /*text-shadow: none;*/ + margin-top: -3px; +} \ No newline at end of file diff --git a/website_example/themes/default.css b/website_example/themes/default.css new file mode 100644 index 0000000..d93f459 --- /dev/null +++ b/website_example/themes/default.css @@ -0,0 +1,1016 @@ +@import url(//fonts.googleapis.com/css?family=Roboto+Condensed:400,700); +@import url(//fonts.googleapis.com/css?family=Roboto:400,300,500,700); +@import url(//fonts.googleapis.com/css?family=Inconsolata:400,700); + +body { + background-color: #f5f5f5; + font-family: 'Roboto', Helvetica, Arial, sans-serif; + line-height: 1.428571429; + color: #262626; + margin: 0; + padding: 0; + margin-bottom: 85px; + font-size: 16px; + overflow-y: scroll; +} +a { + color: #014e71; +} +a:hover { + color: #0274a8; +} +hr { + border-top-color: #0274a8; +} +strong, b { + font-weight: 500; +} +h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { + font-weight: 400; + -webkit-font-smoothing: antialiased; + font-family: 'Roboto Condensed', Arial, sans-serif; +} +h3,.h3 { + font-size: 32px; + margin-bottom: 14px; +} +code { + color: #0274a8; + background-color: #efefef; + padding: 2px 10px; + border-radius: 0; +} + +.padding-5 { padding: 5px !important; } +.padding-10 { padding: 10px !important; } +.padding-15 { padding: 15px !important; } +.padding-20 { padding: 20px !important; } +.padding-25 { padding: 25px !important; } +.padding-30 { padding: 30px !important; } + +.padding-t-5 { padding-top: 5px !important; } +.padding-t-10 { padding-top: 10px !important; } +.padding-t-15 { padding-top: 15px !important; } +.padding-t-20 { padding-top: 20px !important; } +.padding-t-25 { padding-top: 25px !important; } +.padding-t-30 { padding-top: 30px !important; } + +.padding-b-5 { padding-bottom: 5px !important; } +.padding-b-10 { padding-bottom: 10px !important; } +.padding-b-15 { padding-bottom: 15px !important; } +.padding-b-20 { padding-bottom: 20px !important; } +.padding-b-25 { padding-bottom: 25px !important; } +.padding-b-30 { padding-bottom: 30px !important; } + +.padding-l-5 { padding-left: 5px !important; } +.padding-l-10 { padding-left: 10px !important; } +.padding-l-15 { padding-left: 15px !important; } +.padding-l-20 { padding-left: 20px !important; } +.padding-l-25 { padding-left: 25px !important; } +.padding-l-30 { padding-left: 30px !important; } + +.padding-r-5 { padding-right: 5px !important; } +.padding-r-10 { padding-right: 10px !important; } +.padding-r-15 { padding-right: 15px !important; } +.padding-r-20 { padding-right: 20px !important; } +.padding-r-25 { padding-right: 25px !important; } +.padding-r-30 { padding-right: 30px !important; } + +.push-up-5 { margin-top: 5px !important; } +.push-up-10 { margin-top: 10px !important; } +.push-up-15 { margin-top: 15px !important; } +.push-up-20 { margin-top: 20px !important; } +.push-up-25 { margin-top: 25px !important; } +.push-up-30 { margin-top: 30px !important; } + +.push-down-5 { margin-bottom: 5px !important; } +.push-down-10 { margin-bottom: 10px !important; } +.push-down-15 { margin-bottom: 15px !important; } +.push-down-20 { margin-bottom: 20px !important; } +.push-down-25 { margin-bottom: 25px !important; } +.push-down-30 { margin-bottom: 30px !important; } + +/* Footer */ +footer{ + position: fixed; + bottom: 0; + width: 100%; + background-color: #262626; + font-size: 14px; + color: #efefef; + z-index: 9999; +} +footer a { + color: #6ED5EE; +} +footer > div{ + color: #fff; + margin: 10px auto; + text-align: center; +} + +/* Wrappers */ +#wrapper { + width: 100%; +} +#page-wrapper { + padding: 0 15px; + min-height: 568px; +} +@media (min-width: 992px) { + #page-wrapper { + position: inherit; + margin: 0 0 0 225px; + padding: 60px 30px 0 30px; + } +} +#loading{ + font-size: 2em; +} + +/* Sidebar */ +.nav-side-menu { + overflow: auto; + font-size: 16px; + font-weight: 200; + background-color: rgba(0, 0, 0, .8); + position: fixed; + top: 0; + width: 225px; + height: 100%; + color: #fff; + z-index: 1000; +} +.nav-side-menu .brand { + background-color: #014e71; + line-height: 50px; + display: block; + text-align: center; + font-size: 20px; + color: #fff; + font-weight: 400; +} +.nav-side-menu .brand a { + color: #fff; + text-decoration: none; +} + +.nav-side-menu .toggle-btn { + display: none; +} +.nav-side-menu ul, +.nav-side-menu li { + list-style: none; + padding: 0; + margin: 0; + line-height: 35px; + cursor: pointer; +} +.nav-side-menu ul :not(collapsed) .arrow:before, +.nav-side-menu li :not(collapsed) .arrow:before { + font-family: FontAwesome; + content: "\f078"; + display: inline-block; + padding-left: 10px; + padding-right: 10px; + vertical-align: middle; + float: right; +} +.nav-side-menu ul .active, +.nav-side-menu li .active { + border-left: 3px solid #03a9f4; + background-color: #0274a8; +} +.nav-side-menu ul .active a, +.nav-side-menu li .active a { + color: #fff; +} +.nav-side-menu ul .sub-menu li.active, +.nav-side-menu li .sub-menu li.active { + color: #03a9f4; +} +.nav-side-menu ul .sub-menu li a, +.nav-side-menu li .sub-menu li a { + display: inline-block; + width: 85%; +} +.nav-side-menu ul .sub-menu li.active a, +.nav-side-menu li .sub-menu li.active a { + color: #03a9f4; +} +.nav-side-menu ul .sub-menu li, +.nav-side-menu li .sub-menu li { + background-color: #181c20; + border: none; + line-height: 28px; + border-bottom: 1px solid #23282e; + margin-left: 0; +} +.nav-side-menu ul .sub-menu li:hover, +.nav-side-menu li .sub-menu li:hover { + background-color: #020203; +} +.nav-side-menu ul .sub-menu li:before, +.nav-side-menu li .sub-menu li:before { + font-family: FontAwesome; + content: "\f105"; + display: inline-block; + padding-left: 10px; + padding-right: 10px; + vertical-align: middle; +} +.nav-side-menu li { + padding-left: 0; + border-left: 3px solid #2e353d; + border-bottom: 1px solid #23282e; +} +.nav-side-menu li a { + text-decoration: none; + color: #fff; + display: block; + width: 100%; + padding: 2px 0; +} +.nav-side-menu li a i { + padding-left: 10px; + width: 20px; + padding-right: 20px; +} +.nav-side-menu li:hover { + border-left: 3px solid #03a9f4; + background-color: #0274a8; + -webkit-transition: all 1s ease; + -moz-transition: all 1s ease; + -o-transition: all 1s ease; + -ms-transition: all 1s ease; + transition: all 1s ease; +} +.nav-side-menu li:hover a { + color: #fff; +} +@media (max-width: 991px) { + .nav-side-menu { + position: relative; + width: 100%; + } + .nav-side-menu .toggle-btn { + display: block; + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; + z-index: 10 !important; + padding: 3px; + background-color: #ffffff; + color: #000; + width: 40px; + text-align: center; + border-radius: 3px; + } + .brand { + text-align: left !important; + font-size: 22px; + padding-left: 20px; + line-height: 58px !important; + } +} +@media (min-width: 992px) { + .nav-side-menu .menu-list .menu-content { + display: block; + } +} + +/* Top Bar */ +#top-bar { + background-color: #0274a8; + line-height: 50px; + display: block; + font-size: 17px; + color: #fff; + padding: 0 15px; + z-index: 1000; +} +#top-bar div { + display: inline-block; + padding: 0 15px; +} +#top-bar div strong { + font-weight: 700; +} +#top-bar #statsUpdated { + font-weight: normal; +} +@media (max-width: 1199px) { + #top-bar #statsUpdated .text { + display: none; + } +} +@media (min-width: 992px) { + #top-bar { + overflow: auto; + position: fixed; + top: 0; + left: 225px; + right: 0; + padding: 0 15px; + } +} +@media (max-width: 767px) { + #top-bar { + display: none; + } +} + +/* Bootstrap Buttons */ +.btn-default { + color: #ffffff; + background-color: #014e71; + border-color: #014e71; +} +.btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default { + color: #ffffff; + background-color: #013d58; + border-color: #013d58; +} +.btn-default.disabled,.btn-default[disabled], fieldset[disabled] .btn-default, .btn-default.disabled:hover, .btn-default[disabled]:hover, fieldset[disabled] .btn-default:hover, .btn-default.disabled:focus, .btn-default[disabled]:focus, fieldset[disabled] .btn-default:focus, .btn-default.disabled:active, .btn-default[disabled]:active, fieldset[disabled] .btn-default:active, .btn-default.disabled.active, .btn-default[disabled].active, fieldset[disabled] .btn-default.active { + background-color: #014e71; + border-color: #014e71; +} +.btn-default .badge { + color: #014e71; + background-color: #ffffff; +} +.btn-primary { + color: #ffffff; + background-color: #03a678; + border-color: #03a678; +} +.btn-primary:hover,.btn-primary:focus, .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #027454; + border-color: #026a4d; +} +.btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, .btn-primary[disabled], fieldset[disabled] .btn-primary, .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, .btn-primary.disabled:active, .btn-primary[disabled]:active, fieldset[disabled] .btn-primary:active, .btn-primary.disabled.active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary.active { + background-color: #03a678; + border-color: #03a678; +} +.btn-primary .badge { + color: #03a678; + background-color: #ffffff; +} + +/* Bootstrap Form Controls */ +.input-group-addon { + color: #014e71; +} + +/* Bootstrap Tables */ +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #000000; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #000000; +} +.table > thead > tr > th { + color: #014e71; + border-bottom-color: #014e71; + font-weight: 400; +} +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: rgba(151, 222, 255, 0.3); +} +.table > tbody > tr:hover td, +.table > tbody > tr:hover th { + background-color: rgba(3, 169, 244, 0.3); +} +.table > tbody > tr > td { + border-top-color: #c9e0e9; +} +table th.sort:hover{ + cursor: pointer; +} + +/* jQuery sparkline tooltip */ +.jqstooltip { + border: none !important; + background: rgba(0, 0, 0, 0.8) !important; + border-radius: 2px !important; + margin-top: -20px !important; + /* Override Bootstrap defaults to fix jQuery.Sparkline tooltips appearance */ + -webkit-box-sizing: content-box !important; + -moz-box-sizing: content-box; + box-sizing: content-box !important; +} +.jqstooltip .jqsfield { + color: #ccc; + font-size: 13px !important; +} +.jqstooltip .jqsfield b { + color: #fff; +} + +/* Content Card */ +.card { + display: inline-block; + position: relative; + width: 100%; + margin: 0; + padding: 0; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); + border-radius: 3px; + color: rgba(0, 0, 0, 0.87); + background: #fff; +} + +.card table, .card .table { + margin-bottom: 0; +} + +/* Info box */ +.infoBox { + background-color: #03a9f4; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); + height: 90px; + cursor: default; + position: relative; + overflow: hidden; + margin-bottom: 30px; + border-radius: 3px; +} +@media (max-width: 767px){ + .infoBox{ + margin-bottom: 15px; + } +} +.infoBox .icon { + position: absolute; + right: 10px; + top: 10px; + text-align: center; + font-size: 60px; + line-height: 60px; + width: 75px; + text-align: center; + color: rgba(255, 255, 255, 0.3); +} +.infoBox .content { + display: inline-block; + padding: 7px 16px; +} +.infoBox .content .text { + font-size: 13px; + margin-top: 11px; + color: #fff; + text-transform: uppercase; +} +.infoBox .content .value { + font-size: 22px; + font-weight: 700; + margin-top: -4px; + color: #fff; + text-shadow: 1px 1px 1px #000; +} +.infoBox .content .value .smallText { + font-size: 16px; + font-weight: normal; +} +#marketInfos .infoBox { + height: 100px; +} +#marketInfos .infoBox .source { + font-size: 13px; + font-style: italic; + color: #fff; +} +#marketInfos .infoBox .source a, #marketInfos .infoBox .source a:hover { + color: #fff; +} + + +/* Hash info box */ +.hashInfo { + background-color: #014e71; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); + cursor: default; + padding: 15px; + border-radius: 3px; + color: #fff; +/* margin-bottom: 30px; +} +@media (max-width: 767px){ + .hashInfo { + margin-bottom: 15px; + } */ +} +.hashInfo .text { + font-size: 13px; + color: #fff; + text-transform: uppercase; +} +.hashInfo .content { + overflow: hidden; +} +.hashInfo .content .value { + float: left; + font-family: 'Inconsolata', monospace; + font-size: 16px; + font-weight: 800; + color: #fff; + padding-right: 8px; +} +.hashInfo .content .value a { + color: #fff; +} +.hashInfo .content .time { + float: left; + font-size: 15px; + font-weight: normal; +} + +/* Slush payment info box */ +.slushInfo { + background-color: #014e71; + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14); + cursor: default; + padding: 15px; + border-radius: 3px; + color: #fff; + margin-bottom: 30px; +} +.slushInfo .text { + font-size: 13px; + color: #fff; + text-transform: uppercase; +} +@media (max-width: 767px){ + .slushInfo { + margin-bottom: 15px; + } +} +.slushInfo .content .value { + float: left; + font-size: 16px; + font-weight: 400; + color: #fff; + padding: 8px 8px 0 0; +} +.slushInfo .content .value a { + color: #fff; +} + +/* Pool Statistics */ +.stats > div:not(#addressError) { + color: #262626; + padding: 10px 0px; +} +.stats > div:not(#addressError).marketFooter { + padding: 5px 0; +} +.stats > div:not(#addressError) > i.fa { + color: #0274a8; + font-size: 21px; + width: 30px; + text-align: center; +} +.stats > div:not(#addressError) > span:not(.input-group-btn):first-of-type { + font-weight: 500; + padding: 0 2px; + color: #0274a8; +} + +/* Pool and Worker Charts */ +.poolChart, .marketChart, .userChart { + display: none; +} +.poolChart .chart, .marketChart .chart, .userChart .chart { + height: 140px; +} +.poolChart, .marketChart { + margin-bottom: 30px; +} +@media (max-width: 767px){ + .poolChart, .marketChart { + margin-bottom: 15px; + } +} +.poolChart h4, +.marketChart h4, +.userChart h4 { + text-align: center; + font-size: 21px; +} +.chart a.chart-style { + display: none; /* never displayed; we just use it to store the following colors: */ + background-color: rgba(3, 169, 244, .4); + border-color: #03a9f4; + width: 1px; /* controls the chart border width (must be specified in px) */ +} + +/* Market Rates */ +#marketStats { + margin-top: 20px; +} +#marketUpdate { + display: none; +} + +/* Mining Profit Calculator */ +#miningProfitCalc{ + margin: 35px 0; +} +#miningProfitCalc .input-group-addon{ + padding: 6px; +} +#miningProfitCalc .input-group-addon, +#miningProfitCalc .input-group-btn .btn, +#miningProfitCalc .input-group .form-control{ + height:45px; +} +#calcHashDropdown{ + border-radius: 0; + border-left: 0; + border-right: 0; +} +#calcHashHolder{ + width: 590px; + max-width: 100%; +} +#calcHashRate{ + z-index: inherit; + font-family: 'Inconsolata', monospace; +} +#calcHashAmount{ + font-family: 'Inconsolata', monospace; + display: inline-block; + text-align: center; + vertical-align: middle; + overflow: hidden; +} +#calcHashPeriod{ + font-family: 'Inconsolata', monospace; + display: inline-block; + width: 60px; + vertical-align: middle; +} +#calcHashResultsHolder{ + min-width: 170px; + max-width: 170px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +@media (min-width: 768px) { + #calcHashResultsHolder{ + min-width: 225px; + max-width: 225px; + } +} + +/* Mining Ports */ +#miningPorts tr > td { + vertical-align: middle; + font-size: 0.95em; +} +#miningPorts table .col1, +#miningPorts table .col2 { + text-align: center; +} +@media (min-width: 768px) { + #miningPorts table th { + white-space: nowrap; + } + #miningPorts table .col1 { + width:60px; + } + #miningPorts table .col2 { + width:160px; + text-align: center; + } +} + +/* Mining Apps */ +#miningApps .table > thead > tr > th { + white-space: nowrap; +} +#miningApps_rows > tr:first-child > td { + padding-top: 10px; +} +#miningApps_rows > tr > td { + border-top: none; +} +#miningApps_rows .appInfo td { + padding: 10px; + margin: 0; +} +#miningApps_rows .appConfig td { + padding: 0 10px; +} +#miningApps_rows tr:hover, +#miningApps_rows tr:hover > td, +#miningApps_rows tr:hover > th { + background-color: transparent; +} +#miningApps .btn { + width: 100%; +} +#miningApps .miningAppTitle { + font-weight: bold; +} +#miningApps .exampleAddress, +#miningApps .exampleWorkerName { + font-style: italic; +} + +/* Worker Statistics */ +#yourStatsInput { + z-index: inherit; + font-family: 'Inconsolata', monospace; +} +#yourAddressDisplay { + word-wrap: break-word; +} +#yourAddressDisplay > span { + font-family: 'Inconsolata', monospace; +} +#lookUp > span:nth-child(2) { + display: none; +} +.yourStats, +.yourWorkers { + display: none; +} +#yourAddressDisplay { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + font-family: 'Inconsolata', monospace; + font-size: 0.9em; +} +#addressError { + color: red; +} +#workersReport td { + vertical-align: middle; +} +#workersReport .status-ok{ + color: #17a600; +} +#workersReport .status-error{ + color: #a60000; +} +#workersReport table .col1, +#workersReport table .col3, +#workersReport table .col4, +#workersReport table .col5 { + text-align: center; +} +@media (min-width: 768px) { + #workersReport table th { + white-space: nowrap; + } + #workersReport table .col1 { + width:80px; + } + #workersReport table .col3, + #workersReport table .col4, + #workersReport table .col5, + #workersReport table .col6 { + width:120px; + } + #workersReport table .col7 { + width: 210px; + } + #workersReport table .col8 { + width: 210px; + } +} +#workerPayments td { + vertical-align: middle; + font-family: 'Inconsolata', monospace; + font-size: 0.95em; +} +#workerPayments table .col3, +#workerPayments table .col4 { + text-align: center; +} +#workerPayments table .summary { + font-weight: 700; +} +@media (min-width: 768px) { + #workerPayments table { + table-layout:fixed; + } + #workerPayments table th { + white-space: nowrap; + } + #workerPayments table .col1 { + width:190px; + } + #workerPayments table .col2 { + overflow: hidden; + text-overflow: ellipsis; + } + #workerPayments table .col3 { + width:130px; + } + #workerPayments table .col4 { + width: 70px; + } +} + +/* Pool Blocks */ +#blocksStats { + margin-top: 20px; +} +#blocksStats .infoBox { + margin-bottom: 15px; +} +#blocksStats .luckGood { + color: #5eff5e; +} +#blocksStats .luckBad { + color: red; +} +#blocksStats .luckMid { + color: #FFF500; +} +#blocksReport tr > td { + vertical-align: middle; + font-family: 'Inconsolata', monospace; + font-size: 0.95em; +} +#blocksReport table .col2, +#blocksReport table .col3, +#blocksReport table .col4, +#blocksReport table .col6, +#blocksReport table .col7 { + text-align: center; +} +@media (min-width: 768px) { + #blocksReport table { + table-layout:fixed; + } + #blocksReport table th { + white-space: nowrap; + } + #blocksReport table .col1 { + width:190px; + } + #blocksReport table .col2 { + width:80px; + } + #blocksReport table .col3 { + width:90px; + } + #blocksReport table .col4 { + width:120px; + } + #blocksReport table .col5 { + overflow: hidden; + text-overflow: ellipsis; + } + #blocksReport table .col6 { + width: 55px; + } + #blocksReport table .col7 { + width: 80px; + } +} +#blocksReport .pending td{ + background-color:rgba(255, 255, 255, .3); +} +#blocksReport .unlocked td{ + background-color:rgba(0, 255, 0, .1); +} +#blocksReport .orphaned td{ + background-color:rgba(255, 0, 0, .1); +} +#blocksReport .luckGood, +#blocksReport .unlocked .col7 { + color: #17a600; +} +#blocksReport .luckBad, +#blocksReport .orphaned .col7 { + color: #a60000; +} +#blocksReport .luckMid { + color: #df9d00; +} + +/* Payments */ +#paymentsStats { + margin-top: 20px; +} +#paymentsStats .infoBox { + margin-bottom: 15px; +} +#paymentsReport td { + vertical-align: middle; + font-family: 'Inconsolata', monospace; + font-size: 0.95em; +} +#paymentsReport table .col3, +#paymentsReport table .col4, +#paymentsReport table .col5, +#paymentsReport table .col6 { + text-align: center; +} +@media (min-width: 768px) { + #paymentsReport table { + table-layout:fixed; + } + #paymentsReport table th { + white-space: nowrap; + } + #paymentsReport table .col1 { + width:190px; + } + #paymentsReport table .col2 { + overflow: hidden; + text-overflow: ellipsis; + } + #paymentsReport table .col3 { + width:130px; + } + #paymentsReport table .col4 { + width:110px; + } + #paymentsReport table .col5 { + width: 70px; + } + #paymentsReport table .col6 { + width: 70px; + } +} + +/* Top 10 miners */ +#top10miners td { + vertical-align: middle; +} +#top10miners table .col1, +#top10miners table .col3, +#top10miners table .col4, +#top10miners table .col5 { + text-align: center; +} +@media (min-width: 768px) { + #top10miners table th { + white-space: nowrap; + } + #top10miners table .col1 { + width:80px; + } + #top10miners table .col3 { + width:150px; + } + #top10miners table .col4 { + width: 210px; + } + #top10miners table .col5 { + width: 210px; + } +} + +/* Language selector */ +#langSelector { + float: right; + padding-right: 0 !important; +} + +#langSelector select { + display: inline-block; + width: auto; + height: 32px; + padding: 6px 10px; +} + +#mLangSelector { + display: none; + padding: 10px; +} +@media (max-width: 767px) { + #mLangSelector { + display: block; + } +} \ No newline at end of file