first commit

This commit is contained in:
muscleman 2022-01-15 11:31:38 -06:00
commit 29ba08020f
128 changed files with 19305 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
package-lock.json
node_modules/
build/
.idea/
logs/
website/
*.swp

23
Dockerfile Normal file
View file

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

339
LICENSE Normal file
View file

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
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.

800
README.md Normal file
View file

@ -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 <address> param with +<difficulty> postfix */
"fixedDiff": {
"enabled": true,
"separator": "+", // Character separator between <address> and <difficulty>
},
/* Set payment ID on miner client side by passing <address>.<paymentID> */
"paymentId": {
"addressSeparator": ".", // Character separator between <address> and <paymentID>
"validation": true // Refuse login if non alphanumeric characters in <paymentID>
"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/<YourPoolFacebook";
/* Market stat display params from https://www.cryptonator.com/widget */
var marketCurrencies = ["{symbol}-BTC", "{symbol}-USD", "{symbol}-EUR", "{symbol}-CAD"];
/* Used for front-end block links. */
var blockchainExplorer = "http://chainradar.com/{symbol}/block/{id}";
/* Used by front-end transaction links. */
var transactionExplorer = "http://chainradar.com/{symbol}/transaction/{id}";
/* Any custom CSS theme for pool frontend */
var themeCss = "themes/light.css";
/* Default language */
var defaultLang = 'en';
```
#### 5) Customize your website
The following files are included so that you can customize your pool website without having to make significant changes
to `index.html` or other front-end files thus reducing the difficulty of merging updates with your own changes:
* `custom.css` for creating your own pool style
* `custom.js` for changing the functionality of your pool website
Then simply serve the files via nginx, Apache, Google Drive, or anything that can host static content.
#### SSL
You can configure the API to be accessible via SSL using various methods. Find an example for nginx below:
* Using SSL api in `config.json`:
By using this you will need to update your `api` variable in the `website_example/config.js`. For example:
`var api = "https://poolhost:8119";`
* Inside your SSL Listener, add the following:
``` javascript
location ~ ^/api/(.*) {
proxy_pass http://127.0.0.1:8117/$1$is_args$args;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```
By adding this you will need to update your `api` variable in the `website_example/config.js` to include the /api. For example:
`var api = "http://poolhost/api";`
You no longer need to include the port in the variable because of the proxy connection.
* Using his own subdomain, for example `api.poolhost.com`:
```bash
server {
server_name api.poolhost.com
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /your/ssl/certificate;
ssl_certificate_key /your/ssl/certificate_key;
location / {
more_set_headers 'Access-Control-Allow-Origin: *';
proxy_pass http://127.0.01:8117;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache_bypass $http_upgrade;
}
}
```
By adding this you will need to update your `api` variable in the `website_example/config.js`. For example:
`var api = "//api.poolhost.com";`
You no longer need to include the port in the variable because of the proxy connection.
#### Upgrading
When updating to the latest code its important to not only `git pull` the latest from this repo, but to also update
the Node.js modules, and any config files that may have been changed.
* Inside your pool directory (where the init.js script is) do `git pull` to get the latest code.
* Remove the dependencies by deleting the `node_modules` directory with `rm -r node_modules`.
* Run `npm update` to force updating/reinstalling of the dependencies.
* Compare your `config.json` to the latest example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes.
### JSON-RPC Commands from CLI
Documentation for JSON-RPC commands can be found here:
* Daemon https://wiki.bytecoin.org/wiki/JSON_RPC_API
* Wallet https://wiki.bytecoin.org/wiki/Wallet_JSON_RPC_API
Curl can be used to use the JSON-RPC commands from command-line. Here is an example of calling `getblockheaderbyheight` for block 100:
```bash
curl 127.0.0.1:18081/json_rpc -d '{"method":"getblockheaderbyheight","params":{"height":100}}'
```
### Monitoring Your Pool
* To inspect and make changes to redis I suggest using [redis-commander](https://github.com/joeferner/redis-commander)
* To monitor server load for CPU, Network, IO, etc - I suggest using [Netdata](https://github.com/firehol/netdata)
* To keep your pool node script running in background, logging to file, and automatically restarting if it crashes - I suggest using [forever](https://github.com/nodejitsu/forever) or [PM2](https://github.com/Unitech/pm2)
Donations
---------
Thanks for supporting my works on this project! If you want to make a donation to [muscleman](https://github.com/muscleman/), the developper of this project, you can send any amount of your choice to one of theses addresses:
* Bitcoin (BTC): `34GDVuVbuxyYdR8bPZ7g6r12AhPPCrNfXt`
* Ethereum (ETH): `0xce3c0e8ee19173947ae5c4cc9796e84f9b613b9c`
* Litecoin (LTC): `LW169WygGDMBN1PGSr8kNbrFBx94emGWfB`
* Monero (XMR): `87Xk1L9u8ETHiWTTxBLSeQ8GbbYNMq8utjPb6G6BJDLwUf4jBCrdDeVTsUtmisQrAeSMX4Umod7LEatpEiMtEh1AFkc9A5W`
* Graft (GRFT): `GMPHYf5KRkcAyik7Jw9oHRfJtUdw2Kj5f4VTFJ25AaFVYxofetir8Cnh7S76Q854oMXzwaguL8p5KEz1tm3rn1SA6r6p9dMjuV81yqXCgi`
* Haven (XHV): `hvs1erNkyabFQZGfGh6eNL6myrcAcmHiEjGm1Mu64iaqBXRmqn1cNpm5ZCYZguBvTmCmzuDXfXNBNSdh9CWz1p2X6sopkUoAMg`
* Masari (MSR): `5t5mEm254JNJ9HqRjY9vCiTE8aZALHX3v8TqhyQ3TTF9VHKZQXkRYjPDweT9kK4rJw7dDLtZXGjav2z9y24vXCdRc3DY4daikoNTeK1v4e`
Credits
---------
* [fancoder](//github.com/fancoder) - Developper on cryptonote-universal-pool project from which current project is forked.
* dvandal (//github.com/dvandal) - Developer of cryptonote-nodejs-pool software
* Musclesonvacation (//github.com/muscleman) - Current developer for pool software
License
-------
Released under the GNU General Public License v2
http://www.gnu.org/licenses/gpl-2.0.html

318
config-test.json Normal file
View file

@ -0,0 +1,318 @@
{
"poolHost": "zano.somewhere.com",
"coin": "Zano",
"symbol": "ZANO",
"coinUnits": 1000000000000,
"coinDecimalPlaces": 4,
"coinDifficultyTarget": 120,
"blockchainExplorer": "https://testnet-explorer.zano.org/block/{id}",
"transactionExplorer": "https://testnet-explorer.zano.org/transaction/{id}",
"daemonType": "default",
"cnAlgorithm": "ethash",
"cnVariant": 2,
"cnBlobType": 0,
"isRandomX": false,
"includeHeight": false,
"previousOffset": 7,
"offset": 2,
"isCryptonight": false,
"reward": 1000000000000,
"logging": {
"files": {
"level": "info",
"directory": "logs",
"flushInterval": 5,
"prefix": "Zano"
},
"console": {
"level": "info",
"colors": true
}
},
"childPools": [],
"poolServer": {
"enabled": true,
"mergedMining": false,
"clusterForks": 3,
"poolAddress": "** pool address **",
"intAddressPrefix": null,
"blockRefreshInterval": 1000,
"minerTimeout": 900,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"ports": [
{
"port": 3336,
"difficulty": 50000000,
"desc": "Low end hardware"
},
{
"port": 3337,
"difficulty": 600000000,
"desc": "middle end hardware"
},
{
"port": 3338,
"difficulty": 5000000000,
"desc": "Nicehash, MRR"
}
],
"varDiff": {
"minDiff": 50000000,
"maxDiff": 5000000000,
"targetTime": 45,
"retargetTime": 60,
"variancePercent": 5,
"maxJump": 20
},
"paymentId": {
"addressSeparator": "+"
},
"fixedDiff": {
"enabled": true,
"addressSeparator": "."
},
"shareTrust": {
"enabled": true,
"min": 10,
"stepDown": 3,
"threshold": 10,
"penalty": 30
},
"banning": {
"enabled": true,
"time": 600,
"invalidPercent": 25,
"checkThreshold": 30
},
"slushMining": {
"enabled": false,
"weight": 300,
"blockTime": 60,
"lastBlockCheckRate": 1
}
},
"payments": {
"enabled": true,
"interval": 900,
"maxAddresses": 5,
"mixin": 1,
"priority": 0,
"transferFee": 10000000000,
"dynamicTransferFee": true,
"minerPayFee" : true,
"minPayment": 1000000000000,
"maxPayment": 100000000000000,
"maxTransactionAmount": 100000000000000,
"denomination": 1000000000000
},
"blockUnlocker": {
"enabled": true,
"interval": 60,
"depth": 10,
"poolFee": 0.2,
"devDonation": 0.5,
"networkFee": 0.0
},
"api": {
"enabled": true,
"hashrateWindow": 600,
"updateInterval": 15,
"bindIp": "0.0.0.0",
"port": 2117,
"blocks": 30,
"payments": 30,
"password": "password",
"ssl": false,
"sslPort": 2119,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"trustProxyIP": true
},
"zmq": {
"enabled": false,
"host": "127.0.0.1",
"port": 39995
},
"daemon": {
"host": "10.0.0.13",
"port": 12111
},
"wallet": {
"host": "10.0.0.13",
"port": 39996
},
"redis": {
"host": "127.0.0.1",
"port": 6379,
"auth": null,
"db": 11,
"cleanupInterval": 15
},
"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": {
"enabled": false,
"fromAddress": "your@email.com",
"transport": "sendmail",
"sendmail": {
"path": "/usr/sbin/sendmail"
},
"smtp": {
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": {
"user": "username",
"pass": "password"
},
"tls": {
"rejectUnauthorized": false
}
},
"mailgun": {
"key": "your-private-key",
"domain": "mg.yourdomain"
}
},
"telegram": {
"enabled": false,
"botName": "",
"token": "",
"channel": "",
"channelStats": {
"enabled": false,
"interval": 30
},
"botCommands": {
"stats": "/stats",
"report": "/report",
"notify": "/notify",
"blocks": "/blocks"
}
},
"monitoring": {
"daemon": {
"checkInterval": 60,
"rpcMethod": "getblockcount"
},
"wallet": {
"checkInterval": 60,
"rpcMethod": "getbalance"
}
},
"prices": {
"source": "tradeogre",
"currency": "USD"
},
"charts": {
"pool": {
"hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"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": {
"hashrate": {
"enabled": true,
"updateInterval": 180,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"worker_hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 60,
"maximumPeriod": 86400
},
"payments": {
"enabled": true
}
},
"blocks": {
"enabled": true,
"days": 30
}
}
}

317
config.json Normal file
View file

@ -0,0 +1,317 @@
{
"poolHost": "zano.somewhere.com",
"coin": "Zano",
"symbol": "ZANO",
"coinUnits": 1000000000000,
"coinDecimalPlaces": 4,
"coinDifficultyTarget": 120,
"blockchainExplorer": "https://explorer.zano.org/block/{id}",
"transactionExplorer": "https://explorer.zano.org/transaction/{id}",
"daemonType": "default",
"cnAlgorithm": "ethash",
"cnVariant": 2,
"cnBlobType": 0,
"isRandomX": false,
"includeHeight": false,
"previousOffset": 7,
"offset": 2,
"isCryptonight": false,
"reward": 1000000000000,
"logging": {
"files": {
"level": "info",
"directory": "logs",
"flushInterval": 5,
"prefix": "Zano"
},
"console": {
"level": "info",
"colors": true
}
},
"childPools": [],
"poolServer": {
"enabled": true,
"mergedMining": false,
"clusterForks": 3,
"poolAddress": "** pool wallet **",
"intAddressPrefix": null,
"blockRefreshInterval": 1000,
"minerTimeout": 900,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"ports": [
{
"port": 3336,
"difficulty": 50000000,
"desc": "Low end hardware"
},
{
"port": 3337,
"difficulty": 600000000,
"desc": "middle end hardware"
},
{
"port": 3338,
"difficulty": 5000000000,
"desc": "Nicehash, MRR"
}
],
"varDiff": {
"minDiff": 50000000,
"maxDiff": 5000000000,
"targetTime": 45,
"retargetTime": 60,
"variancePercent": 5,
"maxJump": 20
},
"paymentId": {
"addressSeparator": "+"
},
"fixedDiff": {
"enabled": true,
"addressSeparator": "."
},
"shareTrust": {
"enabled": true,
"min": 10,
"stepDown": 3,
"threshold": 10,
"penalty": 30
},
"banning": {
"enabled": true,
"time": 600,
"invalidPercent": 25,
"checkThreshold": 30
},
"slushMining": {
"enabled": false,
"weight": 300,
"blockTime": 60,
"lastBlockCheckRate": 1
}
},
"payments": {
"enabled": true,
"interval": 900,
"maxAddresses": 5,
"mixin": 1,
"priority": 0,
"transferFee": 10000000000,
"dynamicTransferFee": true,
"minerPayFee" : true,
"minPayment": 1000000000000,
"maxPayment": 100000000000000,
"maxTransactionAmount": 100000000000000,
"denomination": 1000000000000
},
"blockUnlocker": {
"enabled": true,
"interval": 60,
"depth": 10,
"poolFee": 0.2,
"devDonation": 0.5,
"networkFee": 0.0
},
"api": {
"enabled": true,
"hashrateWindow": 600,
"updateInterval": 15,
"bindIp": "0.0.0.0",
"port": 2117,
"blocks": 30,
"payments": 30,
"password": "password",
"ssl": false,
"sslPort": 2119,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"trustProxyIP": true
},
"zmq": {
"enabled": false,
"host": "127.0.0.1",
"port": 39995
},
"daemon": {
"host": "127.0.0.1",
"port": 11211
},
"wallet": {
"host": "127.0.0.1",
"port": 39996
},
"redis": {
"host": "127.0.0.1",
"port": 6379,
"auth": null,
"db": 11,
"cleanupInterval": 15
},
"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": {
"enabled": false,
"fromAddress": "your@email.com",
"transport": "sendmail",
"sendmail": {
"path": "/usr/sbin/sendmail"
},
"smtp": {
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": {
"user": "username",
"pass": "password"
},
"tls": {
"rejectUnauthorized": false
}
},
"mailgun": {
"key": "your-private-key",
"domain": "mg.yourdomain"
}
},
"telegram": {
"enabled": false,
"botName": "",
"token": "",
"channel": "",
"channelStats": {
"enabled": false,
"interval": 30
},
"botCommands": {
"stats": "/stats",
"report": "/report",
"notify": "/notify",
"blocks": "/blocks"
}
},
"monitoring": {
"daemon": {
"checkInterval": 60,
"rpcMethod": "getblockcount"
},
"wallet": {
"checkInterval": 60,
"rpcMethod": "getbalance"
}
},
"prices": {
"source": "tradeogre",
"currency": "USD"
},
"charts": {
"pool": {
"hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"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": {
"hashrate": {
"enabled": true,
"updateInterval": 180,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"worker_hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 60,
"maximumPeriod": 86400
},
"payments": {
"enabled": true
}
},
"blocks": {
"enabled": true,
"days": 30
}
}
}

View file

@ -0,0 +1,17 @@
[Unit]
Description=Coin Daemon
After=network.target
[Service]
Type=forking
GuessMainPID=no
Restart=always
# Change this line to set the location of your coin daemon
ExecStart=/path/to/coin/coind --rpc-bind-ip 0.0.0.0 --confirm-external-bind --detach
# Change this line to the user that will run your coin daemon
User=pool-user
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,16 @@
[Unit]
Description=Wallet RPC Daemon
After=network.target
[Service]
Type=simple
Restart=always
# Change this line to set the location of your coin wallet RPC daemon and set the RPC port you want to use
ExecStart=/path/to/coin/wallet-rpc --rpc-bind-ip=127.0.0.1 --rpc-bind-port=5432 --wallet=/path/to/wallet --password=your.wallet.password!
# Change this line to the user that will run your wallet RPC daemon
User=pool-user
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=Mining Pool Service
After=network.target
[Service]
Type=simple
Restart=always
SyslogIdentifier=cryptonote-nodejs-pool
ExecStart=/usr/bin/node init.js
# Change to the location of cryptonote-node-js-pool
WorkingDirectory=/path/to/your/cryptonote-nodejs-pool/pool
# Set user and group that will run the pool
User=pool-user
Group=pool-user
[Install]
WantedBy=multi-user.target

7
deployment/logrotate Normal file
View file

@ -0,0 +1,7 @@
/path/to/pool/logs/*.log {
daily
rotate 7
missingok
compress
create
}

View file

@ -0,0 +1,102 @@
# HTTP — redirect all traffic to HTTPS
server {
listen 80;
# listen [::]:80 ipv6only=on;
server_name api.arqma.com;
return 301 https://$host$request_uri;
}
# HTTPS — proxy all requests to the pool app
server {
# Enable HTTP/2
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your.pool.host;
# Set files location
root /var/www/multicoin;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Set API proxy
location ~ ^/apiMerged/(.*) {
proxy_pass http://127.0.0.1:8217/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged1/(.*) {
proxy_pass http://127.0.0.1:8317/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged2/(.*) {
proxy_pass http://127.0.0.1:8417/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged3/(.*) {
proxy_pass http://127.0.0.1:8517/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged4/(.*) {
proxy_pass http://127.0.0.1:8617/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged5/(.*) {
proxy_pass http://127.0.0.1:8717/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged6/(.*) {
proxy_pass http://127.0.0.1:8817/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/apiMerged7/(.*) {
proxy_pass http://127.0.0.1:8917/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
ssl_certificate /etc/letsencrypt/live/your.pool.host/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/your.pool.host/privkey.pem; # managed by Certbot
}

View file

@ -0,0 +1,39 @@
# HTTP — redirect all traffic to HTTPS
server {
listen 80;
listen [::]:80 ipv6only=on;
server_name YOURDOMAIN;
return 301 https://$host$request_uri;
}
# HTTPS — proxy all requests to the pool app
server {
# Enable HTTP/2
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name YOURDOMAIN;
# Use the Lets Encrypt certificates
ssl_certificate /etc/letsencrypt/live/YOURDOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Set files location
root /path/to/pool/website/;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Set API proxy
location ~ ^/api/(.*) {
proxy_pass http://127.0.0.1:8117/$1$is_args$args;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View file

@ -0,0 +1,213 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>BlockHashMining.com</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<style>
@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;
font-size: 16px;
overflow-y: scroll;
}
#coin { text-align: center; background-color: #fff; width: 100%; box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 5px 0px; margin-bottom: 30px; }
#coin a { display: block; }
#coin a:hover { text-decoration: none; }
#coin img { margin: 30px; max-width: 100%; max-height: 125px }
#coin .name { display: block; width: 100%; background-color: #024e72; color: #fff; font-size: 18px; font-weight: 700; padding: 6px 12px; text-align: center; }
#coin ul.stats { padding: 0; margin: 0; text-align: left; font-family: 'Inconsolata', monospace; }
#coin ul.stats strong { font-weight: 700; }
#coin ul.stats li { list-style: none; border-bottom: 1px solid #ececec; padding: 7px 15px; font-size: 15px; color: #333; }
</style>
</head>
<body>
<div class="container"><div class="row">
<div class="col-xs-12">
<br/>
<h1>Sample landing page!</h1>
<br/>
</div>
<div class="col-md-4 col-sm-6"><div id="coin">
<a href="https://graft.blockhashmining.com/">
<span class="name">Graft (GRFT)</span>
<ul class="stats">
<li><strong><span id="graft_poolMiners">...</span></strong> active miners</span></li>
<li><strong><span id="graft_poolHashrate">...</span></strong> pool hashrate</span></li>
<li><strong><span id="graft_networkHashrate">...</span></strong> network hashrate</span></li>
<li><strong><span id="graft_hashPower">...</span></strong> of network hashrate</span></li>
<li><strong><span id="graft_blocksFound">...</span></strong> blocks found</span></li>
<li><strong><span id="graft_algorithm">...</span></strong> algorithm</span></li>
</ul>
</a>
</div></div>
<div class="col-md-4 col-sm-6"><div id="coin">
<a href="https://haven.blockhashmining.com/">
<span class="name">Haven (XHV)</span>
<ul class="stats">
<li><strong><span id="haven_poolMiners">...</span></strong> active miners</span></li>
<li><strong><span id="haven_poolHashrate">...</span></strong> pool hashrate</span></li>
<li><strong><span id="haven_networkHashrate">...</span></strong> network hashrate</span></li>
<li><strong><span id="haven_hashPower">...</span></strong> of network hashrate</span></li>
<li><strong><span id="haven_blocksFound">...</span></strong> blocks found</span></li>
<li><strong><span id="haven_algorithm">...</span></strong> algorithm</span></li>
</ul>
</a>
</div></div>
<div class="col-md-4 col-sm-6"><div id="coin">
<a href="https://stellite.blockhashmining.com/">
<span class="name">Stellite (XTL)</span>
<ul class="stats">
<li><strong><span id="stellite_poolMiners">...</span></strong> active miners</span></li>
<li><strong><span id="stellite_poolHashrate">...</span></strong> pool hashrate</span></li>
<li><strong><span id="stellite_networkHashrate">...</span></strong> network hashrate</span></li>
<li><strong><span id="stellite_hashPower">...</span></strong> of network hashrate</span></li>
<li><strong><span id="stellite_blocksFound">...</span></strong> blocks found</span></li>
<li><strong><span id="stellite_algorithm">...</span></strong> algorithm</span></li>
</ul>
</a>
</div></div>
</div></div>
<br/>
<script>
/**
* Pool statistics
**/
// Get stats from pool API
function getPoolStats(poolID, poolURL) {
let apiURL = poolURL + '/stats';
$.get(apiURL, function(data){
if (!data) return ;
let poolHashrate = 'N/A';
let poolMiners = 'N/A';
let poolWorkers = 'N/A';
if (data.pool) {
poolHashrate = getReadableHashRate(data.pool.hashrate);
poolMiners = data.pool.miners || 0;
poolWorkers = data.pool.workers || 0;
}
let networkHashrate = 'N/A';
let networkDiff = 'N/A';
if (data.network) {
networkHashrate = getReadableHashRate(data.network.difficulty / data.config.coinDifficultyTarget);
networkDiff = data.network.difficulty;
}
let hashPower = 'N/A';
if (data.pool && data.network) {
hashPower = data.pool.hashrate / (data.network.difficulty / data.config.coinDifficultyTarget) * 100;
hashPower = hashPower.toFixed(2) + '%';
}
let blocksFound = data.pool.totalBlocks.toString();
let cnAlgorithm = data.config.cnAlgorithm || "cryptonight";
let cnVariant = data.config.cnVariant || 0;
if (cnAlgorithm == "cryptonight_light") {
if (cnVariant === 1) {
algorithm = 'Cryptonight Light (Aeon v7)';
} else if (cnVariant === 2) {
algorithm = 'Cryptonight Light (IPBC)';
} else {
algorithm = 'Cryptonight Light (Original)';
}
}
else if (cnAlgorithm == "cryptonight_heavy") {
if (cnVariant === 1) {
algorithm = 'Cryptonight Heavy (Haven)';
}
else {
algorithm = 'Cryptonight Heavy';
}
}
else {
if (cnVariant === 1) {
algorithm = 'Cryptonight (Monero v7)';
} else if (cnVariant === 3) {
algorithm = 'Cryptonight (Stellite v7)';
} else if (cnVariant === 4) {
algorithm = 'Cryptonight Fast (Masari)';
} else if (cnVariant === 8) {
algorithm = 'Cryptonight (Monero v8)';
} else {
algorithm = 'Cryptonight (Original)';
}
}
updateText(poolID + '_poolHashrate', poolHashrate);
updateText(poolID + '_poolMiners', poolMiners);
updateText(poolID + '_networkHashrate', networkHashrate);
updateText(poolID + '_hashPower', hashPower);
updateText(poolID + '_blocksFound', blocksFound);
updateText(poolID + '_algorithm', algorithm);
});
}
// Update pools
function updatePools() {
getPoolStats('graft', 'https://graft.blockhashmining.com/api');
getPoolStats('haven', 'https://haven.blockhashmining.com/api');
getPoolStats('intense', 'https://intense.blockhashmining.com/api');
getPoolStats('loki', 'https://loki.blockhashmining.com/api');
getPoolStats('masari', 'https://masari.blockhashmining.com/api');
getPoolStats('stellite', 'https://stellite.blockhashmining.com/api');
}
// Initialize
$(function() {
setInterval(updatePools, (30*1000));
updatePools();
});
/**
* Strings
**/
// Update Text content
function updateText(elementId, text){
let el = document.getElementById(elementId);
if (el && el.textContent !== text){
el.textContent = text;
}
return el;
}
// Get readable hashrate
function getReadableHashRate(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] + '/s';
}
</script>
</body>
</html>

View file

@ -0,0 +1,6 @@
Hello,
%MESSAGE%
Thank you,
Cheers, the allmighty BlockMaster at %POOL_HOST%

390
init.js Normal file
View file

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

2147
lib/api.js Normal file

File diff suppressed because it is too large Load diff

121
lib/apiInterfaces.js Normal file
View file

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

295
lib/blockUnlocker.js Normal file
View file

@ -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()

380
lib/charts.js Normal file
View file

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

View file

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

79
lib/childDaemon.js Normal file
View file

@ -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()

124
lib/configReader.js Normal file
View file

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

148
lib/daemon.js Normal file
View file

@ -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()
}

72
lib/email.js Normal file
View file

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

25
lib/exceptionWriter.js Normal file
View file

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

96
lib/logger.js Normal file
View file

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

635
lib/market.js Normal file
View file

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

308
lib/notifications.js Normal file
View file

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

291
lib/paymentProcessor.js Normal file
View file

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

1407
lib/pool.js Normal file

File diff suppressed because it is too large Load diff

62
lib/telegram.js Normal file
View file

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

347
lib/telegramBot.js Normal file
View file

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

191
lib/utils.js Normal file
View file

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

100
lib/zmqDaemon.js Normal file
View file

@ -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')
})

33
package.json Normal file
View file

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

147
website_example/admin.html Normal file
View file

@ -0,0 +1,147 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>Arqma with PLE or XCY or TRTL Mining Pool - Admin Panel</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.6.3/jquery.timeago.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.1/mustache.min.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div id="wrapper">
<!-- Navigation -->
<div class="nav-side-menu">
<div class="brand"><a href="/">Admin Panel</a></div>
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapsed out">
<li><a class="hot_link" data-page="admin/statistics.html" href="#">
<i class="fa fa-bar-chart-o"></i> Statistics
</a></li>
<li><a class="hot_link" data-page="admin/monitoring.html" href="#monitoring">
<i class="fa fa-eye"></i> Monitoring
</a></li>
<li><a class="hot_link" data-page="admin/userslist.html" href="#users_list">
<i class="fa fa-users"></i> Users List
</a></li>
<li><a class="hot_link" data-page="admin/ports.html" href="#ports">
<i class="fa fa-link"></i> Ports Usage
</a></li>
<li><a class="hot_link" data-page="admin/tools.html" href="#tools">
<i class="fa fa-gears"></i> Tools
</a></li>
<li class="sign-out"><a class="hot_link" href="/">
<i class="fa fa-sign-out"></i> Return to Pool
</a></li>
</ul>
</div>
</div>
<!-- Page content -->
<div id="page-wrapper">
<div id="page"></div>
<p id="loading" class="text-center"><i class="fa fa-circle-o-notch fa-spin"></i></p>
</div>
</div>
<!-- Footer -->
<footer>
<div class="text-muted">
Powered by <a target="_blank" href="https://github.com/dvandal/cryptonote-nodejs-pool"><i class="fa fa-github"></i> cryptonote-nodejs-pool</a>
<span id="poolVersion"></span>
<span class="hidden-xs">open sourced under the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GPL</a></span>
</div>
</footer>
<!-- Javascript -->
<script src="config.js"></script>
<script src="js/common.js"></script>
<script>
// Fetch pool statistics
lastStats = {};
mergedStats = {};
let mergedApis = {};
$(function() {
let merged_apis = $.ajax({
url: `${api}/get_apis`,
dataType: 'json',
cache: 'false'
})
let poolStats = $.ajax({
url: `${api}/stats`,
dataType: 'json',
cache: 'false'
})
Promise.all([poolStats, merged_apis])
.then(values => {
lastStats = values[0]
mergedApis = values[1]
let subs = [];
Object.keys(mergedApis).some(key => {
let apiUrl = `${mergedApis[key].api}/stats`
subs.push($.ajax({url: apiUrl, dataType: 'json', cache: 'false'}))
})
Promise.all(subs)
.then(data => {
data.forEach(item => {
mergedStats[item.config.coin] = item
})
$('#poolVersion').html(lastStats.config.version);
routePage();
})
})
});
function fetchLiveStats() {
$.ajax({
url: api + '/live_stats',
dataType: 'json',
cache: 'false'
}).done(function(data) {
if(currentPage.update) {
currentPage.update();
}
}).always(function() {
fetchLiveStats();
});
}
function renderTemplate(usersData, templateId, view) {
let source = $(templateId).html()
Mustache.parse(source)
let rendered = Mustache.render(source, usersData)
$(view).append(rendered)
}
// Initialize
$(function(){
$("head").append("<link rel='stylesheet' href=" + themeCss + ">");
$("head").append("<link rel='stylesheet' href=themes/admin.css>");
$("head").append("<link rel='stylesheet' href=themes/custom.css>");
});
</script>
</body>
</html>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

19
website_example/config.js Normal file
View file

@ -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/<YourPoolFacebook>";
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';

BIN
website_example/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

305
website_example/index.html Normal file
View file

@ -0,0 +1,305 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
<title>Zano Mining by Muscleman</title>
<meta name="Description" content="Zano Mining Pool by Muscleman. Cryptocurrency mining. Mine direct to exchange or wallet. Low pool fees and fast payments!">
<meta name="keywords" content="zano, mining, pool, cryptocurrency, exchange, bitrex, coinmarketcap, tradeogre, payments, coinbase, escodex, bitrex">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.6.3/jquery.timeago.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.1/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link href="themes/default.css?" rel="stylesheet">
<link href="themes/custom.css" rel="stylesheet">
</head>
<body>
<div id="wrapper">
<!-- Navigation -->
<div class="nav-side-menu">
<div class="brand"><a href="/"><span id="coinSymbol"></span> <span data-tkey="miningPool">Mining Pool</span></a></div>
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapsed out">
<li><a class="hot_link" data-page="home.html" href="#">
<i class="fa fa-home"></i> <span data-tkey="dashboard">Dashboard</span>
</a></li>
<li><a class="hot_link" data-page="worker_stats.html" href="#worker_stats">
<i class="fa fa-dashboard"></i> <span data-tkey="yourStats">Worker Statistics</span>
</a></li>
<li><a class="hot_link" data-page="getting_started.html" href="#getting_started">
<i class="fa fa-rocket"></i> <span data-tkey="gettingStarted">Getting Started</span>
</a></li>
<li><a class="hot_link" data-page="pool_blocks.html" href="#pool_blocks">
<i class="fa fa-cubes"></i> <span data-tkey="poolBlocks">Pool Blocks</span>
</a></li>
<li><a class="hot_link" data-page="payments.html" href="#payments">
<i class="fa fa-money"></i> <span data-tkey="payments">Payments</span>
</a></li>
<li><a class="hot_link" data-page="top10miners.html" href="#top10miners">
<i class="fa fa-trophy"></i> <span data-tkey="top10miners">Top 10 miners</span>
</a></li>
<li><a class="hot_link" data-page="market.html" href="#market">
<i class="fa fa-bank"></i> <span data-tkey="market">Market / Calculator</span>
</a></li>
<li><a class="hot_link" data-page="settings.html" href="#settings">
<i class="fa fa-gears"></i> <span data-tkey="settings">Settings</span>
</a></li>
<li><a class="hot_link" data-page="faq.html" href="#faq">
<i class="fa fa-comments"></i> <span data-tkey="faq">FAQ</span>
</a></li>
</ul>
</div>
</div>
<!-- Top Bar -->
<div id="top-bar">
<div><span data-tkey="network">Network</span>: <strong><span id="g_networkHashrate"><span data-tkey="na">N/A</span></span></strong></div>
<div><span data-tkey="poolProp">Prop Pool</span>: <strong><span id="g_poolHashrate"><span data-tkey="na">N/A</span></span></strong></div>
<div><span data-tkey="poolSolo">Solo Pool</span>: <strong><span id="g_poolHashrateSolo"><span data-tkey="na">N/A</span></span></strong></div>
<div><span data-tkey="you">You</span>: <strong><span id="g_userHashrate"><span tkey="na">N/A</span></span></strong></div>
<div><span id="statsUpdated"><span data-tkey="statsUpdated">Stats Updated</span> &nbsp;</span></div>
<div id="langSelector"></div>
</div>
<!-- Page content -->
<div id="page-wrapper">
<div id="page"></div>
<p id="loading" class="text-center"><i class="fa fa-circle-o-notch fa-spin"></i></p>
</div>
</div>
<!-- Footer -->
<footer>
<div class="text-muted">
<span data-tkey="poweredBy">Powered by</span> <a target="_blank" href="https://github.com/hyle-team/progpowz-nodejs-pool"><i class="fa fa-github"></i> progpowz-nodejs-pool</a>
<span id="poolVersion"></span>
<span class="hidden-xs"><span data-tkey="openSource">open sourced under the</span> <a href="http://www.gnu.org/licenses/gpl-2.0.html">GPL</a></span>
</div>
</footer>
<!-- Javascript -->
<script src="config.js?"></script>
<script src="lang/languages.js"></script>
<script src="js/common.js"></script>
<script src="js/custom.js"></script>
<script>
// Store last pool statistics
let lastStats;
let mergedStats = {};
let blockExplorers = {};
let mergedApis = {};
function getUrlVars() {
let vars = {};
let location = window.location.href.replace('#worker_stats', '');
let parts = location.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
function getUrlParam(parameter, defaultvalue){
let urlparameter = defaultvalue;
if(window.location.href.indexOf(parameter) > -1){
urlparameter = getUrlVars()[parameter];
}
return urlparameter;
}
// Get current miner address
function getCurrentAddress(coin) {
let address = ''
if (coin) {
let urlWalletAddress = getUrlParam(coin, 0);
address = urlWalletAddress || docCookies.getItem(`mining_address_${coin}`);
}
return address;
}
// Pulse live update
function pulseLiveUpdate(){
let stats_update = document.getElementById('statsUpdated');
stats_update.style.transition = 'opacity 100ms ease-out';
stats_update.style.opacity = 1;
setTimeout(function(){
stats_update.style.transition = 'opacity 7000ms linear';
stats_update.style.opacity = 0;
}, 500);
}
// Update live informations
function updateLiveStats(data, key) {
pulseLiveUpdate();
if (key !== parentCoin) {
mergedStats[key] = data;
} else {
lastStats = data;
if (lastStats && lastStats.pool && lastStats.pool.totalMinersPaid.toString() == '-1'){
lastStats.pool.totalMinersPaid = 0;
}
updateIndex();
}
if (currentPage) currentPage.update(key);
}
// Update global informations
function updateIndex(){
updateText('coinSymbol', lastStats.config.symbol);
updateText('g_networkHashrate', getReadableHashRateString(lastStats.network.difficulty / lastStats.config.coinDifficultyTarget) + '/sec');
updateText('g_poolHashrate', getReadableHashRateString(lastStats.pool.hashrate) + '/sec');
updateText('g_poolHashrateSolo', getReadableHashRateString(lastStats.pool.hashrateSolo) + '/sec');
if (lastStats.miner && lastStats.miner.hashrate){
updateText('g_userHashrate', getReadableHashRateString(lastStats.miner.hashrate) + '/sec');
}
else{
updateText('g_userHashrate', 'N/A');
}
updateText('poolVersion', lastStats.config.version);
}
// Load live statistics
function loadLiveStats(reload) {
let apiURL = api + '/stats';
let address = getCurrentAddress();
if (address) { apiURL = apiURL + '?address=' + encodeURIComponent(address); }
if (xhrLiveStats[parentCoin]){
xhrLiveStats[parentCoin].abort();
}
$.get(apiURL, function(data){
updateLiveStats(data, parentCoin);
if (!reload) {
routePage(fetchLiveStats(api, parentCoin));
}
});
Object.keys(mergedApis).some(key => {
let apiUrl = `${mergedApis[key].api}/stats`
// if (xhrLiveStats[key]){
// xhrLiveStats[key].abort();
// }
$.get(apiUrl, function(data){
updateLiveStats(data, key);
if (!reload){
routePage(fetchLiveStats(mergedApis[key].api, key));
}
});
})
}
// Fetch live statistics
let xhrLiveStats = {};
function fetchLiveStats(endPoint, key) {
let apiURL = endPoint + '/live_stats';
let address = getCurrentAddress(key);
if (address) {
apiURL = apiURL + '?address=' + encodeURIComponent(address);
}
// if (xhrLiveStats[key] && xhrLiveStats[key].status !== 200){
// xhrLiveStats[key].abort();
// }
xhrLiveStats[key] = $.ajax({
url: apiURL,
dataType: 'json',
cache: 'false'
}).done(function(data){
updateLiveStats(data, key);
}).always(function(){
fetchLiveStats(endPoint, key);
});
}
// Fetch Block and Transaction Explorer Urls
let xhrBlockExplorers;
let xhrMergedApis;
function fetchBlockExplorers() {
let apiURL = api + '/block_explorers';
xhrBlockExplorers = $.ajax({
url: apiURL,
dataType: 'json',
cache: 'false'
}).done(function(data){
blockExplorers = data;
})
apiURL = api + '/get_apis';
xhrMergedApis = $.ajax({
url: apiURL,
dataType: 'json',
cache: 'false',
}).done(function(data){
mergedApis = data;
loadLiveStats()
})
}
// Initialize
$(function(){
// Load current theme if not default
if (themeCss && themeCss != 'themes/default.css') {
$("head").append("<link rel='stylesheet' href=" + themeCss + ">");
}
// Add support informations to menu
if (typeof telegram !== 'undefined' && telegram) {
$('#menu-content').append('<li><a target="_new" href="'+telegram+'"><i class="fa fa-telegram"></i> <span data-tkey="telegram">Telegram group</span></a></li>');
}
if (typeof discord !== 'undefined' && discord) {
$('#menu-content').append('<li><a target="_new" href="'+discord+'"><i class="fa fa-ticket"></i> <span data-tkey="discord">Discord</span></a></li>');
}
if (typeof email !== 'undefined' && email) {
$('#menu-content').append('<li><a target="_new" href="mailto:'+email+'"><i class="fa fa-envelope"></i> <span data-tkey="contactUs">Contact Us</span></a></li>');
}
if (typeof facebook !== 'undefined' && facebook) {
$('#menu-content').append('<li><a target="_new" href="'+facebook+'"><i class="fa fa-facebook"></i> <span data-tkey="facebook">Facebook</span></a></li>');
}
if (typeof langs !== 'undefined' && langs) {
$('#menu-content').append('<div id="mLangSelector"></div>');
renderLangSelector();
}
if (xhrBlockExplorers)
xhrBlockExplorers.abort();
if (xhrMergedApis)
xhrMergedApis.abort();
fetchBlockExplorers()
});
</script>
</body>
</html>

2310
website_example/js/common.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
/* Insert your pool's unique Javascript here */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
var langs = { 'en': 'English', 'es': 'Español', 'fr': 'Français', 'it': 'Italiano', 'ru': 'Русский', 'ca': 'Català', 'ko': '한국어', 'zh-CN': '简体中文' };

View file

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

View file

@ -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: []
};
}));

View file

@ -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: []
};
}));

View file

@ -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 عام']);}
};
}));

View file

@ -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: []
};
}));

View file

@ -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 години"
};
}));

View file

@ -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: " "
};
}));

View file

@ -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: []
};
}));

View file

@ -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']);}
};
})();
}));

View file

@ -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: []
};
}));

View file

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

View file

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

View file

@ -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: []
};
}));

View file

@ -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 χρόνια"
};
}));

View file

@ -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: []
};
}));

View file

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

View file

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

View file

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

View file

@ -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: ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']
};
}));

View file

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

View file

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

View file

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

View file

@ -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 שנים";}
};
}));

View file

@ -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: " "
};
}));

View file

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

View file

@ -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 տարի"
};
}));

View file

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

View file

@ -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: []
};
}));

View file

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

View file

@ -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: ""
};
}));

View file

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

View file

@ -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: []
};
}));

View file

@ -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 жыл"); }
};
}));

View file

@ -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: []
};
}));

View file

@ -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: []
};
}));

View file

@ -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 години"
};
})();
}));

View file

@ -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: []
};
}));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: " "
};
}));

View file

@ -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 лет"); }
};
}));

View file

@ -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: []
};
}));

View file

@ -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 කට පමණ"
};
}));

View file

@ -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']);}
};
})();
}));

View file

@ -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: " "
};
}));

View file

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

View file

@ -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: " "
};
}));

View file

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

Some files were not shown because too many files have changed in this diff Show more