commit
a9fb1fd15d
127 changed files with 23691 additions and 0 deletions
@ -0,0 +1,6 @@ |
|||||||
|
.DS_Store |
||||||
|
config.json |
||||||
|
testnet.json |
||||||
|
|
||||||
|
/build/_workspace/ |
||||||
|
/build/bin/ |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
language: go |
||||||
|
|
||||||
|
go: |
||||||
|
- "1.10" |
||||||
|
- tip |
||||||
|
|
||||||
|
services: |
||||||
|
- redis-server |
||||||
@ -0,0 +1,621 @@ |
|||||||
|
GNU GENERAL PUBLIC LICENSE |
||||||
|
Version 3, 29 June 2007 |
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
||||||
|
Everyone is permitted to copy and distribute verbatim copies |
||||||
|
of this license document, but changing it is not allowed. |
||||||
|
|
||||||
|
Preamble |
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for |
||||||
|
software and other kinds of works. |
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed |
||||||
|
to take away your freedom to share and change the works. By contrast, |
||||||
|
the GNU General Public License is intended to guarantee your freedom to |
||||||
|
share and change all versions of a program--to make sure it remains free |
||||||
|
software for all its users. We, the Free Software Foundation, use the |
||||||
|
GNU General Public License for most of our software; it applies also to |
||||||
|
any other work released this way by its authors. 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 |
||||||
|
them 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 prevent others from denying you |
||||||
|
these rights or asking you to surrender the rights. Therefore, you have |
||||||
|
certain responsibilities if you distribute copies of the software, or if |
||||||
|
you modify it: responsibilities to respect the freedom of others. |
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether |
||||||
|
gratis or for a fee, you must pass on to the recipients the same |
||||||
|
freedoms that you received. 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. |
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: |
||||||
|
(1) assert copyright on the software, and (2) offer you this License |
||||||
|
giving you legal permission to copy, distribute and/or modify it. |
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains |
||||||
|
that there is no warranty for this free software. For both users' and |
||||||
|
authors' sake, the GPL requires that modified versions be marked as |
||||||
|
changed, so that their problems will not be attributed erroneously to |
||||||
|
authors of previous versions. |
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run |
||||||
|
modified versions of the software inside them, although the manufacturer |
||||||
|
can do so. This is fundamentally incompatible with the aim of |
||||||
|
protecting users' freedom to change the software. The systematic |
||||||
|
pattern of such abuse occurs in the area of products for individuals to |
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we |
||||||
|
have designed this version of the GPL to prohibit the practice for those |
||||||
|
products. If such problems arise substantially in other domains, we |
||||||
|
stand ready to extend this provision to those domains in future versions |
||||||
|
of the GPL, as needed to protect the freedom of users. |
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. |
||||||
|
States should not allow patents to restrict development and use of |
||||||
|
software on general-purpose computers, but in those that do, we wish to |
||||||
|
avoid the special danger that patents applied to a free program could |
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that |
||||||
|
patents cannot be used to render the program non-free. |
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and |
||||||
|
modification follow. |
||||||
|
|
||||||
|
TERMS AND CONDITIONS |
||||||
|
|
||||||
|
0. Definitions. |
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License. |
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of |
||||||
|
works, such as semiconductor masks. |
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this |
||||||
|
License. Each licensee is addressed as "you". "Licensees" and |
||||||
|
"recipients" may be individuals or organizations. |
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work |
||||||
|
in a fashion requiring copyright permission, other than the making of an |
||||||
|
exact copy. The resulting work is called a "modified version" of the |
||||||
|
earlier work or a work "based on" the earlier work. |
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based |
||||||
|
on the Program. |
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without |
||||||
|
permission, would make you directly or secondarily liable for |
||||||
|
infringement under applicable copyright law, except executing it on a |
||||||
|
computer or modifying a private copy. Propagation includes copying, |
||||||
|
distribution (with or without modification), making available to the |
||||||
|
public, and in some countries other activities as well. |
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other |
||||||
|
parties to make or receive copies. Mere interaction with a user through |
||||||
|
a computer network, with no transfer of a copy, is not conveying. |
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" |
||||||
|
to the extent that it includes a convenient and prominently visible |
||||||
|
feature that (1) displays an appropriate copyright notice, and (2) |
||||||
|
tells the user that there is no warranty for the work (except to the |
||||||
|
extent that warranties are provided), that licensees may convey the |
||||||
|
work under this License, and how to view a copy of this License. If |
||||||
|
the interface presents a list of user commands or options, such as a |
||||||
|
menu, a prominent item in the list meets this criterion. |
||||||
|
|
||||||
|
1. Source Code. |
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work |
||||||
|
for making modifications to it. "Object code" means any non-source |
||||||
|
form of a work. |
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official |
||||||
|
standard defined by a recognized standards body, or, in the case of |
||||||
|
interfaces specified for a particular programming language, one that |
||||||
|
is widely used among developers working in that language. |
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other |
||||||
|
than the work as a whole, that (a) is included in the normal form of |
||||||
|
packaging a Major Component, but which is not part of that Major |
||||||
|
Component, and (b) serves only to enable use of the work with that |
||||||
|
Major Component, or to implement a Standard Interface for which an |
||||||
|
implementation is available to the public in source code form. A |
||||||
|
"Major Component", in this context, means a major essential component |
||||||
|
(kernel, window system, and so on) of the specific operating system |
||||||
|
(if any) on which the executable work runs, or a compiler used to |
||||||
|
produce the work, or an object code interpreter used to run it. |
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all |
||||||
|
the source code needed to generate, install, and (for an executable |
||||||
|
work) run the object code and to modify the work, including scripts to |
||||||
|
control those activities. However, it does not include the work's |
||||||
|
System Libraries, or general-purpose tools or generally available free |
||||||
|
programs which are used unmodified in performing those activities but |
||||||
|
which are not part of the work. For example, Corresponding Source |
||||||
|
includes interface definition files associated with source files for |
||||||
|
the work, and the source code for shared libraries and dynamically |
||||||
|
linked subprograms that the work is specifically designed to require, |
||||||
|
such as by intimate data communication or control flow between those |
||||||
|
subprograms and other parts of the work. |
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users |
||||||
|
can regenerate automatically from other parts of the Corresponding |
||||||
|
Source. |
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that |
||||||
|
same work. |
||||||
|
|
||||||
|
2. Basic Permissions. |
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of |
||||||
|
copyright on the Program, and are irrevocable provided the stated |
||||||
|
conditions are met. This License explicitly affirms your unlimited |
||||||
|
permission to run the unmodified Program. The output from running a |
||||||
|
covered work is covered by this License only if the output, given its |
||||||
|
content, constitutes a covered work. This License acknowledges your |
||||||
|
rights of fair use or other equivalent, as provided by copyright law. |
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not |
||||||
|
convey, without conditions so long as your license otherwise remains |
||||||
|
in force. You may convey covered works to others for the sole purpose |
||||||
|
of having them make modifications exclusively for you, or provide you |
||||||
|
with facilities for running those works, provided that you comply with |
||||||
|
the terms of this License in conveying all material for which you do |
||||||
|
not control copyright. Those thus making or running the covered works |
||||||
|
for you must do so exclusively on your behalf, under your direction |
||||||
|
and control, on terms that prohibit them from making any copies of |
||||||
|
your copyrighted material outside their relationship with you. |
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under |
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10 |
||||||
|
makes it unnecessary. |
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological |
||||||
|
measure under any applicable law fulfilling obligations under article |
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||||||
|
similar laws prohibiting or restricting circumvention of such |
||||||
|
measures. |
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid |
||||||
|
circumvention of technological measures to the extent such circumvention |
||||||
|
is effected by exercising rights under this License with respect to |
||||||
|
the covered work, and you disclaim any intention to limit operation or |
||||||
|
modification of the work as a means of enforcing, against the work's |
||||||
|
users, your or third parties' legal rights to forbid circumvention of |
||||||
|
technological measures. |
||||||
|
|
||||||
|
4. Conveying Verbatim Copies. |
||||||
|
|
||||||
|
You may convey 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; |
||||||
|
keep intact all notices stating that this License and any |
||||||
|
non-permissive terms added in accord with section 7 apply to the code; |
||||||
|
keep intact all notices of the absence of any warranty; and give all |
||||||
|
recipients a copy of this License along with the Program. |
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey, |
||||||
|
and you may offer support or warranty protection for a fee. |
||||||
|
|
||||||
|
5. Conveying Modified Source Versions. |
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to |
||||||
|
produce it from the Program, in the form of source code under the |
||||||
|
terms of section 4, provided that you also meet all of these conditions: |
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified |
||||||
|
it, and giving a relevant date. |
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is |
||||||
|
released under this License and any conditions added under section |
||||||
|
7. This requirement modifies the requirement in section 4 to |
||||||
|
"keep intact all notices". |
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this |
||||||
|
License to anyone who comes into possession of a copy. This |
||||||
|
License will therefore apply, along with any applicable section 7 |
||||||
|
additional terms, to the whole of the work, and all its parts, |
||||||
|
regardless of how they are packaged. This License gives no |
||||||
|
permission to license the work in any other way, but it does not |
||||||
|
invalidate such permission if you have separately received it. |
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display |
||||||
|
Appropriate Legal Notices; however, if the Program has interactive |
||||||
|
interfaces that do not display Appropriate Legal Notices, your |
||||||
|
work need not make them do so. |
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent |
||||||
|
works, which are not by their nature extensions of the covered work, |
||||||
|
and which are not combined with it such as to form a larger program, |
||||||
|
in or on a volume of a storage or distribution medium, is called an |
||||||
|
"aggregate" if the compilation and its resulting copyright are not |
||||||
|
used to limit the access or legal rights of the compilation's users |
||||||
|
beyond what the individual works permit. Inclusion of a covered work |
||||||
|
in an aggregate does not cause this License to apply to the other |
||||||
|
parts of the aggregate. |
||||||
|
|
||||||
|
6. Conveying Non-Source Forms. |
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms |
||||||
|
of sections 4 and 5, provided that you also convey the |
||||||
|
machine-readable Corresponding Source under the terms of this License, |
||||||
|
in one of these ways: |
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product |
||||||
|
(including a physical distribution medium), accompanied by the |
||||||
|
Corresponding Source fixed on a durable physical medium |
||||||
|
customarily used for software interchange. |
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product |
||||||
|
(including a physical distribution medium), accompanied by a |
||||||
|
written offer, valid for at least three years and valid for as |
||||||
|
long as you offer spare parts or customer support for that product |
||||||
|
model, to give anyone who possesses the object code either (1) a |
||||||
|
copy of the Corresponding Source for all the software in the |
||||||
|
product that is covered by this License, on a durable physical |
||||||
|
medium customarily used for software interchange, for a price no |
||||||
|
more than your reasonable cost of physically performing this |
||||||
|
conveying of source, or (2) access to copy the |
||||||
|
Corresponding Source from a network server at no charge. |
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the |
||||||
|
written offer to provide the Corresponding Source. This |
||||||
|
alternative is allowed only occasionally and noncommercially, and |
||||||
|
only if you received the object code with such an offer, in accord |
||||||
|
with subsection 6b. |
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated |
||||||
|
place (gratis or for a charge), and offer equivalent access to the |
||||||
|
Corresponding Source in the same way through the same place at no |
||||||
|
further charge. You need not require recipients to copy the |
||||||
|
Corresponding Source along with the object code. If the place to |
||||||
|
copy the object code is a network server, the Corresponding Source |
||||||
|
may be on a different server (operated by you or a third party) |
||||||
|
that supports equivalent copying facilities, provided you maintain |
||||||
|
clear directions next to the object code saying where to find the |
||||||
|
Corresponding Source. Regardless of what server hosts the |
||||||
|
Corresponding Source, you remain obligated to ensure that it is |
||||||
|
available for as long as needed to satisfy these requirements. |
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided |
||||||
|
you inform other peers where the object code and Corresponding |
||||||
|
Source of the work are being offered to the general public at no |
||||||
|
charge under subsection 6d. |
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded |
||||||
|
from the Corresponding Source as a System Library, need not be |
||||||
|
included in conveying the object code work. |
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any |
||||||
|
tangible personal property which is normally used for personal, family, |
||||||
|
or household purposes, or (2) anything designed or sold for incorporation |
||||||
|
into a dwelling. In determining whether a product is a consumer product, |
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular |
||||||
|
product received by a particular user, "normally used" refers to a |
||||||
|
typical or common use of that class of product, regardless of the status |
||||||
|
of the particular user or of the way in which the particular user |
||||||
|
actually uses, or expects or is expected to use, the product. A product |
||||||
|
is a consumer product regardless of whether the product has substantial |
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent |
||||||
|
the only significant mode of use of the product. |
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods, |
||||||
|
procedures, authorization keys, or other information required to install |
||||||
|
and execute modified versions of a covered work in that User Product from |
||||||
|
a modified version of its Corresponding Source. The information must |
||||||
|
suffice to ensure that the continued functioning of the modified object |
||||||
|
code is in no case prevented or interfered with solely because |
||||||
|
modification has been made. |
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or |
||||||
|
specifically for use in, a User Product, and the conveying occurs as |
||||||
|
part of a transaction in which the right of possession and use of the |
||||||
|
User Product is transferred to the recipient in perpetuity or for a |
||||||
|
fixed term (regardless of how the transaction is characterized), the |
||||||
|
Corresponding Source conveyed under this section must be accompanied |
||||||
|
by the Installation Information. But this requirement does not apply |
||||||
|
if neither you nor any third party retains the ability to install |
||||||
|
modified object code on the User Product (for example, the work has |
||||||
|
been installed in ROM). |
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a |
||||||
|
requirement to continue to provide support service, warranty, or updates |
||||||
|
for a work that has been modified or installed by the recipient, or for |
||||||
|
the User Product in which it has been modified or installed. Access to a |
||||||
|
network may be denied when the modification itself materially and |
||||||
|
adversely affects the operation of the network or violates the rules and |
||||||
|
protocols for communication across the network. |
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, |
||||||
|
in accord with this section must be in a format that is publicly |
||||||
|
documented (and with an implementation available to the public in |
||||||
|
source code form), and must require no special password or key for |
||||||
|
unpacking, reading or copying. |
||||||
|
|
||||||
|
7. Additional Terms. |
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this |
||||||
|
License by making exceptions from one or more of its conditions. |
||||||
|
Additional permissions that are applicable to the entire Program shall |
||||||
|
be treated as though they were included in this License, to the extent |
||||||
|
that they are valid under applicable law. If additional permissions |
||||||
|
apply only to part of the Program, that part may be used separately |
||||||
|
under those permissions, but the entire Program remains governed by |
||||||
|
this License without regard to the additional permissions. |
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option |
||||||
|
remove any additional permissions from that copy, or from any part of |
||||||
|
it. (Additional permissions may be written to require their own |
||||||
|
removal in certain cases when you modify the work.) You may place |
||||||
|
additional permissions on material, added by you to a covered work, |
||||||
|
for which you have or can give appropriate copyright permission. |
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you |
||||||
|
add to a covered work, you may (if authorized by the copyright holders of |
||||||
|
that material) supplement the terms of this License with terms: |
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the |
||||||
|
terms of sections 15 and 16 of this License; or |
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or |
||||||
|
author attributions in that material or in the Appropriate Legal |
||||||
|
Notices displayed by works containing it; or |
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or |
||||||
|
requiring that modified versions of such material be marked in |
||||||
|
reasonable ways as different from the original version; or |
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or |
||||||
|
authors of the material; or |
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some |
||||||
|
trade names, trademarks, or service marks; or |
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that |
||||||
|
material by anyone who conveys the material (or modified versions of |
||||||
|
it) with contractual assumptions of liability to the recipient, for |
||||||
|
any liability that these contractual assumptions directly impose on |
||||||
|
those licensors and authors. |
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further |
||||||
|
restrictions" within the meaning of section 10. If the Program as you |
||||||
|
received it, or any part of it, contains a notice stating that it is |
||||||
|
governed by this License along with a term that is a further |
||||||
|
restriction, you may remove that term. If a license document contains |
||||||
|
a further restriction but permits relicensing or conveying under this |
||||||
|
License, you may add to a covered work material governed by the terms |
||||||
|
of that license document, provided that the further restriction does |
||||||
|
not survive such relicensing or conveying. |
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you |
||||||
|
must place, in the relevant source files, a statement of the |
||||||
|
additional terms that apply to those files, or a notice indicating |
||||||
|
where to find the applicable terms. |
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the |
||||||
|
form of a separately written license, or stated as exceptions; |
||||||
|
the above requirements apply either way. |
||||||
|
|
||||||
|
8. Termination. |
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly |
||||||
|
provided under this License. Any attempt otherwise to propagate or |
||||||
|
modify it is void, and will automatically terminate your rights under |
||||||
|
this License (including any patent licenses granted under the third |
||||||
|
paragraph of section 11). |
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your |
||||||
|
license from a particular copyright holder is reinstated (a) |
||||||
|
provisionally, unless and until the copyright holder explicitly and |
||||||
|
finally terminates your license, and (b) permanently, if the copyright |
||||||
|
holder fails to notify you of the violation by some reasonable means |
||||||
|
prior to 60 days after the cessation. |
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is |
||||||
|
reinstated permanently if the copyright holder notifies you of the |
||||||
|
violation by some reasonable means, this is the first time you have |
||||||
|
received notice of violation of this License (for any work) from that |
||||||
|
copyright holder, and you cure the violation prior to 30 days after |
||||||
|
your receipt of the notice. |
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the |
||||||
|
licenses of parties who have received copies or rights from you under |
||||||
|
this License. If your rights have been terminated and not permanently |
||||||
|
reinstated, you do not qualify to receive new licenses for the same |
||||||
|
material under section 10. |
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies. |
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or |
||||||
|
run a copy of the Program. Ancillary propagation of a covered work |
||||||
|
occurring solely as a consequence of using peer-to-peer transmission |
||||||
|
to receive a copy likewise does not require acceptance. However, |
||||||
|
nothing other than this License grants you permission to propagate or |
||||||
|
modify any covered work. These actions infringe copyright if you do |
||||||
|
not accept this License. Therefore, by modifying or propagating a |
||||||
|
covered work, you indicate your acceptance of this License to do so. |
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients. |
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically |
||||||
|
receives a license from the original licensors, to run, modify and |
||||||
|
propagate that work, subject to this License. You are not responsible |
||||||
|
for enforcing compliance by third parties with this License. |
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an |
||||||
|
organization, or substantially all assets of one, or subdividing an |
||||||
|
organization, or merging organizations. If propagation of a covered |
||||||
|
work results from an entity transaction, each party to that |
||||||
|
transaction who receives a copy of the work also receives whatever |
||||||
|
licenses to the work the party's predecessor in interest had or could |
||||||
|
give under the previous paragraph, plus a right to possession of the |
||||||
|
Corresponding Source of the work from the predecessor in interest, if |
||||||
|
the predecessor has it or can get it with reasonable efforts. |
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the |
||||||
|
rights granted or affirmed under this License. For example, you may |
||||||
|
not impose a license fee, royalty, or other charge for exercise of |
||||||
|
rights granted under this License, and you may not initiate litigation |
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||||||
|
any patent claim is infringed by making, using, selling, offering for |
||||||
|
sale, or importing the Program or any portion of it. |
||||||
|
|
||||||
|
11. Patents. |
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this |
||||||
|
License of the Program or a work on which the Program is based. The |
||||||
|
work thus licensed is called the contributor's "contributor version". |
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims |
||||||
|
owned or controlled by the contributor, whether already acquired or |
||||||
|
hereafter acquired, that would be infringed by some manner, permitted |
||||||
|
by this License, of making, using, or selling its contributor version, |
||||||
|
but do not include claims that would be infringed only as a |
||||||
|
consequence of further modification of the contributor version. For |
||||||
|
purposes of this definition, "control" includes the right to grant |
||||||
|
patent sublicenses in a manner consistent with the requirements of |
||||||
|
this License. |
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||||||
|
patent license under the contributor's essential patent claims, to |
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and |
||||||
|
propagate the contents of its contributor version. |
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express |
||||||
|
agreement or commitment, however denominated, not to enforce a patent |
||||||
|
(such as an express permission to practice a patent or covenant not to |
||||||
|
sue for patent infringement). To "grant" such a patent license to a |
||||||
|
party means to make such an agreement or commitment not to enforce a |
||||||
|
patent against the party. |
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, |
||||||
|
and the Corresponding Source of the work is not available for anyone |
||||||
|
to copy, free of charge and under the terms of this License, through a |
||||||
|
publicly available network server or other readily accessible means, |
||||||
|
then you must either (1) cause the Corresponding Source to be so |
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the |
||||||
|
patent license for this particular work, or (3) arrange, in a manner |
||||||
|
consistent with the requirements of this License, to extend the patent |
||||||
|
license to downstream recipients. "Knowingly relying" means you have |
||||||
|
actual knowledge that, but for the patent license, your conveying the |
||||||
|
covered work in a country, or your recipient's use of the covered work |
||||||
|
in a country, would infringe one or more identifiable patents in that |
||||||
|
country that you have reason to believe are valid. |
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or |
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a |
||||||
|
covered work, and grant a patent license to some of the parties |
||||||
|
receiving the covered work authorizing them to use, propagate, modify |
||||||
|
or convey a specific copy of the covered work, then the patent license |
||||||
|
you grant is automatically extended to all recipients of the covered |
||||||
|
work and works based on it. |
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within |
||||||
|
the scope of its coverage, prohibits the exercise of, or is |
||||||
|
conditioned on the non-exercise of one or more of the rights that are |
||||||
|
specifically granted under this License. You may not convey a covered |
||||||
|
work if you are a party to an arrangement with a third party that is |
||||||
|
in the business of distributing software, under which you make payment |
||||||
|
to the third party based on the extent of your activity of conveying |
||||||
|
the work, and under which the third party grants, to any of the |
||||||
|
parties who would receive the covered work from you, a discriminatory |
||||||
|
patent license (a) in connection with copies of the covered work |
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily |
||||||
|
for and in connection with specific products or compilations that |
||||||
|
contain the covered work, unless you entered into that arrangement, |
||||||
|
or that patent license was granted, prior to 28 March 2007. |
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting |
||||||
|
any implied license or other defenses to infringement that may |
||||||
|
otherwise be available to you under applicable patent law. |
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom. |
||||||
|
|
||||||
|
If 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 convey a |
||||||
|
covered work so as to satisfy simultaneously your obligations under this |
||||||
|
License and any other pertinent obligations, then as a consequence you may |
||||||
|
not convey it at all. For example, if you agree to terms that obligate you |
||||||
|
to collect a royalty for further conveying from those to whom you convey |
||||||
|
the Program, the only way you could satisfy both those terms and this |
||||||
|
License would be to refrain entirely from conveying the Program. |
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License. |
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have |
||||||
|
permission to link or combine any covered work with a work licensed |
||||||
|
under version 3 of the GNU Affero General Public License into a single |
||||||
|
combined work, and to convey the resulting work. The terms of this |
||||||
|
License will continue to apply to the part which is the covered work, |
||||||
|
but the special requirements of the GNU Affero General Public License, |
||||||
|
section 13, concerning interaction through a network will apply to the |
||||||
|
combination as such. |
||||||
|
|
||||||
|
14. Revised Versions of this License. |
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of |
||||||
|
the GNU 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 that a certain numbered version of the GNU General |
||||||
|
Public License "or any later version" applies to it, you have the |
||||||
|
option of following the terms and conditions either of that numbered |
||||||
|
version or of any later version published by the Free Software |
||||||
|
Foundation. If the Program does not specify a version number of the |
||||||
|
GNU General Public License, you may choose any version ever published |
||||||
|
by the Free Software Foundation. |
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future |
||||||
|
versions of the GNU General Public License can be used, that proxy's |
||||||
|
public statement of acceptance of a version permanently authorizes you |
||||||
|
to choose that version for the Program. |
||||||
|
|
||||||
|
Later license versions may give you additional or different |
||||||
|
permissions. However, no additional obligations are imposed on any |
||||||
|
author or copyright holder as a result of your choosing to follow a |
||||||
|
later version. |
||||||
|
|
||||||
|
15. Disclaimer of Warranty. |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
16. Limitation of Liability. |
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||||||
|
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. |
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16. |
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided |
||||||
|
above cannot be given local legal effect according to their terms, |
||||||
|
reviewing courts shall apply local law that most closely approximates |
||||||
|
an absolute waiver of all civil liability in connection with the |
||||||
|
Program, unless a warranty or assumption of liability accompanies a |
||||||
|
copy of the Program in return for a fee. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
# This Makefile is meant to be used by people that do not usually work
|
||||||
|
# with Go source code. If you know what GOPATH is then you probably
|
||||||
|
# don't need to bother with make.
|
||||||
|
|
||||||
|
.PHONY: all test clean |
||||||
|
|
||||||
|
GOBIN = build/bin
|
||||||
|
|
||||||
|
all: |
||||||
|
build/env.sh go get -v ./...
|
||||||
|
|
||||||
|
test: all |
||||||
|
build/env.sh go test -v ./...
|
||||||
|
|
||||||
|
clean: |
||||||
|
rm -fr build/_workspace/pkg/ $(GOBIN)/*
|
||||||
@ -0,0 +1,446 @@ |
|||||||
|
## Open Source Perkle (ETC 2021) Mining Pool |
||||||
|
|
||||||
|
|
||||||
|
### Features |
||||||
|
|
||||||
|
**This pool is being further developed to provide an easy to use pool for Perkle miners. Testing and bug submissions are welcome!** |
||||||
|
|
||||||
|
* Updated to work with Perkle 0.2.1 |
||||||
|
* Support for HTTP and Stratum mining |
||||||
|
* Detailed block stats with luck percentage and full reward |
||||||
|
* Failover geth instances: geth high availability built in |
||||||
|
* Modern beautiful Ember.js frontend |
||||||
|
* Separate stats for workers: can highlight timed-out workers so miners can perform maintenance of rigs |
||||||
|
* JSON-API for stats |
||||||
|
* PPLNS block reward |
||||||
|
* Multi-tx payout at once |
||||||
|
* Beautiful front-end highcharts embedded |
||||||
|
|
||||||
|
#### Proxies |
||||||
|
|
||||||
|
* [Ether-Proxy](https://github.com/sammy007/ether-proxy) HTTP proxy with web interface |
||||||
|
* [Stratum Proxy](https://github.com/Atrides/eth-proxy) for Ethereum |
||||||
|
|
||||||
|
## Guide to make your very own Perkle mining pool |
||||||
|
|
||||||
|
### Building on Linux |
||||||
|
|
||||||
|
Dependencies: |
||||||
|
|
||||||
|
* go >= 1.10 |
||||||
|
* redis-server >= 2.8.0 |
||||||
|
* nodejs >= 4 LTS |
||||||
|
* nginx |
||||||
|
* geth (multi-geth) |
||||||
|
|
||||||
|
**I highly recommend to use Ubuntu 16.04 LTS.** |
||||||
|
|
||||||
|
### Install go lang |
||||||
|
|
||||||
|
$ sudo apt-get install -y build-essential golang-1.10-go unzip |
||||||
|
$ sudo ln -s /usr/lib/go-1.10/bin/go /usr/local/bin/go |
||||||
|
|
||||||
|
### Install redis-server |
||||||
|
|
||||||
|
$ sudo apt-get install redis-server |
||||||
|
|
||||||
|
It is recommended to bind your DB address on 127.0.0.1 or on internal ip. Also, please set up the password for advanced security!!! |
||||||
|
|
||||||
|
### Install nginx |
||||||
|
|
||||||
|
$ sudo apt-get install nginx |
||||||
|
|
||||||
|
sample config located at configs/nginx.default.example (HINT, edit and move to /etc/nginx/sites-available/default) |
||||||
|
|
||||||
|
### Install NODE |
||||||
|
|
||||||
|
This will install the latest nodejs |
||||||
|
|
||||||
|
$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - |
||||||
|
$ sudo apt-get install -y nodejs |
||||||
|
|
||||||
|
### Install Perkle Node |
||||||
|
See https://github.com/esprezzo/perkle |
||||||
|
|
||||||
|
### Install Perkle Pool |
||||||
|
|
||||||
|
$ git clone https://github.com/yuriy0803/open-etc-pool-friends |
||||||
|
$ cd open-perkle-pool |
||||||
|
$ make all |
||||||
|
|
||||||
|
If you see open-perkle-pool after ls ~/open-etc-pool-friends/build/bin/, the installation has completed. |
||||||
|
|
||||||
|
$ ls ~/open-perkle-pool/build/bin/ |
||||||
|
|
||||||
|
### Set up Perkle pool |
||||||
|
|
||||||
|
$ mv config.example.json config.json |
||||||
|
$ nano config.json |
||||||
|
|
||||||
|
Set up based on commands below. |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
// The number of cores of CPU. |
||||||
|
"threads": 2, |
||||||
|
// Prefix for keys in redis store |
||||||
|
"coin": "prkl", |
||||||
|
// Give unique name to each instance |
||||||
|
"name": "main", |
||||||
|
// PPLNS rounds |
||||||
|
"pplns": 9000, |
||||||
|
|
||||||
|
"proxy": { |
||||||
|
"enabled": true, |
||||||
|
|
||||||
|
// Bind HTTP mining endpoint to this IP:PORT |
||||||
|
"listen": "0.0.0.0:8888", |
||||||
|
|
||||||
|
// Allow only this header and body size of HTTP request from miners |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
|
||||||
|
/* Set to true if you are behind CloudFlare (not recommended) or behind http-reverse |
||||||
|
proxy to enable IP detection from X-Forwarded-For header. |
||||||
|
Advanced users only. It's tricky to make it right and secure. |
||||||
|
*/ |
||||||
|
"behindReverseProxy": false, |
||||||
|
|
||||||
|
// Stratum mining endpoint |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
// Bind stratum mining socket to this IP:PORT |
||||||
|
"listen": "0.0.0.0:8008", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
// Try to get new job from geth in this interval |
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
// If there are many rejects because of heavy hash, difficulty should be increased properly. |
||||||
|
"difficulty": 2000000000, |
||||||
|
|
||||||
|
/* Reply error to miner instead of job if redis is unavailable. |
||||||
|
Should save electricity to miners if pool is sick and they didn't set up failovers. |
||||||
|
*/ |
||||||
|
"healthCheck": true, |
||||||
|
// Mark pool sick after this number of redis failures. |
||||||
|
"maxFails": 100, |
||||||
|
// TTL for workers stats, usually should be equal to large hashrate window from API section |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
|
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
|
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
/* Name of ipset for banning. |
||||||
|
Check http://ipset.netfilter.org/ documentation. |
||||||
|
*/ |
||||||
|
"ipset": "blacklist", |
||||||
|
// Remove ban after this amount of time |
||||||
|
"timeout": 1800, |
||||||
|
// Percent of invalid shares from all shares to ban miner |
||||||
|
"invalidPercent": 30, |
||||||
|
// Check after after miner submitted this number of shares |
||||||
|
"checkThreshold": 30, |
||||||
|
// Bad miner after this number of malformed requests |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
// Connection rate limit |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
// Number of initial connections |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
// Increase allowed number of connections on each valid share |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// Provides JSON data for frontend which is static website |
||||||
|
"api": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
// Collect miners stats (hashrate, ...) in this interval |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
// Purge stale stats interval |
||||||
|
"purgeInterval": "10m", |
||||||
|
// Fast hashrate estimation window for each miner from it's shares |
||||||
|
"hashrateWindow": "30m", |
||||||
|
// Long and precise hashrate from shares, 3h is cool, keep it |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
// Collect stats for shares/diff ratio for this number of blocks |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
// Max number of payments to display in frontend |
||||||
|
"payments": 50, |
||||||
|
// Max numbers of blocks to display in frontend |
||||||
|
"blocks": 50, |
||||||
|
// Frontend Chart related settings |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74 |
||||||
|
|
||||||
|
/* If you are running API node on a different server where this module |
||||||
|
is reading data from redis writeable slave, you must run an api instance with this option enabled in order to purge hashrate stats from main redis node. |
||||||
|
Only redis writeable slave will work properly if you are distributing using redis slaves. |
||||||
|
Very advanced. Usually all modules should share same redis instance. |
||||||
|
*/ |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
// Check health of each geth node in this interval |
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
/* List of geth nodes to poll for new jobs. Pool will try to get work from |
||||||
|
first alive one and check in background for failed to back up. |
||||||
|
Current block template of the pool is always cached in RAM indeed. |
||||||
|
*/ |
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.2:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
// This is standard redis connection options |
||||||
|
"redis": { |
||||||
|
// Where your redis instance is listening for commands |
||||||
|
// NOTE THAT THE POOL IS CONFIGURED FOR Redis database "1" |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
// This module periodically remits ether to miners |
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
// Pool fee percentage |
||||||
|
"poolFee": 1.0, |
||||||
|
// the address is for pool fee. Personal wallet is recommended to prevent from server hacking. |
||||||
|
"poolFeeAddress": "", |
||||||
|
// Amount of donation to a pool maker. 10 percent of pool fee is donated to a pool maker now. If pool fee is 1 percent, 0.1 percent which is 10 percent of pool fee should be donated to a pool maker. |
||||||
|
"donate": true, |
||||||
|
// Unlock only if this number of blocks mined back |
||||||
|
"depth": 120, |
||||||
|
// Simply don't touch this option |
||||||
|
"immatureDepth": 20, |
||||||
|
// Keep mined transaction fees as pool fees |
||||||
|
"keepTxFees": false, |
||||||
|
// Run unlocker in this interval |
||||||
|
"interval": "10m", |
||||||
|
// Geth instance node rpc endpoint for unlocking blocks |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
// Rise error if can't reach geth in this amount of time |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
// Pay out miners using this module |
||||||
|
"payouts": { |
||||||
|
"enabled": true, |
||||||
|
// Require minimum number of peers on node |
||||||
|
"requirePeers": 5, |
||||||
|
// Run payouts in this interval |
||||||
|
"interval": "12h", |
||||||
|
// Geth instance node rpc endpoint for payouts processing |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
// Rise error if can't reach geth in this amount of time |
||||||
|
"timeout": "10s", |
||||||
|
// Address with pool coinbase wallet address. |
||||||
|
"address": "0x0", |
||||||
|
// Let geth to determine gas and gasPrice |
||||||
|
"autoGas": true, |
||||||
|
// Gas amount and price for payout tx (advanced users only) |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
// The minimum distribution of mining reward. It is 1 CLO now. |
||||||
|
"threshold": 1000000000, |
||||||
|
// Perform BGSAVE on Redis after successful payouts session |
||||||
|
"bgsave": false |
||||||
|
"concurrentTx": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
If you are distributing your pool deployment to several servers or processes, |
||||||
|
create several configs and disable unneeded modules on each server. (Advanced users) |
||||||
|
|
||||||
|
I recommend this deployment strategy: |
||||||
|
|
||||||
|
* Mining instance - 1x (it depends, you can run one node for EU, one for US, one for Asia) |
||||||
|
* Unlocker and payouts instance - 1x each (strict!) |
||||||
|
* API instance - 1x |
||||||
|
|
||||||
|
|
||||||
|
### Run Pool |
||||||
|
It is required to run pool by serviced. If it is not, the terminal could be stopped, and pool doesn’t work. |
||||||
|
|
||||||
|
$ sudo nano /etc/systemd/system/etherpool.service |
||||||
|
|
||||||
|
Copy the following example |
||||||
|
|
||||||
|
``` |
||||||
|
[Unit] |
||||||
|
Description=Etherpool |
||||||
|
After=perkle.target |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=/home/<your-user-name>/open-etc-pool-friends/build/bin/open-etc-pool-friends /home/<your-user-name>/open-etc-pool-friends/config.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
``` |
||||||
|
|
||||||
|
Then run pool by the following commands |
||||||
|
|
||||||
|
$ sudo systemctl enable etherpool |
||||||
|
$ sudo systemctl start etherpool |
||||||
|
|
||||||
|
If you want to debug the node command |
||||||
|
|
||||||
|
$ sudo systemctl status etherpool |
||||||
|
|
||||||
|
Backend operation has completed so far. |
||||||
|
|
||||||
|
### Open Firewall |
||||||
|
|
||||||
|
Firewall should be opened to operate this service. Whether Ubuntu firewall is basically opened or not, the firewall should be opened based on your situation. |
||||||
|
You can open firewall by opening 80,443,8080,8888,8008. |
||||||
|
|
||||||
|
## Install Frontend |
||||||
|
|
||||||
|
### Modify configuration file |
||||||
|
|
||||||
|
$ nano ~/open-etc-pool-friends/www/config/environment.js |
||||||
|
|
||||||
|
Make some modifications in these settings. |
||||||
|
|
||||||
|
BrowserTitle: 'Perkle Mining Pool', |
||||||
|
ApiUrl: '//your-pool-domain/', |
||||||
|
HttpHost: 'http://your-pool-domain', |
||||||
|
StratumHost: 'your-pool-domain', |
||||||
|
PoolFee: '1%', |
||||||
|
|
||||||
|
The frontend is a single-page Ember.js application that polls the pool API to render miner stats. |
||||||
|
|
||||||
|
$ cd ~/open-etc-pool-friends/www |
||||||
|
$ sudo npm install -g ember-cli@2.9.1 |
||||||
|
$ sudo npm install -g bower |
||||||
|
$ sudo chown -R $USER:$GROUP ~/.npm |
||||||
|
$ sudo chown -R $USER:$GROUP ~/.config |
||||||
|
$ npm install |
||||||
|
$ bower install |
||||||
|
$ ./build.sh |
||||||
|
$ cp -R ~/open-etc-pool-friends/www/dist ~/www |
||||||
|
|
||||||
|
As you can see above, the frontend of the pool homepage is created. Then, move to the directory, www, which services the file. |
||||||
|
|
||||||
|
Set up nginx. |
||||||
|
|
||||||
|
$ sudo nano /etc/nginx/sites-available/default |
||||||
|
|
||||||
|
Modify based on configuration file. |
||||||
|
|
||||||
|
# Default server configuration |
||||||
|
# nginx example |
||||||
|
|
||||||
|
upstream api { |
||||||
|
server 127.0.0.1:8080; |
||||||
|
} |
||||||
|
|
||||||
|
server { |
||||||
|
listen 80 default_server; |
||||||
|
listen [::]:80 default_server; |
||||||
|
root /home/<your-user-name>/www; |
||||||
|
|
||||||
|
# Add index.php to the list if you are using PHP |
||||||
|
index index.html index.htm index.nginx-debian.html; |
||||||
|
|
||||||
|
server_name _; |
||||||
|
|
||||||
|
location / { |
||||||
|
# First attempt to serve request as file, then |
||||||
|
# as directory, then fall back to displaying a 404. |
||||||
|
try_files $uri $uri/ =404; |
||||||
|
} |
||||||
|
|
||||||
|
location /api { |
||||||
|
proxy_pass http://api; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
After setting nginx is completed, run the command below. |
||||||
|
|
||||||
|
$ sudo service nginx restart |
||||||
|
|
||||||
|
Type your homepage address or IP address on the web. |
||||||
|
If you face screen without any issues, pool installation has completed. |
||||||
|
|
||||||
|
### Extra) How To Secure the pool frontend with Let's Encrypt (https) |
||||||
|
|
||||||
|
This guide was originally referred from [digitalocean - How To Secure Nginx with Let's Encrypt on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-16-04) |
||||||
|
|
||||||
|
First, install the Certbot's Nginx package with apt-get |
||||||
|
|
||||||
|
``` |
||||||
|
$ sudo add-apt-repository ppa:certbot/certbot |
||||||
|
$ sudo apt-get update |
||||||
|
$ sudo apt-get install python-certbot-nginx |
||||||
|
``` |
||||||
|
|
||||||
|
And then open your nginx setting file, make sure the server name is configured! |
||||||
|
|
||||||
|
``` |
||||||
|
$ sudo nano /etc/nginx/sites-available/default |
||||||
|
. . . |
||||||
|
server_name <your-pool-domain>; |
||||||
|
. . . |
||||||
|
``` |
||||||
|
|
||||||
|
Change the _ to your pool domain, and now you can obtain your auto-renewaled ssl certificate for free! |
||||||
|
|
||||||
|
``` |
||||||
|
$ sudo certbot --nginx -d <your-pool-domain> |
||||||
|
``` |
||||||
|
|
||||||
|
Now you can access your pool's frontend via https! Share your pool link! |
||||||
|
|
||||||
|
### Notes |
||||||
|
|
||||||
|
* Unlocking and payouts are sequential, 1st tx go, 2nd waiting for 1st to confirm and so on. You can disable that in code. Carefully read `docs/PAYOUTS.md`. |
||||||
|
* Also, keep in mind that **unlocking and payouts will halt in case of backend or node RPC errors**. In that case check everything and restart. |
||||||
|
* You must restart module if you see errors with the word *suspended*. |
||||||
|
* Don't run payouts and unlocker modules as part of mining node. Create separate configs for both, launch independently and make sure you have a single instance of each module running. |
||||||
|
* If `poolFeeAddress` is not specified all pool profit will remain on coinbase address. If it specified, make sure to periodically send some dust back required for payments. |
||||||
|
* DO NOT OPEN YOUR RPC OR REDIS ON 0.0.0.0!!! It will eventually cause coin theft. |
||||||
|
|
||||||
|
### Credits |
||||||
|
|
||||||
|
Made by sammy007. Licensed under GPLv3. |
||||||
|
Modified by Akira Takizawa & The Ellaism Project & The Esprezzo Team. |
||||||
|
|
||||||
|
#### Contributors |
||||||
|
|
||||||
|
[Alex Leverington](https://github.com/subtly) |
||||||
|
|
||||||
|
### Donations |
||||||
|
|
||||||
|
ETH/ETC/ETSC/CLO: 0xd92fa5a9732a0aec36dc8d5a6a1305dc2d3e09e6 |
||||||
|
|
||||||
|
 |
||||||
|
|
||||||
|
Highly appreciated. |
||||||
@ -0,0 +1,374 @@ |
|||||||
|
package api |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gorilla/mux" |
||||||
|
"github.com/robfig/cron" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
type ApiConfig struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
Listen string `json:"listen"` |
||||||
|
PoolCharts string `json:"poolCharts"` |
||||||
|
PoolChartsNum int64 `json:"poolChartsNum"` |
||||||
|
MinerChartsNum int64 `json:"minerChartsNum"` |
||||||
|
MinerCharts string `json:"minerCharts"` |
||||||
|
StatsCollectInterval string `json:"statsCollectInterval"` |
||||||
|
HashrateWindow string `json:"hashrateWindow"` |
||||||
|
HashrateLargeWindow string `json:"hashrateLargeWindow"` |
||||||
|
LuckWindow []int `json:"luckWindow"` |
||||||
|
Payments int64 `json:"payments"` |
||||||
|
Blocks int64 `json:"blocks"` |
||||||
|
PurgeOnly bool `json:"purgeOnly"` |
||||||
|
PurgeInterval string `json:"purgeInterval"` |
||||||
|
} |
||||||
|
|
||||||
|
type ApiServer struct { |
||||||
|
config *ApiConfig |
||||||
|
backend *storage.RedisClient |
||||||
|
hashrateWindow time.Duration |
||||||
|
hashrateLargeWindow time.Duration |
||||||
|
stats atomic.Value |
||||||
|
miners map[string]*Entry |
||||||
|
minersMu sync.RWMutex |
||||||
|
statsIntv time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
type Entry struct { |
||||||
|
stats map[string]interface{} |
||||||
|
updatedAt int64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewApiServer(cfg *ApiConfig, backend *storage.RedisClient) *ApiServer { |
||||||
|
hashrateWindow := util.MustParseDuration(cfg.HashrateWindow) |
||||||
|
hashrateLargeWindow := util.MustParseDuration(cfg.HashrateLargeWindow) |
||||||
|
return &ApiServer{ |
||||||
|
config: cfg, |
||||||
|
backend: backend, |
||||||
|
hashrateWindow: hashrateWindow, |
||||||
|
hashrateLargeWindow: hashrateLargeWindow, |
||||||
|
miners: make(map[string]*Entry), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) Start() { |
||||||
|
if s.config.PurgeOnly { |
||||||
|
log.Printf("Starting API in purge-only mode") |
||||||
|
} else { |
||||||
|
log.Printf("Starting API on %v", s.config.Listen) |
||||||
|
} |
||||||
|
|
||||||
|
s.statsIntv = util.MustParseDuration(s.config.StatsCollectInterval) |
||||||
|
statsTimer := time.NewTimer(s.statsIntv) |
||||||
|
log.Printf("Set stats collect interval to %v", s.statsIntv) |
||||||
|
|
||||||
|
purgeIntv := util.MustParseDuration(s.config.PurgeInterval) |
||||||
|
purgeTimer := time.NewTimer(purgeIntv) |
||||||
|
log.Printf("Set purge interval to %v", purgeIntv) |
||||||
|
|
||||||
|
sort.Ints(s.config.LuckWindow) |
||||||
|
|
||||||
|
if s.config.PurgeOnly { |
||||||
|
s.purgeStale() |
||||||
|
} else { |
||||||
|
s.purgeStale() |
||||||
|
s.collectStats() |
||||||
|
} |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-statsTimer.C: |
||||||
|
if !s.config.PurgeOnly { |
||||||
|
s.collectStats() |
||||||
|
} |
||||||
|
statsTimer.Reset(s.statsIntv) |
||||||
|
case <-purgeTimer.C: |
||||||
|
s.purgeStale() |
||||||
|
purgeTimer.Reset(purgeIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
c := cron.New() |
||||||
|
|
||||||
|
poolCharts := s.config.PoolCharts |
||||||
|
log.Printf("pool charts config is :%v", poolCharts) |
||||||
|
c.AddFunc(poolCharts, func() { |
||||||
|
s.collectPoolCharts() |
||||||
|
}) |
||||||
|
|
||||||
|
minerCharts := s.config.MinerCharts |
||||||
|
log.Printf("miner charts config is :%v", minerCharts) |
||||||
|
c.AddFunc(minerCharts, func() { |
||||||
|
|
||||||
|
miners, err := s.backend.GetAllMinerAccount() |
||||||
|
if err != nil { |
||||||
|
log.Println("Get all miners account error: ", err) |
||||||
|
} |
||||||
|
for _, login := range miners { |
||||||
|
miner, _ := s.backend.CollectWorkersStats(s.hashrateWindow, s.hashrateLargeWindow, login) |
||||||
|
s.collectMinerCharts(login, miner["currentHashrate"].(int64), miner["hashrate"].(int64), miner["workersOnline"].(int64)) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
c.Start() |
||||||
|
}() |
||||||
|
|
||||||
|
if !s.config.PurgeOnly { |
||||||
|
s.listen() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) collectPoolCharts() { |
||||||
|
ts := util.MakeTimestamp() / 1000 |
||||||
|
now := time.Now() |
||||||
|
year, month, day := now.Date() |
||||||
|
hour, min, _ := now.Clock() |
||||||
|
t2 := fmt.Sprintf("%d-%02d-%02d %02d_%02d", year, month, day, hour, min) |
||||||
|
stats := s.getStats() |
||||||
|
hash := fmt.Sprint(stats["hashrate"]) |
||||||
|
log.Println("Pool Hash is ", ts, t2, hash) |
||||||
|
err := s.backend.WritePoolCharts(ts, t2, hash) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to fetch pool charts from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) collectMinerCharts(login string, hash int64, largeHash int64, workerOnline int64) { |
||||||
|
ts := util.MakeTimestamp() / 1000 |
||||||
|
now := time.Now() |
||||||
|
year, month, day := now.Date() |
||||||
|
hour, min, _ := now.Clock() |
||||||
|
t2 := fmt.Sprintf("%d-%02d-%02d %02d_%02d", year, month, day, hour, min) |
||||||
|
|
||||||
|
log.Println("Miner "+login+" Hash is", ts, t2, hash, largeHash) |
||||||
|
err := s.backend.WriteMinerCharts(ts, t2, login, hash, largeHash, workerOnline) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to fetch miner %v charts from backend: %v", login, err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) listen() { |
||||||
|
r := mux.NewRouter() |
||||||
|
r.HandleFunc("/api/stats", s.StatsIndex) |
||||||
|
r.HandleFunc("/api/miners", s.MinersIndex) |
||||||
|
r.HandleFunc("/api/blocks", s.BlocksIndex) |
||||||
|
r.HandleFunc("/api/payments", s.PaymentsIndex) |
||||||
|
r.HandleFunc("/api/accounts/{login:0x[0-9a-fA-F]{40}}", s.AccountIndex) |
||||||
|
r.NotFoundHandler = http.HandlerFunc(notFound) |
||||||
|
err := http.ListenAndServe(s.config.Listen, r) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Failed to start API: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func notFound(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
w.WriteHeader(http.StatusNotFound) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) purgeStale() { |
||||||
|
start := time.Now() |
||||||
|
total, err := s.backend.FlushStaleStats(s.hashrateWindow, s.hashrateLargeWindow) |
||||||
|
if err != nil { |
||||||
|
log.Println("Failed to purge stale data from backend:", err) |
||||||
|
} else { |
||||||
|
log.Printf("Purged stale stats from backend, %v shares affected, elapsed time %v", total, time.Since(start)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) collectStats() { |
||||||
|
start := time.Now() |
||||||
|
stats, err := s.backend.CollectStats(s.hashrateWindow, s.config.Blocks, s.config.Payments) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to fetch stats from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if len(s.config.LuckWindow) > 0 { |
||||||
|
stats["luck"], err = s.backend.CollectLuckStats(s.config.LuckWindow) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to fetch luck stats from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
stats["poolCharts"], err = s.backend.GetPoolCharts(s.config.PoolChartsNum) |
||||||
|
s.stats.Store(stats) |
||||||
|
log.Printf("Stats collection finished %s", time.Since(start)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) StatsIndex(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
reply := make(map[string]interface{}) |
||||||
|
nodes, err := s.backend.GetNodeStates() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to get nodes stats from backend: %v", err) |
||||||
|
} |
||||||
|
reply["nodes"] = nodes |
||||||
|
|
||||||
|
stats := s.getStats() |
||||||
|
if stats != nil { |
||||||
|
reply["now"] = util.MakeTimestamp() |
||||||
|
reply["stats"] = stats["stats"] |
||||||
|
reply["poolCharts"] = stats["poolCharts"] |
||||||
|
reply["hashrate"] = stats["hashrate"] |
||||||
|
reply["minersTotal"] = stats["minersTotal"] |
||||||
|
reply["maturedTotal"] = stats["maturedTotal"] |
||||||
|
reply["immatureTotal"] = stats["immatureTotal"] |
||||||
|
reply["candidatesTotal"] = stats["candidatesTotal"] |
||||||
|
} |
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(reply) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error serializing API response: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) MinersIndex(w http.ResponseWriter, r *http.Request) { |
||||||
|
|
||||||
|
// TODO: Want to get the most used server from workers, so it can be deisplayed in miners page
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
reply := make(map[string]interface{}) |
||||||
|
stats := s.getStats() |
||||||
|
if stats != nil { |
||||||
|
reply["now"] = util.MakeTimestamp() |
||||||
|
reply["miners"] = stats["miners"] |
||||||
|
reply["hashrate"] = stats["hashrate"] |
||||||
|
reply["minersTotal"] = stats["minersTotal"] |
||||||
|
} |
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(reply) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error serializing API response: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) BlocksIndex(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
reply := make(map[string]interface{}) |
||||||
|
stats := s.getStats() |
||||||
|
if stats != nil { |
||||||
|
reply["matured"] = stats["matured"] |
||||||
|
reply["maturedTotal"] = stats["maturedTotal"] |
||||||
|
reply["immature"] = stats["immature"] |
||||||
|
reply["immatureTotal"] = stats["immatureTotal"] |
||||||
|
reply["candidates"] = stats["candidates"] |
||||||
|
reply["candidatesTotal"] = stats["candidatesTotal"] |
||||||
|
reply["luck"] = stats["luck"] |
||||||
|
} |
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(reply) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error serializing API response: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) PaymentsIndex(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
|
||||||
|
reply := make(map[string]interface{}) |
||||||
|
stats := s.getStats() |
||||||
|
if stats != nil { |
||||||
|
reply["payments"] = stats["payments"] |
||||||
|
reply["paymentsTotal"] = stats["paymentsTotal"] |
||||||
|
} |
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(reply) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error serializing API response: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) AccountIndex(w http.ResponseWriter, r *http.Request) { |
||||||
|
w.Header().Set("Content-Type", "application/json; charset=UTF-8") |
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||||
|
w.Header().Set("Cache-Control", "no-cache") |
||||||
|
|
||||||
|
login := strings.ToLower(mux.Vars(r)["login"]) |
||||||
|
s.minersMu.Lock() |
||||||
|
defer s.minersMu.Unlock() |
||||||
|
|
||||||
|
reply, ok := s.miners[login] |
||||||
|
now := util.MakeTimestamp() |
||||||
|
cacheIntv := int64(s.statsIntv / time.Millisecond) |
||||||
|
// Refresh stats if stale
|
||||||
|
if !ok || reply.updatedAt < now-cacheIntv { |
||||||
|
exist, err := s.backend.IsMinerExists(login) |
||||||
|
if !exist { |
||||||
|
w.WriteHeader(http.StatusNotFound) |
||||||
|
return |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
log.Printf("Failed to fetch stats from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
stats, err := s.backend.GetMinerStats(login, s.config.Payments) |
||||||
|
if err != nil { |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
log.Printf("Failed to fetch stats from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
workers, err := s.backend.CollectWorkersStats(s.hashrateWindow, s.hashrateLargeWindow, login) |
||||||
|
if err != nil { |
||||||
|
w.WriteHeader(http.StatusInternalServerError) |
||||||
|
log.Printf("Failed to fetch stats from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
for key, value := range workers { |
||||||
|
stats[key] = value |
||||||
|
} |
||||||
|
stats["pageSize"] = s.config.Payments |
||||||
|
stats["minerCharts"], err = s.backend.GetMinerCharts(s.config.MinerChartsNum, login) |
||||||
|
stats["paymentCharts"], err = s.backend.GetPaymentCharts(login) |
||||||
|
reply = &Entry{stats: stats, updatedAt: now} |
||||||
|
s.miners[login] = reply |
||||||
|
} |
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK) |
||||||
|
err := json.NewEncoder(w).Encode(reply.stats) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error serializing API response: ", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ApiServer) getStats() map[string]interface{} { |
||||||
|
stats := s.stats.Load() |
||||||
|
if stats != nil { |
||||||
|
return stats.(map[string]interface{}) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
#!/bin/sh |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
if [ ! -f "build/env.sh" ]; then |
||||||
|
echo "$0 must be run from the root of the repository." |
||||||
|
exit 2 |
||||||
|
fi |
||||||
|
|
||||||
|
# Create fake Go workspace if it doesn't exist yet. |
||||||
|
workspace="$PWD/build/_workspace" |
||||||
|
root="$PWD" |
||||||
|
ethdir="$workspace/src/github.com/esprezzo" |
||||||
|
if [ ! -L "$ethdir/open-etc-pool-friends" ]; then |
||||||
|
mkdir -p "$ethdir" |
||||||
|
cd "$ethdir" |
||||||
|
ln -s ../../../../../. open-etc-pool-friends |
||||||
|
cd "$root" |
||||||
|
fi |
||||||
|
|
||||||
|
cd "$ethdir" |
||||||
|
mv "open-etc-pool-friends" "open-etc-pool-friends.old" |
||||||
|
ln -s ../../../../../. open-etc-pool-friends |
||||||
|
cd "$root" |
||||||
|
|
||||||
|
|
||||||
|
# Set up the environment to use the workspace. |
||||||
|
# Also add Godeps workspace so we build using canned dependencies. |
||||||
|
GOPATH="$workspace" |
||||||
|
GOBIN="$PWD/build/bin" |
||||||
|
export GOPATH GOBIN |
||||||
|
|
||||||
|
# Run the command inside the workspace. |
||||||
|
cd "$ethdir/open-etc-pool-friends" |
||||||
|
PWD="$ethdir/open-etc-pool-friends" |
||||||
|
|
||||||
|
# Launch the arguments with the configured environment. |
||||||
|
exec "$@" |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
{ |
||||||
|
"threads": 2, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8888", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8008", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 2000000000, |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"*/20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"*/20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.2:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
server { |
||||||
|
listen 80; |
||||||
|
listen [::]:80; |
||||||
|
|
||||||
|
root /var/www/etcpool; |
||||||
|
index index.html index.htm index.nginx-debian.html; |
||||||
|
|
||||||
|
server_name etc.yourdomain.name; |
||||||
|
|
||||||
|
location / { |
||||||
|
try_files $uri $uri/ =404; |
||||||
|
} |
||||||
|
|
||||||
|
location /api { |
||||||
|
proxy_pass http://127.0.0.1:8080/api; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
{ |
||||||
|
"threads": 2, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8888", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8008", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 2000000000, |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "0xd92fa5a9732a0aec36dc8d5a6a1305dc2d3e09e6", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8591", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": true, |
||||||
|
"requirePeers": 4, |
||||||
|
"interval": "1h", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "120s", |
||||||
|
"address": "0x3f156afdb248618892cb5089ba5a5fcac8ee0b01", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
{ |
||||||
|
"threads": 2, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8882", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8002", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 2000000000, |
||||||
|
"stratumHostname": "perkle-pool.esprezzo.io", |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
{ |
||||||
|
"threads": 4, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8884", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8004", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 4000000000, |
||||||
|
"stratumHostname": "perkle-pool.esprezzo.io", |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.2:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
{ |
||||||
|
"threads": 8, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8888", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8008", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 8000000000, |
||||||
|
"stratumHostname": "perkle-pool.esprezzo.io", |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.2:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 15, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "0x0f31986d7a0d4f160acd97583e3c3b591dcb5dde", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
{ |
||||||
|
"threads": 8, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8889", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8009", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 9000000000, |
||||||
|
"stratumHostname": "perkle-pool.esprezzo.io", |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.2:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 15, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": false, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "0x0f31986d7a0d4f160acd97583e3c3b591dcb5dde", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": true, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
{ |
||||||
|
"threads": 2, |
||||||
|
"coin": "prkl", |
||||||
|
"name": "main", |
||||||
|
"pplns": 9000, |
||||||
|
"proxy": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8888", |
||||||
|
"limitHeadersSize": 1024, |
||||||
|
"limitBodySize": 256, |
||||||
|
"behindReverseProxy": false, |
||||||
|
"stratum": { |
||||||
|
"enabled": true, |
||||||
|
"listen": "0.0.0.0:8008", |
||||||
|
"timeout": "120s", |
||||||
|
"maxConn": 8192 |
||||||
|
}, |
||||||
|
|
||||||
|
"blockRefreshInterval": "120ms", |
||||||
|
"stateUpdateInterval": "3s", |
||||||
|
"difficulty": 2000000000, |
||||||
|
"healthCheck": true, |
||||||
|
"maxFails": 100, |
||||||
|
"hashrateExpiration": "3h", |
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
"banning": { |
||||||
|
"enabled": false, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"api": { |
||||||
|
"enabled": false, |
||||||
|
"listen": "0.0.0.0:8080", |
||||||
|
"statsCollectInterval": "5s", |
||||||
|
"purgeInterval": "10m", |
||||||
|
"hashrateWindow": "30m", |
||||||
|
"hashrateLargeWindow": "3h", |
||||||
|
"luckWindow": [64, 128, 256], |
||||||
|
"payments": 50, |
||||||
|
"blocks": 50, |
||||||
|
"poolCharts":"0 */20 * * * *", |
||||||
|
"poolChartsNum":74, |
||||||
|
"minerCharts":"0 */20 * * * *", |
||||||
|
"minerChartsNum":74, |
||||||
|
"purgeOnly": false |
||||||
|
}, |
||||||
|
|
||||||
|
"upstreamCheckInterval": "5s", |
||||||
|
|
||||||
|
"upstream": [ |
||||||
|
{ |
||||||
|
"name": "main", |
||||||
|
"url": "http://40.121.105.44:8501", |
||||||
|
"timeout": "10s" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"name": "backup", |
||||||
|
"url": "http://127.0.0.1:8501", |
||||||
|
"timeout": "10s" |
||||||
|
} |
||||||
|
], |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 10, |
||||||
|
"database": 1, |
||||||
|
"password": "" |
||||||
|
}, |
||||||
|
|
||||||
|
"unlocker": { |
||||||
|
"enabled": true, |
||||||
|
"poolFee": 0.5, |
||||||
|
"poolFeeAddress": "0x0f31986d7a0d4f160acd97583e3c3b591dcb5dde", |
||||||
|
"donate": true, |
||||||
|
"depth": 120, |
||||||
|
"immatureDepth": 20, |
||||||
|
"keepTxFees": false, |
||||||
|
"interval": "10m", |
||||||
|
"daemon": "http://127.0.0.1:8501", |
||||||
|
"timeout": "120s" |
||||||
|
}, |
||||||
|
|
||||||
|
"payouts": { |
||||||
|
"enabled": false, |
||||||
|
"requirePeers": 5, |
||||||
|
"interval": "3h", |
||||||
|
"daemon": "http://127.0.0.1:8545", |
||||||
|
"timeout": "10s", |
||||||
|
"address": "0x", |
||||||
|
"autoGas": true, |
||||||
|
"gas": "21000", |
||||||
|
"gasPrice": "50000000000", |
||||||
|
"threshold": 1000000000, |
||||||
|
"bgsave": false, |
||||||
|
"concurrentTx": 5 |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,144 @@ |
|||||||
|
**First of all make sure your Redis instance and backups are configured properly http://redis.io/topics/persistence.** |
||||||
|
|
||||||
|
Keep in mind that pool maintains all balances in **Shannon**. |
||||||
|
|
||||||
|
# Processing and Resolving Payouts |
||||||
|
|
||||||
|
**You MUST run payouts module in a separate process**, ideally don't run it as daemon and process payouts 2-3 times per day and watch how it goes. **You must configure logging**, otherwise it can lead to big problems. |
||||||
|
|
||||||
|
Module will fetch accounts and sequentially process payouts. |
||||||
|
|
||||||
|
For every account who reached minimal threshold: |
||||||
|
|
||||||
|
* Check if we have enough peers on a node |
||||||
|
* Check that account is unlocked |
||||||
|
|
||||||
|
If any of checks fails, module will not even try to continue. |
||||||
|
|
||||||
|
* Check if we have enough money for payout (should not happen under normal circumstances) |
||||||
|
* Lock payments |
||||||
|
|
||||||
|
If payments can't be locked (another lock exist, usually after a failure) module will halt payouts. |
||||||
|
|
||||||
|
* Deduct balance of a miner and log pending payment |
||||||
|
* Submit a transaction to a node via `eth_sendTransaction` |
||||||
|
|
||||||
|
**If transaction submission fails, payouts will remain locked and halted in erroneous state.** |
||||||
|
|
||||||
|
If transaction submission was successful, we have a TX hash: |
||||||
|
|
||||||
|
* Write this TX hash to a database |
||||||
|
* Unlock payouts |
||||||
|
|
||||||
|
And so on. Repeat for every account. |
||||||
|
|
||||||
|
After payout session, payment module will perform `BGSAVE` (background saving) on Redis if you have enabled `bgsave` option. |
||||||
|
|
||||||
|
## Resolving Failed Payments (automatic) |
||||||
|
|
||||||
|
If your payout is not logged and not confirmed by Ethereum network you can resolve it automatically. You need to payouts in maintenance mode by setting up `RESOLVE_PAYOUT=1` or `RESOLVE_PAYOUT=True` environment variable: |
||||||
|
|
||||||
|
`RESOLVE_PAYOUT=1 ./build/bin/open-ethereum-pool payouts.json`. |
||||||
|
|
||||||
|
Payout module will fetch all rows from Redis with key `eth:payments:pending` and credit balance back to miners. Usually you will have only single entry there. |
||||||
|
|
||||||
|
If you see `No pending payments to resolve` we have no data about failed debits. |
||||||
|
|
||||||
|
If there was a debit operation performed which is not followed by actual money transfer (after `eth_sendTransaction` returned an error), you will likely see: |
||||||
|
|
||||||
|
``` |
||||||
|
Will credit back following balances: |
||||||
|
Address: 0x34AE12692BD4567A27e3E86411b58Ea6954BA773, Amount: 166798415 Shannon, 2016-05-11 08:14:34 |
||||||
|
``` |
||||||
|
|
||||||
|
followed by |
||||||
|
|
||||||
|
``` |
||||||
|
Credited 166798415 Shannon back to 0x34AE12692BD4567A27e3E86411b58Ea6954BA773 |
||||||
|
``` |
||||||
|
|
||||||
|
Usually every maintenance run ends with following message and halt: |
||||||
|
|
||||||
|
``` |
||||||
|
Payouts unlocked |
||||||
|
Now you have to restart payouts module with RESOLVE_PAYOUT=0 for normal run |
||||||
|
``` |
||||||
|
|
||||||
|
Unset `RESOLVE_PAYOUT=1` or run payouts with `RESOLVE_PAYOUT=0`. |
||||||
|
|
||||||
|
## Resolving Failed Payment (manual) |
||||||
|
|
||||||
|
You can perform manual maintenance using `geth` and `redis-cli` utilities. |
||||||
|
|
||||||
|
### Check For Failed Transactions: |
||||||
|
|
||||||
|
Perform the following command in a `redis-cli`: |
||||||
|
|
||||||
|
``` |
||||||
|
ZREVRANGE "eth:payments:pending" 0 -1 WITHSCORES |
||||||
|
``` |
||||||
|
|
||||||
|
Result will be like this: |
||||||
|
|
||||||
|
> 1) "0x34AE12692BD4567A27e3E86411b58Ea6954BA773:25000000" |
||||||
|
|
||||||
|
It's a pair of `LOGIN:AMOUNT`. |
||||||
|
|
||||||
|
>2) "1462920526" |
||||||
|
|
||||||
|
It's a `UNIXTIME` |
||||||
|
|
||||||
|
### Manual Payment Submission |
||||||
|
|
||||||
|
**Make sure there is no TX sent using block explorer. Skip this step if payment actually exist in a blockchain.** |
||||||
|
|
||||||
|
```javascript |
||||||
|
eth.sendTransaction({ |
||||||
|
from: eth.coinbase, |
||||||
|
to: '0x34AE12692BD4567A27e3E86411b58Ea6954BA773', |
||||||
|
value: web3.toWei(25000000, 'shannon') |
||||||
|
}) |
||||||
|
|
||||||
|
// => 0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331 |
||||||
|
``` |
||||||
|
|
||||||
|
**Write down tx hash**. |
||||||
|
|
||||||
|
### Store Payment in Redis |
||||||
|
|
||||||
|
Also usable for fixing missing payment entries. |
||||||
|
|
||||||
|
``` |
||||||
|
ZADD "eth:payments:all" 1462920526 0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331:0x34AE12692BD4567A27e3E86411b58Ea6954BA773:25000000 |
||||||
|
``` |
||||||
|
|
||||||
|
``` |
||||||
|
ZADD "eth:payments:0x34AE12692BD4567A27e3E86411b58Ea6954BA773" 1462920526 0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d1527331:25000000 |
||||||
|
``` |
||||||
|
|
||||||
|
### Delete Erroneous Payment Entry |
||||||
|
|
||||||
|
``` |
||||||
|
ZREM "eth:payments:pending" "0x34AE12692BD4567A27e3E86411b58Ea6954BA773:25000000" |
||||||
|
``` |
||||||
|
|
||||||
|
### Update Internal Stats |
||||||
|
|
||||||
|
``` |
||||||
|
HINCRBY "eth:finances" pending -25000000 |
||||||
|
HINCRBY "eth:finances" paid 25000000 |
||||||
|
``` |
||||||
|
|
||||||
|
### Unlock Payouts |
||||||
|
|
||||||
|
``` |
||||||
|
DEL "eth:payments:lock" |
||||||
|
``` |
||||||
|
|
||||||
|
## Resolving Missing Payment Entries |
||||||
|
|
||||||
|
If pool actually paid but didn't log transaction, scroll up to `Store Payment in Redis` section. You should have a transaction hash from block explorer. |
||||||
|
|
||||||
|
## Transaction Didn't Confirm |
||||||
|
|
||||||
|
If you are sure, just repeat it manually, you should have all the logs. |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
# Enforcing Policies |
||||||
|
|
||||||
|
Pool policy server collecting several stats on per IP basis. There are two options: `iptables+ipset` or simple application level bans. Banning is disabled by default. |
||||||
|
|
||||||
|
## Firewall Banning |
||||||
|
|
||||||
|
First you need to configure your firewall to use `ipset`, read [this article](https://wiki.archlinux.org/index.php/Ipset). |
||||||
|
|
||||||
|
Specify `ipset` name for banning in `policy` section. Timeout argument (in seconds) will be passed to this `ipset`. Stratum will use `os/exec` command like `sudo ipset add banlist x.x.x.x 1800` for banning, so you have to configure `sudo` properly and make sure that your system will never ask for password: |
||||||
|
|
||||||
|
Example `/etc/sudoers.d/pool` where `pool` is a username under which pool runs: |
||||||
|
|
||||||
|
pool ALL=NOPASSWD: /sbin/ipset |
||||||
|
|
||||||
|
If you need something simple, just set `ipset` name to blank string and simple application level banning will be used instead. |
||||||
|
|
||||||
|
## Limiting |
||||||
|
|
||||||
|
Under some weird circumstances you can enforce limits to prevent connection flood to stratum, there are initial settings: `limit` and `limitJump`. Policy server will increase number of allowed connections per IP address on each valid share submission. Stratum will not enforce this policy for a `grace` period specified after stratum start. |
||||||
@ -0,0 +1,143 @@ |
|||||||
|
# Stratum Mining Protocol |
||||||
|
|
||||||
|
This is the description of stratum protocol used in this pool. |
||||||
|
|
||||||
|
Stratum defines simple exception handling. Example of rejected share looks like: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: 23, message: "Invalid share" } } |
||||||
|
``` |
||||||
|
|
||||||
|
Each response with exception is followed by disconnect. |
||||||
|
|
||||||
|
## Authentication |
||||||
|
|
||||||
|
Request looks like: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"id": 1, |
||||||
|
"jsonrpc": "2.0", |
||||||
|
"method": "eth_submitLogin", |
||||||
|
"params": ["0x34AE12692BD4567A27e3E86411b58Ea6954BA773"] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Request can include additional 2nd param (email for example): |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"id": 1, |
||||||
|
"jsonrpc": "2.0", |
||||||
|
"method": "eth_submitLogin", |
||||||
|
"params": ["0x34AE12692BD4567A27e3E86411b58Ea6954BA773", "admin@example.net"] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Successful response: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": true } |
||||||
|
``` |
||||||
|
|
||||||
|
Exceptions: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: -1, message: "Invalid login" } } |
||||||
|
``` |
||||||
|
|
||||||
|
## Request For Job |
||||||
|
|
||||||
|
Request looks like: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "method": "eth_getWork" } |
||||||
|
``` |
||||||
|
|
||||||
|
Successful response: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"id": 1, |
||||||
|
"jsonrpc": "2.0", |
||||||
|
"result": [ |
||||||
|
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"0x5eed00000000000000000000000000005eed0000000000000000000000000000", |
||||||
|
"0xd1ff1c01710000000000000000000000d1ff1c01710000000000000000000000" |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Exceptions: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 10, "result": null, "error": { code: 0, message: "Work not ready" } } |
||||||
|
``` |
||||||
|
|
||||||
|
## New Job Notification |
||||||
|
|
||||||
|
Server sends job to peers if new job is available: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"jsonrpc": "2.0", |
||||||
|
"result": [ |
||||||
|
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"0x5eed00000000000000000000000000005eed0000000000000000000000000000", |
||||||
|
"0xd1ff1c01710000000000000000000000d1ff1c01710000000000000000000000" |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Share Submission |
||||||
|
|
||||||
|
Request looks like: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ |
||||||
|
"id": 1, |
||||||
|
"jsonrpc": "2.0", |
||||||
|
"method": "eth_submitWork", |
||||||
|
"params": [ |
||||||
|
"0xe05d1fd4002d962f", |
||||||
|
"0x6c872e2304cd1e64b553a65387d7383470f22331aff288cbce5748dc430f016a", |
||||||
|
"0x2b20a6c641ed155b893ee750ef90ec3be5d24736d16838b84759385b6724220d" |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Request can include optional `worker` param: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "worker": "rig-1" /* ... */ } |
||||||
|
``` |
||||||
|
|
||||||
|
Response: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": true } |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": false } |
||||||
|
``` |
||||||
|
|
||||||
|
Exceptions: |
||||||
|
|
||||||
|
Pool MAY return exception on invalid share submission usually followed by temporal ban. |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: 23, message: "Invalid share" } } |
||||||
|
``` |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: 22, message: "Duplicate share" } } |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: -1, message: "High rate of invalid shares" } } |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: 25, message: "Not subscribed" } } |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": null, "error": { code: -1, message: "Malformed PoW result" } } |
||||||
|
``` |
||||||
|
|
||||||
|
## Submit Hashrate |
||||||
|
|
||||||
|
`eth_submitHashrate` is a nonsense method. Pool ignores it and the reply is always: |
||||||
|
|
||||||
|
```javascript |
||||||
|
{ "id": 1, "jsonrpc": "2.0", "result": true } |
||||||
|
``` |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
// +build go1.9
|
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"log" |
||||||
|
"math/rand" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"runtime" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/yvasiyarov/gorelic" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/api" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/payouts" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/proxy" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
) |
||||||
|
|
||||||
|
var cfg proxy.Config |
||||||
|
var backend *storage.RedisClient |
||||||
|
|
||||||
|
func startProxy() { |
||||||
|
s := proxy.NewProxy(&cfg, backend) |
||||||
|
s.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func startApi() { |
||||||
|
s := api.NewApiServer(&cfg.Api, backend) |
||||||
|
s.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func startBlockUnlocker() { |
||||||
|
u := payouts.NewBlockUnlocker(&cfg.BlockUnlocker, backend) |
||||||
|
u.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func startPayoutsProcessor() { |
||||||
|
u := payouts.NewPayoutsProcessor(&cfg.Payouts, backend) |
||||||
|
u.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func startNewrelic() { |
||||||
|
if cfg.NewrelicEnabled { |
||||||
|
nr := gorelic.NewAgent() |
||||||
|
nr.Verbose = cfg.NewrelicVerbose |
||||||
|
nr.NewrelicLicense = cfg.NewrelicKey |
||||||
|
nr.NewrelicName = cfg.NewrelicName |
||||||
|
nr.Run() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func readConfig(cfg *proxy.Config) { |
||||||
|
configFileName := "config.json" |
||||||
|
if len(os.Args) > 1 { |
||||||
|
configFileName = os.Args[1] |
||||||
|
} |
||||||
|
configFileName, _ = filepath.Abs(configFileName) |
||||||
|
log.Printf("Loading config: %v", configFileName) |
||||||
|
|
||||||
|
configFile, err := os.Open(configFileName) |
||||||
|
if err != nil { |
||||||
|
log.Fatal("File error: ", err.Error()) |
||||||
|
} |
||||||
|
defer configFile.Close() |
||||||
|
jsonParser := json.NewDecoder(configFile) |
||||||
|
if err := jsonParser.Decode(&cfg); err != nil { |
||||||
|
log.Fatal("Config error: ", err.Error()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
readConfig(&cfg) |
||||||
|
rand.Seed(time.Now().UnixNano()) |
||||||
|
|
||||||
|
if cfg.Threads > 0 { |
||||||
|
runtime.GOMAXPROCS(cfg.Threads) |
||||||
|
log.Printf("Running with %v threads", cfg.Threads) |
||||||
|
} |
||||||
|
|
||||||
|
startNewrelic() |
||||||
|
|
||||||
|
backend = storage.NewRedisClient(&cfg.Redis, cfg.Coin, cfg.Pplns) |
||||||
|
pong, err := backend.Check() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Can't establish connection to backend: %v", err) |
||||||
|
} else { |
||||||
|
log.Printf("Backend check reply: %v", pong) |
||||||
|
} |
||||||
|
|
||||||
|
if cfg.Proxy.Enabled { |
||||||
|
go startProxy() |
||||||
|
} |
||||||
|
if cfg.Api.Enabled { |
||||||
|
go startApi() |
||||||
|
} |
||||||
|
if cfg.BlockUnlocker.Enabled { |
||||||
|
go startBlockUnlocker() |
||||||
|
} |
||||||
|
if cfg.Payouts.Enabled { |
||||||
|
go startPayoutsProcessor() |
||||||
|
} |
||||||
|
quit := make(chan bool) |
||||||
|
<-quit |
||||||
|
} |
||||||
@ -0,0 +1,335 @@ |
|||||||
|
package payouts |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"math/big" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
const txCheckInterval = 5 * time.Second |
||||||
|
|
||||||
|
type PayoutsConfig struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
RequirePeers int64 `json:"requirePeers"` |
||||||
|
Interval string `json:"interval"` |
||||||
|
Daemon string `json:"daemon"` |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
Address string `json:"address"` |
||||||
|
Gas string `json:"gas"` |
||||||
|
GasPrice string `json:"gasPrice"` |
||||||
|
AutoGas bool `json:"autoGas"` |
||||||
|
// In Shannon
|
||||||
|
Threshold int64 `json:"threshold"` |
||||||
|
BgSave bool `json:"bgsave"` |
||||||
|
ConcurrentTx int `json:"concurrentTx"` |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsConfig) GasHex() string { |
||||||
|
x := util.String2Big(self.Gas) |
||||||
|
return hexutil.EncodeBig(x) |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsConfig) GasPriceHex() string { |
||||||
|
x := util.String2Big(self.GasPrice) |
||||||
|
return hexutil.EncodeBig(x) |
||||||
|
} |
||||||
|
|
||||||
|
type PayoutsProcessor struct { |
||||||
|
config *PayoutsConfig |
||||||
|
backend *storage.RedisClient |
||||||
|
rpc *rpc.RPCClient |
||||||
|
halt bool |
||||||
|
lastFail error |
||||||
|
} |
||||||
|
|
||||||
|
func NewPayoutsProcessor(cfg *PayoutsConfig, backend *storage.RedisClient) *PayoutsProcessor { |
||||||
|
u := &PayoutsProcessor{config: cfg, backend: backend} |
||||||
|
u.rpc = rpc.NewRPCClient("PayoutsProcessor", cfg.Daemon, cfg.Timeout) |
||||||
|
return u |
||||||
|
} |
||||||
|
|
||||||
|
func (u *PayoutsProcessor) Start() { |
||||||
|
log.Println("Starting payouts") |
||||||
|
|
||||||
|
if u.mustResolvePayout() { |
||||||
|
log.Println("Running with env RESOLVE_PAYOUT=1, now trying to resolve locked payouts") |
||||||
|
u.resolvePayouts() |
||||||
|
log.Println("Now you have to restart payouts module with RESOLVE_PAYOUT=0 for normal run") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
intv := util.MustParseDuration(u.config.Interval) |
||||||
|
timer := time.NewTimer(intv) |
||||||
|
log.Printf("Set payouts interval to %v", intv) |
||||||
|
|
||||||
|
payments := u.backend.GetPendingPayments() |
||||||
|
if len(payments) > 0 { |
||||||
|
log.Printf("Previous payout failed, you have to resolve it. List of failed payments:\n %v", |
||||||
|
formatPendingPayments(payments)) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
locked, err := u.backend.IsPayoutsLocked() |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to start payouts:", err) |
||||||
|
return |
||||||
|
} |
||||||
|
if locked { |
||||||
|
log.Println("Unable to start payouts because they are locked") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Immediately process payouts after start
|
||||||
|
u.process() |
||||||
|
timer.Reset(intv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-timer.C: |
||||||
|
u.process() |
||||||
|
timer.Reset(intv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
func (u *PayoutsProcessor) process() { |
||||||
|
if u.halt { |
||||||
|
log.Println("Payments suspended due to last critical error:", u.lastFail) |
||||||
|
os.Exit(1) |
||||||
|
return |
||||||
|
} |
||||||
|
mustPay := 0 |
||||||
|
minersPaid := 0 |
||||||
|
totalAmount := big.NewInt(0) |
||||||
|
payees, err := u.backend.GetPayees() |
||||||
|
if err != nil { |
||||||
|
log.Println("Error while retrieving payees from backend:", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
waitingCount := 0 |
||||||
|
var wg sync.WaitGroup |
||||||
|
|
||||||
|
for _, login := range payees { |
||||||
|
amount, _ := u.backend.GetBalance(login) |
||||||
|
amountInShannon := big.NewInt(amount) |
||||||
|
|
||||||
|
// Shannon^2 = Wei
|
||||||
|
amountInWei := new(big.Int).Mul(amountInShannon, util.Shannon) |
||||||
|
|
||||||
|
if !u.reachedThreshold(amountInShannon) { |
||||||
|
continue |
||||||
|
} |
||||||
|
mustPay++ |
||||||
|
|
||||||
|
// Require active peers before processing
|
||||||
|
if !u.checkPeers() { |
||||||
|
break |
||||||
|
} |
||||||
|
// Require unlocked account
|
||||||
|
if !u.isUnlockedAccount() { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Check if we have enough funds
|
||||||
|
poolBalance, err := u.rpc.GetBalance(u.config.Address) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
if poolBalance.Cmp(amountInWei) < 0 { |
||||||
|
err := fmt.Errorf("Not enough balance for payment, need %s Wei, pool has %s Wei", |
||||||
|
amountInWei.String(), poolBalance.String()) |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
// Lock payments for current payout
|
||||||
|
err = u.backend.LockPayouts(login, amount) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to lock payment for %s: %v", login, err) |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
log.Printf("Locked payment for %s, %v Shannon", login, amount) |
||||||
|
|
||||||
|
// Debit miner's balance and update stats
|
||||||
|
err = u.backend.UpdateBalance(login, amount) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to update balance for %s, %v Shannon: %v", login, amount, err) |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
value := hexutil.EncodeBig(amountInWei) |
||||||
|
txHash, err := u.rpc.SendTransaction(u.config.Address, login, u.config.GasHex(), u.config.GasPriceHex(), value, u.config.AutoGas) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to send payment to %s, %v Shannon: %v. Check outgoing tx for %s in block explorer and docs/PAYOUTS.md", |
||||||
|
login, amount, err, login) |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
if postCommand, present := os.LookupEnv("POST_PAYOUT_HOOK"); present { |
||||||
|
go func(postCommand string, login string, value string) { |
||||||
|
out, err := exec.Command(postCommand, login, value).CombinedOutput() |
||||||
|
if err != nil { |
||||||
|
log.Printf("WARNING: Error running post payout hook: %s", err.Error()) |
||||||
|
} |
||||||
|
log.Printf("Running post payout hook with result: %s", out) |
||||||
|
}(postCommand, login, value) |
||||||
|
} |
||||||
|
|
||||||
|
// Log transaction hash
|
||||||
|
err = u.backend.WritePayment(login, txHash, amount) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to log payment data for %s, %v Shannon, tx: %s: %v", login, amount, txHash, err) |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
minersPaid++ |
||||||
|
totalAmount.Add(totalAmount, big.NewInt(amount)) |
||||||
|
log.Printf("Paid %v Shannon to %v, TxHash: %v", amount, login, txHash) |
||||||
|
|
||||||
|
wg.Add(1) |
||||||
|
waitingCount++ |
||||||
|
go func(txHash string, login string, wg *sync.WaitGroup) { |
||||||
|
// Wait for TX confirmation before further payouts
|
||||||
|
for { |
||||||
|
log.Printf("Waiting for tx confirmation: %v", txHash) |
||||||
|
time.Sleep(txCheckInterval) |
||||||
|
receipt, err := u.rpc.GetTxReceipt(txHash) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to get tx receipt for %v: %v", txHash, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
// Tx has been mined
|
||||||
|
if receipt != nil && receipt.Confirmed() { |
||||||
|
if receipt.Successful() { |
||||||
|
log.Printf("Payout tx successful for %s: %s", login, txHash) |
||||||
|
} else { |
||||||
|
log.Printf("Payout tx failed for %s: %s. Address contract throws on incoming tx.", login, txHash) |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
wg.Done() |
||||||
|
}(txHash, login, &wg) |
||||||
|
|
||||||
|
if waitingCount > u.config.ConcurrentTx { |
||||||
|
wg.Wait() |
||||||
|
waitingCount = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
wg.Wait() |
||||||
|
waitingCount = 0 |
||||||
|
|
||||||
|
if mustPay > 0 { |
||||||
|
log.Printf("Paid total %v Shannon to %v of %v payees", totalAmount, minersPaid, mustPay) |
||||||
|
} else { |
||||||
|
log.Println("No payees that have reached payout threshold") |
||||||
|
} |
||||||
|
|
||||||
|
// Save redis state to disk
|
||||||
|
if minersPaid > 0 && u.config.BgSave { |
||||||
|
u.bgSave() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) isUnlockedAccount() bool { |
||||||
|
_, err := self.rpc.Sign(self.config.Address, "0x0") |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to process payouts:", err) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) checkPeers() bool { |
||||||
|
n, err := self.rpc.GetPeerCount() |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to start payouts, failed to retrieve number of peers from node:", err) |
||||||
|
return false |
||||||
|
} |
||||||
|
if n < self.config.RequirePeers { |
||||||
|
log.Println("Unable to start payouts, number of peers on a node is less than required", self.config.RequirePeers) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) reachedThreshold(amount *big.Int) bool { |
||||||
|
return big.NewInt(self.config.Threshold).Cmp(amount) < 0 |
||||||
|
} |
||||||
|
|
||||||
|
func formatPendingPayments(list []*storage.PendingPayment) string { |
||||||
|
var s string |
||||||
|
for _, v := range list { |
||||||
|
s += fmt.Sprintf("\tAddress: %s, Amount: %v Shannon, %v\n", v.Address, v.Amount, time.Unix(v.Timestamp, 0)) |
||||||
|
} |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) bgSave() { |
||||||
|
result, err := self.backend.BgSave() |
||||||
|
if err != nil { |
||||||
|
log.Println("Failed to perform BGSAVE on backend:", err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.Println("Saving backend state to disk:", result) |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) resolvePayouts() { |
||||||
|
payments := self.backend.GetPendingPayments() |
||||||
|
|
||||||
|
if len(payments) > 0 { |
||||||
|
log.Printf("Will credit back following balances:\n%s", formatPendingPayments(payments)) |
||||||
|
|
||||||
|
for _, v := range payments { |
||||||
|
err := self.backend.RollbackBalance(v.Address, v.Amount) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to credit %v Shannon back to %s, error is: %v", v.Amount, v.Address, err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.Printf("Credited %v Shannon back to %s", v.Amount, v.Address) |
||||||
|
} |
||||||
|
err := self.backend.UnlockPayouts() |
||||||
|
if err != nil { |
||||||
|
log.Println("Failed to unlock payouts:", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.Println("No pending payments to resolve") |
||||||
|
} |
||||||
|
|
||||||
|
if self.config.BgSave { |
||||||
|
self.bgSave() |
||||||
|
} |
||||||
|
log.Println("Payouts unlocked") |
||||||
|
} |
||||||
|
|
||||||
|
func (self PayoutsProcessor) mustResolvePayout() bool { |
||||||
|
v, _ := strconv.ParseBool(os.Getenv("RESOLVE_PAYOUT")) |
||||||
|
return v |
||||||
|
} |
||||||
@ -0,0 +1,545 @@ |
|||||||
|
package payouts |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"math/big" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common/math" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
type UnlockerConfig struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
PoolFee float64 `json:"poolFee"` |
||||||
|
PoolFeeAddress string `json:"poolFeeAddress"` |
||||||
|
Donate bool `json:"donate"` |
||||||
|
Depth int64 `json:"depth"` |
||||||
|
ImmatureDepth int64 `json:"immatureDepth"` |
||||||
|
KeepTxFees bool `json:"keepTxFees"` |
||||||
|
Interval string `json:"interval"` |
||||||
|
Daemon string `json:"daemon"` |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
} |
||||||
|
|
||||||
|
const minDepth = 16 |
||||||
|
|
||||||
|
var constReward = math.MustParseBig256("3200000000000000000") |
||||||
|
var uncleReward = new(big.Int).Div(constReward, new(big.Int).SetInt64(32)) |
||||||
|
|
||||||
|
// Donate 5% from pool fees to developers
|
||||||
|
const donationFee = 5.0 |
||||||
|
const donationAccount = "0xd92fa5a9732a0aec36dc8d5a6a1305dc2d3e09e6" |
||||||
|
|
||||||
|
type BlockUnlocker struct { |
||||||
|
config *UnlockerConfig |
||||||
|
backend *storage.RedisClient |
||||||
|
rpc *rpc.RPCClient |
||||||
|
halt bool |
||||||
|
lastFail error |
||||||
|
} |
||||||
|
|
||||||
|
func NewBlockUnlocker(cfg *UnlockerConfig, backend *storage.RedisClient) *BlockUnlocker { |
||||||
|
if len(cfg.PoolFeeAddress) != 0 && !util.IsValidHexAddress(cfg.PoolFeeAddress) { |
||||||
|
log.Fatalln("Invalid poolFeeAddress", cfg.PoolFeeAddress) |
||||||
|
} |
||||||
|
if cfg.Depth < minDepth*2 { |
||||||
|
log.Fatalf("Block maturity depth can't be < %v, your depth is %v", minDepth*2, cfg.Depth) |
||||||
|
} |
||||||
|
if cfg.ImmatureDepth < minDepth { |
||||||
|
log.Fatalf("Immature depth can't be < %v, your depth is %v", minDepth, cfg.ImmatureDepth) |
||||||
|
} |
||||||
|
u := &BlockUnlocker{config: cfg, backend: backend} |
||||||
|
u.rpc = rpc.NewRPCClient("BlockUnlocker", cfg.Daemon, cfg.Timeout) |
||||||
|
return u |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) Start() { |
||||||
|
log.Println("Starting block unlocker") |
||||||
|
intv := util.MustParseDuration(u.config.Interval) |
||||||
|
timer := time.NewTimer(intv) |
||||||
|
log.Printf("Set block unlock interval to %v", intv) |
||||||
|
|
||||||
|
// Immediately unlock after start
|
||||||
|
u.unlockPendingBlocks() |
||||||
|
u.unlockAndCreditMiners() |
||||||
|
timer.Reset(intv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-timer.C: |
||||||
|
u.unlockPendingBlocks() |
||||||
|
u.unlockAndCreditMiners() |
||||||
|
timer.Reset(intv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
type UnlockResult struct { |
||||||
|
maturedBlocks []*storage.BlockData |
||||||
|
orphanedBlocks []*storage.BlockData |
||||||
|
orphans int |
||||||
|
uncles int |
||||||
|
blocks int |
||||||
|
} |
||||||
|
|
||||||
|
/* Geth does not provide consistent state when you need both new height and new job, |
||||||
|
* so in redis I am logging just what I have in a pool state on the moment when block found. |
||||||
|
* Having very likely incorrect height in database results in a weird block unlocking scheme, |
||||||
|
* when I have to check what the hell we actually found and traversing all the blocks with height-N and height+N |
||||||
|
* to make sure we will find it. We can't rely on round height here, it's just a reference point. |
||||||
|
* ISSUE: https://github.com/ethereum/go-ethereum/issues/2333
|
||||||
|
*/ |
||||||
|
func (u *BlockUnlocker) unlockCandidates(candidates []*storage.BlockData) (*UnlockResult, error) { |
||||||
|
result := &UnlockResult{} |
||||||
|
|
||||||
|
// Data row is: "height:nonce:powHash:mixDigest:timestamp:diff:totalShares"
|
||||||
|
for _, candidate := range candidates { |
||||||
|
orphan := true |
||||||
|
|
||||||
|
/* Search for a normal block with wrong height here by traversing 16 blocks back and forward. |
||||||
|
* Also we are searching for a block that can include this one as uncle. |
||||||
|
*/ |
||||||
|
for i := int64(minDepth * -1); i < minDepth; i++ { |
||||||
|
height := candidate.Height + i |
||||||
|
|
||||||
|
if height < 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
block, err := u.rpc.GetBlockByHeight(height) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while retrieving block %v from node: %v", height, err) |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if block == nil { |
||||||
|
return nil, fmt.Errorf("Error while retrieving block %v from node, wrong node height", height) |
||||||
|
} |
||||||
|
|
||||||
|
if matchCandidate(block, candidate) { |
||||||
|
orphan = false |
||||||
|
result.blocks++ |
||||||
|
|
||||||
|
err = u.handleBlock(block, candidate) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
result.maturedBlocks = append(result.maturedBlocks, candidate) |
||||||
|
log.Printf("Mature block %v with %v tx, hash: %v", candidate.Height, len(block.Transactions), candidate.Hash[0:10]) |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
if len(block.Uncles) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Trying to find uncle in current block during our forward check
|
||||||
|
for uncleIndex, uncleHash := range block.Uncles { |
||||||
|
uncle, err := u.rpc.GetUncleByBlockNumberAndIndex(height, uncleIndex) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("Error while retrieving uncle of block %v from node: %v", uncleHash, err) |
||||||
|
} |
||||||
|
if uncle == nil { |
||||||
|
return nil, fmt.Errorf("Error while retrieving uncle of block %v from node", height) |
||||||
|
} |
||||||
|
|
||||||
|
// Found uncle
|
||||||
|
if matchCandidate(uncle, candidate) { |
||||||
|
orphan = false |
||||||
|
result.uncles++ |
||||||
|
|
||||||
|
err := handleUncle(height, uncle, candidate) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
result.maturedBlocks = append(result.maturedBlocks, candidate) |
||||||
|
log.Printf("Mature uncle %v/%v of reward %v with hash: %v", candidate.Height, candidate.UncleHeight, |
||||||
|
util.FormatReward(candidate.Reward), uncle.Hash[0:10]) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
// Found block or uncle
|
||||||
|
if !orphan { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
// Block is lost, we didn't find any valid block or uncle matching our data in a blockchain
|
||||||
|
if orphan { |
||||||
|
result.orphans++ |
||||||
|
candidate.Orphan = true |
||||||
|
result.orphanedBlocks = append(result.orphanedBlocks, candidate) |
||||||
|
log.Printf("Orphaned block %v:%v", candidate.RoundHeight, candidate.Nonce) |
||||||
|
} |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
func matchCandidate(block *rpc.GetBlockReply, candidate *storage.BlockData) bool { |
||||||
|
// Just compare hash if block is unlocked as immature
|
||||||
|
if len(candidate.Hash) > 0 && strings.EqualFold(candidate.Hash, block.Hash) { |
||||||
|
return true |
||||||
|
} |
||||||
|
// Geth-style candidate matching
|
||||||
|
if len(block.Nonce) > 0 { |
||||||
|
return strings.EqualFold(block.Nonce, candidate.Nonce) |
||||||
|
} |
||||||
|
// Parity's EIP: https://github.com/ethereum/EIPs/issues/95
|
||||||
|
if len(block.SealFields) == 2 { |
||||||
|
return strings.EqualFold(candidate.Nonce, block.SealFields[1]) |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) handleBlock(block *rpc.GetBlockReply, candidate *storage.BlockData) error { |
||||||
|
// Initial 5 Ether static reward
|
||||||
|
reward := new(big.Int).Set(constReward) |
||||||
|
|
||||||
|
correctHeight, err := strconv.ParseInt(strings.Replace(block.Number, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
candidate.Height = correctHeight |
||||||
|
|
||||||
|
// Add TX fees
|
||||||
|
extraTxReward, err := u.getExtraRewardForTx(block) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("Error while fetching TX receipt: %v", err) |
||||||
|
} |
||||||
|
if u.config.KeepTxFees { |
||||||
|
candidate.ExtraReward = extraTxReward |
||||||
|
} else { |
||||||
|
reward.Add(reward, extraTxReward) |
||||||
|
} |
||||||
|
|
||||||
|
// Add reward for including uncles
|
||||||
|
rewardForUncles := big.NewInt(0).Mul(uncleReward, big.NewInt(int64(len(block.Uncles)))) |
||||||
|
reward.Add(reward, rewardForUncles) |
||||||
|
|
||||||
|
candidate.Orphan = false |
||||||
|
candidate.Hash = block.Hash |
||||||
|
candidate.Reward = reward |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func handleUncle(height int64, uncle *rpc.GetBlockReply, candidate *storage.BlockData) error { |
||||||
|
uncleHeight, err := strconv.ParseInt(strings.Replace(uncle.Number, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
reward := getUncleReward(uncleHeight, height) |
||||||
|
candidate.Height = height |
||||||
|
candidate.UncleHeight = uncleHeight |
||||||
|
candidate.Orphan = false |
||||||
|
candidate.Hash = uncle.Hash |
||||||
|
candidate.Reward = reward |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) unlockPendingBlocks() { |
||||||
|
if u.halt { |
||||||
|
log.Println("Unlocking suspended due to last critical error:", u.lastFail) |
||||||
|
os.Exit(1) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
current, err := u.rpc.GetPendingBlock() |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Unable to get current blockchain height from node: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
currentHeight, err := strconv.ParseInt(strings.Replace(current.Number, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Can't parse pending block number: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
candidates, err := u.backend.GetCandidates(currentHeight - u.config.ImmatureDepth) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to get block candidates from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if len(candidates) == 0 { |
||||||
|
log.Println("No block candidates to unlock") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
result, err := u.unlockCandidates(candidates) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to unlock blocks: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.Printf("Immature %v blocks, %v uncles, %v orphans", result.blocks, result.uncles, result.orphans) |
||||||
|
|
||||||
|
err = u.backend.WritePendingOrphans(result.orphanedBlocks) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to insert orphaned blocks into backend: %v", err) |
||||||
|
return |
||||||
|
} else { |
||||||
|
log.Printf("Inserted %v orphaned blocks to backend", result.orphans) |
||||||
|
} |
||||||
|
|
||||||
|
totalRevenue := new(big.Rat) |
||||||
|
totalMinersProfit := new(big.Rat) |
||||||
|
totalPoolProfit := new(big.Rat) |
||||||
|
|
||||||
|
for _, block := range result.maturedBlocks { |
||||||
|
revenue, minersProfit, poolProfit, roundRewards, percents, err := u.calculateRewards(block) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to calculate rewards for round %v: %v", block.RoundKey(), err) |
||||||
|
return |
||||||
|
} |
||||||
|
err = u.backend.WriteImmatureBlock(block, roundRewards) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to credit rewards for round %v: %v", block.RoundKey(), err) |
||||||
|
return |
||||||
|
} |
||||||
|
totalRevenue.Add(totalRevenue, revenue) |
||||||
|
totalMinersProfit.Add(totalMinersProfit, minersProfit) |
||||||
|
totalPoolProfit.Add(totalPoolProfit, poolProfit) |
||||||
|
|
||||||
|
logEntry := fmt.Sprintf( |
||||||
|
"IMMATURE %v: revenue %v, miners profit %v, pool profit: %v", |
||||||
|
block.RoundKey(), |
||||||
|
util.FormatRatReward(revenue), |
||||||
|
util.FormatRatReward(minersProfit), |
||||||
|
util.FormatRatReward(poolProfit), |
||||||
|
) |
||||||
|
entries := []string{logEntry} |
||||||
|
for login, reward := range roundRewards { |
||||||
|
entries = append(entries, fmt.Sprintf("\tREWARD %v: %v: %v Shannon", block.RoundKey(), login, reward)) |
||||||
|
per := new(big.Rat) |
||||||
|
if val, ok := percents[login]; ok { |
||||||
|
per = val |
||||||
|
} |
||||||
|
u.backend.WriteReward(login, reward, per, true, block) |
||||||
|
} |
||||||
|
log.Println(strings.Join(entries, "\n")) |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf( |
||||||
|
"IMMATURE SESSION: revenue %v, miners profit %v, pool profit: %v", |
||||||
|
util.FormatRatReward(totalRevenue), |
||||||
|
util.FormatRatReward(totalMinersProfit), |
||||||
|
util.FormatRatReward(totalPoolProfit), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) unlockAndCreditMiners() { |
||||||
|
if u.halt { |
||||||
|
log.Println("Unlocking suspended due to last critical error:", u.lastFail) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
current, err := u.rpc.GetPendingBlock() |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Unable to get current blockchain height from node: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
currentHeight, err := strconv.ParseInt(strings.Replace(current.Number, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Can't parse pending block number: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
immature, err := u.backend.GetImmatureBlocks(currentHeight - u.config.Depth) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to get block candidates from backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if len(immature) == 0 { |
||||||
|
log.Println("No immature blocks to credit miners") |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
result, err := u.unlockCandidates(immature) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to unlock blocks: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.Printf("Unlocked %v blocks, %v uncles, %v orphans", result.blocks, result.uncles, result.orphans) |
||||||
|
|
||||||
|
for _, block := range result.orphanedBlocks { |
||||||
|
err = u.backend.WriteOrphan(block) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to insert orphaned block into backend: %v", err) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
log.Printf("Inserted %v orphaned blocks to backend", result.orphans) |
||||||
|
|
||||||
|
totalRevenue := new(big.Rat) |
||||||
|
totalMinersProfit := new(big.Rat) |
||||||
|
totalPoolProfit := new(big.Rat) |
||||||
|
|
||||||
|
for _, block := range result.maturedBlocks { |
||||||
|
revenue, minersProfit, poolProfit, roundRewards, percents, err := u.calculateRewards(block) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to calculate rewards for round %v: %v", block.RoundKey(), err) |
||||||
|
return |
||||||
|
} |
||||||
|
err = u.backend.WriteMaturedBlock(block, roundRewards) |
||||||
|
if err != nil { |
||||||
|
u.halt = true |
||||||
|
u.lastFail = err |
||||||
|
log.Printf("Failed to credit rewards for round %v: %v", block.RoundKey(), err) |
||||||
|
return |
||||||
|
} |
||||||
|
totalRevenue.Add(totalRevenue, revenue) |
||||||
|
totalMinersProfit.Add(totalMinersProfit, minersProfit) |
||||||
|
totalPoolProfit.Add(totalPoolProfit, poolProfit) |
||||||
|
|
||||||
|
logEntry := fmt.Sprintf( |
||||||
|
"MATURED %v: revenue %v, miners profit %v, pool profit: %v", |
||||||
|
block.RoundKey(), |
||||||
|
util.FormatRatReward(revenue), |
||||||
|
util.FormatRatReward(minersProfit), |
||||||
|
util.FormatRatReward(poolProfit), |
||||||
|
) |
||||||
|
entries := []string{logEntry} |
||||||
|
for login, reward := range roundRewards { |
||||||
|
entries = append(entries, fmt.Sprintf("\tREWARD %v: %v: %v Shannon", block.RoundKey(), login, reward)) |
||||||
|
per := new(big.Rat) |
||||||
|
if val, ok := percents[login]; ok { |
||||||
|
per = val |
||||||
|
} |
||||||
|
u.backend.WriteReward(login, reward, per, false, block) |
||||||
|
} |
||||||
|
log.Println(strings.Join(entries, "\n")) |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf( |
||||||
|
"MATURE SESSION: revenue %v, miners profit %v, pool profit: %v", |
||||||
|
util.FormatRatReward(totalRevenue), |
||||||
|
util.FormatRatReward(totalMinersProfit), |
||||||
|
util.FormatRatReward(totalPoolProfit), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) calculateRewards(block *storage.BlockData) (*big.Rat, *big.Rat, *big.Rat, map[string]int64, map[string]*big.Rat, error) { |
||||||
|
revenue := new(big.Rat).SetInt(block.Reward) |
||||||
|
minersProfit, poolProfit := chargeFee(revenue, u.config.PoolFee) |
||||||
|
|
||||||
|
shares, err := u.backend.GetRoundShares(block.RoundHeight, block.Nonce) |
||||||
|
if err != nil { |
||||||
|
return nil, nil, nil, nil, nil, err |
||||||
|
} |
||||||
|
|
||||||
|
totalShares := int64(0) |
||||||
|
for _, val := range shares { |
||||||
|
totalShares += val |
||||||
|
} |
||||||
|
|
||||||
|
rewards, percents := calculateRewardsForShares(shares, totalShares, minersProfit) |
||||||
|
|
||||||
|
if block.ExtraReward != nil { |
||||||
|
extraReward := new(big.Rat).SetInt(block.ExtraReward) |
||||||
|
poolProfit.Add(poolProfit, extraReward) |
||||||
|
revenue.Add(revenue, extraReward) |
||||||
|
} |
||||||
|
|
||||||
|
if u.config.Donate { |
||||||
|
var donation = new(big.Rat) |
||||||
|
poolProfit, donation = chargeFee(poolProfit, donationFee) |
||||||
|
login := strings.ToLower(donationAccount) |
||||||
|
rewards[login] += weiToShannonInt64(donation) |
||||||
|
} |
||||||
|
|
||||||
|
if len(u.config.PoolFeeAddress) != 0 { |
||||||
|
address := strings.ToLower(u.config.PoolFeeAddress) |
||||||
|
rewards[address] += weiToShannonInt64(poolProfit) |
||||||
|
} |
||||||
|
|
||||||
|
return revenue, minersProfit, poolProfit, rewards, percents, nil |
||||||
|
} |
||||||
|
|
||||||
|
func calculateRewardsForShares(shares map[string]int64, total int64, reward *big.Rat) (map[string]int64, map[string]*big.Rat) { |
||||||
|
rewards := make(map[string]int64) |
||||||
|
percents := make(map[string]*big.Rat) |
||||||
|
|
||||||
|
for login, n := range shares { |
||||||
|
percents[login] = big.NewRat(n, total) |
||||||
|
workerReward := new(big.Rat).Mul(reward, percents[login]) |
||||||
|
rewards[login] += weiToShannonInt64(workerReward) |
||||||
|
} |
||||||
|
return rewards, percents |
||||||
|
} |
||||||
|
|
||||||
|
// Returns new value after fee deduction and fee value.
|
||||||
|
func chargeFee(value *big.Rat, fee float64) (*big.Rat, *big.Rat) { |
||||||
|
feePercent := new(big.Rat).SetFloat64(fee / 100) |
||||||
|
feeValue := new(big.Rat).Mul(value, feePercent) |
||||||
|
return new(big.Rat).Sub(value, feeValue), feeValue |
||||||
|
} |
||||||
|
|
||||||
|
func weiToShannonInt64(wei *big.Rat) int64 { |
||||||
|
shannon := new(big.Rat).SetInt(util.Shannon) |
||||||
|
inShannon := new(big.Rat).Quo(wei, shannon) |
||||||
|
value, _ := strconv.ParseInt(inShannon.FloatString(0), 10, 64) |
||||||
|
return value |
||||||
|
} |
||||||
|
|
||||||
|
func getUncleReward(uHeight, height int64) *big.Int { |
||||||
|
reward := new(big.Int).Set(constReward) |
||||||
|
reward.Mul(big.NewInt(uHeight+8-height), reward) |
||||||
|
reward.Div(reward, big.NewInt(8)) |
||||||
|
return reward |
||||||
|
} |
||||||
|
|
||||||
|
func (u *BlockUnlocker) getExtraRewardForTx(block *rpc.GetBlockReply) (*big.Int, error) { |
||||||
|
amount := new(big.Int) |
||||||
|
|
||||||
|
for _, tx := range block.Transactions { |
||||||
|
receipt, err := u.rpc.GetTxReceipt(tx.Hash) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if receipt != nil { |
||||||
|
gasUsed := util.String2Big(receipt.GasUsed) |
||||||
|
gasPrice := util.String2Big(tx.GasPrice) |
||||||
|
fee := new(big.Int).Mul(gasUsed, gasPrice) |
||||||
|
amount.Add(amount, fee) |
||||||
|
} |
||||||
|
} |
||||||
|
return amount, nil |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
package payouts |
||||||
|
|
||||||
|
import ( |
||||||
|
"math/big" |
||||||
|
"os" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
) |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
os.Exit(m.Run()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCalculateRewards(t *testing.T) { |
||||||
|
blockReward, _ := new(big.Rat).SetString("5000000000000000000") |
||||||
|
shares := map[string]int64{"0x0": 1000000, "0x1": 20000, "0x2": 5000, "0x3": 10, "0x4": 1} |
||||||
|
expectedRewards := map[string]int64{"0x0": 4877996431, "0x1": 97559929, "0x2": 24389982, "0x3": 48780, "0x4": 4878} |
||||||
|
totalShares := int64(1025011) |
||||||
|
|
||||||
|
rewards := calculateRewardsForShares(shares, totalShares, blockReward) |
||||||
|
expectedTotalAmount := int64(5000000000) |
||||||
|
|
||||||
|
totalAmount := int64(0) |
||||||
|
for login, amount := range rewards { |
||||||
|
totalAmount += amount |
||||||
|
|
||||||
|
if expectedRewards[login] != amount { |
||||||
|
t.Errorf("Amount for %v must be equal to %v vs %v", login, expectedRewards[login], amount) |
||||||
|
} |
||||||
|
} |
||||||
|
if totalAmount != expectedTotalAmount { |
||||||
|
t.Errorf("Total reward must be equal to block reward in Shannon: %v vs %v", expectedTotalAmount, totalAmount) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestChargeFee(t *testing.T) { |
||||||
|
orig, _ := new(big.Rat).SetString("5000000000000000000") |
||||||
|
value, _ := new(big.Rat).SetString("5000000000000000000") |
||||||
|
expectedNewValue, _ := new(big.Rat).SetString("3750000000000000000") |
||||||
|
expectedFee, _ := new(big.Rat).SetString("1250000000000000000") |
||||||
|
newValue, fee := chargeFee(orig, 25.0) |
||||||
|
|
||||||
|
if orig.Cmp(value) != 0 { |
||||||
|
t.Error("Must not change original value") |
||||||
|
} |
||||||
|
if newValue.Cmp(expectedNewValue) != 0 { |
||||||
|
t.Error("Must charge and deduct correct fee") |
||||||
|
} |
||||||
|
if fee.Cmp(expectedFee) != 0 { |
||||||
|
t.Error("Must charge fee") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestWeiToShannonInt64(t *testing.T) { |
||||||
|
wei, _ := new(big.Rat).SetString("1000000000000000000") |
||||||
|
origWei, _ := new(big.Rat).SetString("1000000000000000000") |
||||||
|
shannon := int64(1000000000) |
||||||
|
|
||||||
|
if weiToShannonInt64(wei) != shannon { |
||||||
|
t.Error("Must convert to Shannon") |
||||||
|
} |
||||||
|
if wei.Cmp(origWei) != 0 { |
||||||
|
t.Error("Must charge original value") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetUncleReward(t *testing.T) { |
||||||
|
rewards := make(map[int64]string) |
||||||
|
expectedRewards := map[int64]string{ |
||||||
|
1: "4375000000000000000", |
||||||
|
2: "3750000000000000000", |
||||||
|
3: "3125000000000000000", |
||||||
|
4: "2500000000000000000", |
||||||
|
5: "1875000000000000000", |
||||||
|
6: "1250000000000000000", |
||||||
|
} |
||||||
|
for i := int64(1); i < 7; i++ { |
||||||
|
rewards[i] = getUncleReward(1, i+1).String() |
||||||
|
} |
||||||
|
for i, reward := range rewards { |
||||||
|
if expectedRewards[i] != rewards[i] { |
||||||
|
t.Errorf("Incorrect uncle reward for %v, expected %v vs %v", i, expectedRewards[i], reward) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestMatchCandidate(t *testing.T) { |
||||||
|
gethBlock := &rpc.GetBlockReply{Hash: "0x12345A", Nonce: "0x1A"} |
||||||
|
parityBlock := &rpc.GetBlockReply{Hash: "0x12345A", SealFields: []string{"0x0A", "0x1A"}} |
||||||
|
candidate := &storage.BlockData{Nonce: "0x1a"} |
||||||
|
orphan := &storage.BlockData{Nonce: "0x1abc"} |
||||||
|
|
||||||
|
if !matchCandidate(gethBlock, candidate) { |
||||||
|
t.Error("Must match with nonce") |
||||||
|
} |
||||||
|
if !matchCandidate(parityBlock, candidate) { |
||||||
|
t.Error("Must match with seal fields") |
||||||
|
} |
||||||
|
if matchCandidate(gethBlock, orphan) { |
||||||
|
t.Error("Must not match with orphan with nonce") |
||||||
|
} |
||||||
|
if matchCandidate(parityBlock, orphan) { |
||||||
|
t.Error("Must not match orphan with seal fields") |
||||||
|
} |
||||||
|
|
||||||
|
block := &rpc.GetBlockReply{Hash: "0x12345A"} |
||||||
|
immature := &storage.BlockData{Hash: "0x12345a", Nonce: "0x0"} |
||||||
|
if !matchCandidate(block, immature) { |
||||||
|
t.Error("Must match with hash") |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,317 @@ |
|||||||
|
package policy |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os/exec" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
Workers int `json:"workers"` |
||||||
|
Banning Banning `json:"banning"` |
||||||
|
Limits Limits `json:"limits"` |
||||||
|
ResetInterval string `json:"resetInterval"` |
||||||
|
RefreshInterval string `json:"refreshInterval"` |
||||||
|
} |
||||||
|
|
||||||
|
type Limits struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
Limit int32 `json:"limit"` |
||||||
|
Grace string `json:"grace"` |
||||||
|
LimitJump int32 `json:"limitJump"` |
||||||
|
} |
||||||
|
|
||||||
|
type Banning struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
IPSet string `json:"ipset"` |
||||||
|
Timeout int64 `json:"timeout"` |
||||||
|
InvalidPercent float32 `json:"invalidPercent"` |
||||||
|
CheckThreshold int32 `json:"checkThreshold"` |
||||||
|
MalformedLimit int32 `json:"malformedLimit"` |
||||||
|
} |
||||||
|
|
||||||
|
type Stats struct { |
||||||
|
sync.Mutex |
||||||
|
// We are using atomic with LastBeat,
|
||||||
|
// so moving it before the rest in order to avoid alignment issue
|
||||||
|
LastBeat int64 |
||||||
|
BannedAt int64 |
||||||
|
ValidShares int32 |
||||||
|
InvalidShares int32 |
||||||
|
Malformed int32 |
||||||
|
ConnLimit int32 |
||||||
|
Banned int32 |
||||||
|
} |
||||||
|
|
||||||
|
type PolicyServer struct { |
||||||
|
sync.RWMutex |
||||||
|
statsMu sync.Mutex |
||||||
|
config *Config |
||||||
|
stats map[string]*Stats |
||||||
|
banChannel chan string |
||||||
|
startedAt int64 |
||||||
|
grace int64 |
||||||
|
timeout int64 |
||||||
|
blacklist []string |
||||||
|
whitelist []string |
||||||
|
storage *storage.RedisClient |
||||||
|
} |
||||||
|
|
||||||
|
func Start(cfg *Config, storage *storage.RedisClient) *PolicyServer { |
||||||
|
s := &PolicyServer{config: cfg, startedAt: util.MakeTimestamp()} |
||||||
|
grace := util.MustParseDuration(cfg.Limits.Grace) |
||||||
|
s.grace = int64(grace / time.Millisecond) |
||||||
|
s.banChannel = make(chan string, 64) |
||||||
|
s.stats = make(map[string]*Stats) |
||||||
|
s.storage = storage |
||||||
|
s.refreshState() |
||||||
|
|
||||||
|
timeout := util.MustParseDuration(s.config.ResetInterval) |
||||||
|
s.timeout = int64(timeout / time.Millisecond) |
||||||
|
|
||||||
|
resetIntv := util.MustParseDuration(s.config.ResetInterval) |
||||||
|
resetTimer := time.NewTimer(resetIntv) |
||||||
|
log.Printf("Set policy stats reset every %v", resetIntv) |
||||||
|
|
||||||
|
refreshIntv := util.MustParseDuration(s.config.RefreshInterval) |
||||||
|
refreshTimer := time.NewTimer(refreshIntv) |
||||||
|
log.Printf("Set policy state refresh every %v", refreshIntv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-resetTimer.C: |
||||||
|
s.resetStats() |
||||||
|
resetTimer.Reset(resetIntv) |
||||||
|
case <-refreshTimer.C: |
||||||
|
s.refreshState() |
||||||
|
refreshTimer.Reset(refreshIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
for i := 0; i < s.config.Workers; i++ { |
||||||
|
s.startPolicyWorker() |
||||||
|
} |
||||||
|
log.Printf("Running with %v policy workers", s.config.Workers) |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) startPolicyWorker() { |
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case ip := <-s.banChannel: |
||||||
|
s.doBan(ip) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) resetStats() { |
||||||
|
now := util.MakeTimestamp() |
||||||
|
banningTimeout := s.config.Banning.Timeout * 1000 |
||||||
|
total := 0 |
||||||
|
s.statsMu.Lock() |
||||||
|
defer s.statsMu.Unlock() |
||||||
|
|
||||||
|
for key, m := range s.stats { |
||||||
|
lastBeat := atomic.LoadInt64(&m.LastBeat) |
||||||
|
bannedAt := atomic.LoadInt64(&m.BannedAt) |
||||||
|
|
||||||
|
if now-bannedAt >= banningTimeout { |
||||||
|
atomic.StoreInt64(&m.BannedAt, 0) |
||||||
|
if atomic.CompareAndSwapInt32(&m.Banned, 1, 0) { |
||||||
|
log.Printf("Ban dropped for %v", key) |
||||||
|
delete(s.stats, key) |
||||||
|
total++ |
||||||
|
} |
||||||
|
} |
||||||
|
if now-lastBeat >= s.timeout { |
||||||
|
delete(s.stats, key) |
||||||
|
total++ |
||||||
|
} |
||||||
|
} |
||||||
|
log.Printf("Flushed stats for %v IP addresses", total) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) refreshState() { |
||||||
|
s.Lock() |
||||||
|
defer s.Unlock() |
||||||
|
var err error |
||||||
|
|
||||||
|
s.blacklist, err = s.storage.GetBlacklist() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to get blacklist from backend: %v", err) |
||||||
|
} |
||||||
|
s.whitelist, err = s.storage.GetWhitelist() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to get whitelist from backend: %v", err) |
||||||
|
} |
||||||
|
log.Println("Policy state refresh complete") |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) NewStats() *Stats { |
||||||
|
x := &Stats{ |
||||||
|
ConnLimit: s.config.Limits.Limit, |
||||||
|
} |
||||||
|
x.heartbeat() |
||||||
|
return x |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) Get(ip string) *Stats { |
||||||
|
s.statsMu.Lock() |
||||||
|
defer s.statsMu.Unlock() |
||||||
|
|
||||||
|
if x, ok := s.stats[ip]; !ok { |
||||||
|
x = s.NewStats() |
||||||
|
s.stats[ip] = x |
||||||
|
return x |
||||||
|
} else { |
||||||
|
x.heartbeat() |
||||||
|
return x |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) BanClient(ip string) { |
||||||
|
x := s.Get(ip) |
||||||
|
s.forceBan(x, ip) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) IsBanned(ip string) bool { |
||||||
|
x := s.Get(ip) |
||||||
|
return atomic.LoadInt32(&x.Banned) > 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyLimitPolicy(ip string) bool { |
||||||
|
if !s.config.Limits.Enabled { |
||||||
|
return true |
||||||
|
} |
||||||
|
now := util.MakeTimestamp() |
||||||
|
if now-s.startedAt > s.grace { |
||||||
|
return s.Get(ip).decrLimit() > 0 |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyLoginPolicy(addy, ip string) bool { |
||||||
|
if s.InBlackList(addy) { |
||||||
|
x := s.Get(ip) |
||||||
|
s.forceBan(x, ip) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyMalformedPolicy(ip string) bool { |
||||||
|
x := s.Get(ip) |
||||||
|
n := x.incrMalformed() |
||||||
|
if n >= s.config.Banning.MalformedLimit { |
||||||
|
s.forceBan(x, ip) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplySharePolicy(ip string, validShare bool) bool { |
||||||
|
x := s.Get(ip) |
||||||
|
x.Lock() |
||||||
|
|
||||||
|
if validShare { |
||||||
|
x.ValidShares++ |
||||||
|
if s.config.Limits.Enabled { |
||||||
|
x.incrLimit(s.config.Limits.LimitJump) |
||||||
|
} |
||||||
|
} else { |
||||||
|
x.InvalidShares++ |
||||||
|
} |
||||||
|
|
||||||
|
totalShares := x.ValidShares + x.InvalidShares |
||||||
|
if totalShares < s.config.Banning.CheckThreshold { |
||||||
|
x.Unlock() |
||||||
|
return true |
||||||
|
} |
||||||
|
validShares := float32(x.ValidShares) |
||||||
|
invalidShares := float32(x.InvalidShares) |
||||||
|
x.resetShares() |
||||||
|
x.Unlock() |
||||||
|
|
||||||
|
ratio := invalidShares / validShares |
||||||
|
|
||||||
|
if ratio >= s.config.Banning.InvalidPercent/100.0 { |
||||||
|
s.forceBan(x, ip) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) resetShares() { |
||||||
|
x.ValidShares = 0 |
||||||
|
x.InvalidShares = 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) forceBan(x *Stats, ip string) { |
||||||
|
if !s.config.Banning.Enabled || s.InWhiteList(ip) { |
||||||
|
return |
||||||
|
} |
||||||
|
atomic.StoreInt64(&x.BannedAt, util.MakeTimestamp()) |
||||||
|
|
||||||
|
if atomic.CompareAndSwapInt32(&x.Banned, 0, 1) { |
||||||
|
if len(s.config.Banning.IPSet) > 0 { |
||||||
|
s.banChannel <- ip |
||||||
|
} else { |
||||||
|
log.Println("Banned peer", ip) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) incrLimit(n int32) { |
||||||
|
atomic.AddInt32(&x.ConnLimit, n) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) incrMalformed() int32 { |
||||||
|
return atomic.AddInt32(&x.Malformed, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) decrLimit() int32 { |
||||||
|
return atomic.AddInt32(&x.ConnLimit, -1) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) InBlackList(addy string) bool { |
||||||
|
s.RLock() |
||||||
|
defer s.RUnlock() |
||||||
|
return util.StringInSlice(addy, s.blacklist) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) InWhiteList(ip string) bool { |
||||||
|
s.RLock() |
||||||
|
defer s.RUnlock() |
||||||
|
return util.StringInSlice(ip, s.whitelist) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) doBan(ip string) { |
||||||
|
set, timeout := s.config.Banning.IPSet, s.config.Banning.Timeout |
||||||
|
cmd := fmt.Sprintf("sudo ipset add %s %s timeout %v -!", set, ip, timeout) |
||||||
|
args := strings.Fields(cmd) |
||||||
|
head := args[0] |
||||||
|
args = args[1:] |
||||||
|
|
||||||
|
log.Printf("Banned %v with timeout %v on ipset %s", ip, timeout, set) |
||||||
|
|
||||||
|
_, err := exec.Command(head, args...).Output() |
||||||
|
if err != nil { |
||||||
|
log.Printf("CMD Error: %s", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) heartbeat() { |
||||||
|
now := util.MakeTimestamp() |
||||||
|
atomic.StoreInt64(&x.LastBeat, now) |
||||||
|
} |
||||||
@ -0,0 +1,117 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"math/big" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
const maxBacklog = 3 |
||||||
|
|
||||||
|
type heightDiffPair struct { |
||||||
|
diff *big.Int |
||||||
|
height uint64 |
||||||
|
} |
||||||
|
|
||||||
|
type BlockTemplate struct { |
||||||
|
sync.RWMutex |
||||||
|
Header string |
||||||
|
Seed string |
||||||
|
Target string |
||||||
|
Difficulty *big.Int |
||||||
|
Height uint64 |
||||||
|
GetPendingBlockCache *rpc.GetBlockReplyPart |
||||||
|
nonces map[string]bool |
||||||
|
headers map[string]heightDiffPair |
||||||
|
} |
||||||
|
|
||||||
|
type Block struct { |
||||||
|
difficulty *big.Int |
||||||
|
hashNoNonce common.Hash |
||||||
|
nonce uint64 |
||||||
|
mixDigest common.Hash |
||||||
|
number uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func (b Block) Difficulty() *big.Int { return b.difficulty } |
||||||
|
func (b Block) HashNoNonce() common.Hash { return b.hashNoNonce } |
||||||
|
func (b Block) Nonce() uint64 { return b.nonce } |
||||||
|
func (b Block) MixDigest() common.Hash { return b.mixDigest } |
||||||
|
func (b Block) NumberU64() uint64 { return b.number } |
||||||
|
|
||||||
|
func (s *ProxyServer) fetchBlockTemplate() { |
||||||
|
rpc := s.rpc() |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
pendingReply, height, diff, err := s.fetchPendingBlock() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while refreshing pending block on %s: %s", rpc.Name, err) |
||||||
|
return |
||||||
|
} |
||||||
|
reply, err := rpc.GetWork() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while refreshing block template on %s: %s", rpc.Name, err) |
||||||
|
return |
||||||
|
} |
||||||
|
// No need to update, we have fresh job
|
||||||
|
if t != nil && t.Header == reply[0] { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
pendingReply.Difficulty = util.ToHex(s.config.Proxy.Difficulty) |
||||||
|
|
||||||
|
newTemplate := BlockTemplate{ |
||||||
|
Header: reply[0], |
||||||
|
Seed: reply[1], |
||||||
|
Target: reply[2], |
||||||
|
Height: height, |
||||||
|
Difficulty: big.NewInt(diff), |
||||||
|
GetPendingBlockCache: pendingReply, |
||||||
|
headers: make(map[string]heightDiffPair), |
||||||
|
} |
||||||
|
// Copy job backlog and add current one
|
||||||
|
newTemplate.headers[reply[0]] = heightDiffPair{ |
||||||
|
diff: util.TargetHexToDiff(reply[2]), |
||||||
|
height: height, |
||||||
|
} |
||||||
|
if t != nil { |
||||||
|
for k, v := range t.headers { |
||||||
|
if v.height > height-maxBacklog { |
||||||
|
newTemplate.headers[k] = v |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
s.blockTemplate.Store(&newTemplate) |
||||||
|
log.Printf("New block to mine on %s at height %d / %s", rpc.Name, height, reply[0][0:10]) |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
if s.config.Proxy.Stratum.Enabled { |
||||||
|
go s.broadcastNewJobs() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) fetchPendingBlock() (*rpc.GetBlockReplyPart, uint64, int64, error) { |
||||||
|
rpc := s.rpc() |
||||||
|
reply, err := rpc.GetPendingBlock() |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while refreshing pending block on %s: %s", rpc.Name, err) |
||||||
|
return nil, 0, 0, err |
||||||
|
} |
||||||
|
blockNumber, err := strconv.ParseUint(strings.Replace(reply.Number, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
log.Println("Can't parse pending block number") |
||||||
|
return nil, 0, 0, err |
||||||
|
} |
||||||
|
blockDiff, err := strconv.ParseInt(strings.Replace(reply.Difficulty, "0x", "", -1), 16, 64) |
||||||
|
if err != nil { |
||||||
|
log.Println("Can't parse pending block difficulty") |
||||||
|
return nil, 0, 0, err |
||||||
|
} |
||||||
|
return reply, blockNumber, blockDiff, nil |
||||||
|
} |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/api" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/payouts" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/policy" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
) |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Proxy Proxy `json:"proxy"` |
||||||
|
Api api.ApiConfig `json:"api"` |
||||||
|
Upstream []Upstream `json:"upstream"` |
||||||
|
UpstreamCheckInterval string `json:"upstreamCheckInterval"` |
||||||
|
|
||||||
|
Threads int `json:"threads"` |
||||||
|
|
||||||
|
Coin string `json:"coin"` |
||||||
|
Pplns int64 `json:"pplns"` |
||||||
|
Redis storage.Config `json:"redis"` |
||||||
|
|
||||||
|
BlockUnlocker payouts.UnlockerConfig `json:"unlocker"` |
||||||
|
Payouts payouts.PayoutsConfig `json:"payouts"` |
||||||
|
|
||||||
|
NewrelicName string `json:"newrelicName"` |
||||||
|
NewrelicKey string `json:"newrelicKey"` |
||||||
|
NewrelicVerbose bool `json:"newrelicVerbose"` |
||||||
|
NewrelicEnabled bool `json:"newrelicEnabled"` |
||||||
|
} |
||||||
|
|
||||||
|
type Proxy struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
Listen string `json:"listen"` |
||||||
|
LimitHeadersSize int `json:"limitHeadersSize"` |
||||||
|
LimitBodySize int64 `json:"limitBodySize"` |
||||||
|
BehindReverseProxy bool `json:"behindReverseProxy"` |
||||||
|
BlockRefreshInterval string `json:"blockRefreshInterval"` |
||||||
|
Difficulty int64 `json:"difficulty"` |
||||||
|
StateUpdateInterval string `json:"stateUpdateInterval"` |
||||||
|
HashrateExpiration string `json:"hashrateExpiration"` |
||||||
|
StratumHostname string `json:"stratumHostname"` |
||||||
|
|
||||||
|
Policy policy.Config `json:"policy"` |
||||||
|
|
||||||
|
MaxFails int64 `json:"maxFails"` |
||||||
|
HealthCheck bool `json:"healthCheck"` |
||||||
|
|
||||||
|
Stratum Stratum `json:"stratum"` |
||||||
|
} |
||||||
|
|
||||||
|
type Stratum struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
Listen string `json:"listen"` |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
MaxConn int `json:"maxConn"` |
||||||
|
} |
||||||
|
|
||||||
|
type Upstream struct { |
||||||
|
Name string `json:"name"` |
||||||
|
Url string `json:"url"` |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
} |
||||||
@ -0,0 +1,127 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"errors" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
// Allow only lowercase hexadecimal with 0x prefix
|
||||||
|
var noncePattern = regexp.MustCompile("^0x[0-9a-f]{16}$") |
||||||
|
var hashPattern = regexp.MustCompile("^0x[0-9a-f]{64}$") |
||||||
|
var workerPattern = regexp.MustCompile("^[0-9a-zA-Z-_]{1,8}$") |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
func (s *ProxyServer) handleLoginRPC(cs *Session, params []string, id string) (bool, *ErrorReply) { |
||||||
|
if len(params) == 0 { |
||||||
|
return false, &ErrorReply{Code: -1, Message: "Invalid params"} |
||||||
|
} |
||||||
|
|
||||||
|
login := strings.ToLower(params[0]) |
||||||
|
if !util.IsValidHexAddress(login) { |
||||||
|
return false, &ErrorReply{Code: -1, Message: "Invalid login"} |
||||||
|
} |
||||||
|
if !s.policy.ApplyLoginPolicy(login, cs.ip) { |
||||||
|
return false, &ErrorReply{Code: -1, Message: "You are blacklisted"} |
||||||
|
} |
||||||
|
cs.login = login |
||||||
|
s.registerSession(cs) |
||||||
|
log.Printf("Stratum miner connected %v@%v", login, cs.ip) |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleGetWorkRPC(cs *Session) ([]string, *ErrorReply) { |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
if t == nil || len(t.Header) == 0 || s.isSick() { |
||||||
|
return nil, &ErrorReply{Code: 0, Message: "Work not ready"} |
||||||
|
} |
||||||
|
return []string{t.Header, t.Seed, s.diff}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
func (s *ProxyServer) handleTCPSubmitRPC(cs *Session, id string, params []string) (bool, *ErrorReply) { |
||||||
|
s.sessionsMu.RLock() |
||||||
|
_, ok := s.sessions[cs] |
||||||
|
s.sessionsMu.RUnlock() |
||||||
|
|
||||||
|
if !ok { |
||||||
|
return false, &ErrorReply{Code: 25, Message: "Not subscribed"} |
||||||
|
} |
||||||
|
return s.handleSubmitRPC(cs, cs.login, id, params) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleSubmitRPC(cs *Session, login, id string, params []string) (bool, *ErrorReply) { |
||||||
|
if !workerPattern.MatchString(id){ |
||||||
|
id = "0" |
||||||
|
} |
||||||
|
if len(params) != 3 { |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
log.Printf("Malformed params from %s@%s %v", login, cs.ip, params) |
||||||
|
return false, &ErrorReply{Code: -1, Message: "Invalid params"} |
||||||
|
} |
||||||
|
|
||||||
|
if !noncePattern.MatchString(params[0]) || !hashPattern.MatchString(params[1]) || !hashPattern.MatchString(params[2]) { |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
log.Printf("Malformed PoW result from %s@%s %v", login, cs.ip, params) |
||||||
|
return false, &ErrorReply{Code: -1, Message: "Malformed PoW result"} |
||||||
|
} |
||||||
|
|
||||||
|
go func(s *ProxyServer, cs *Session, login, id string, params []string) { |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
|
||||||
|
//MFO: This function (s.processShare) will process a share as per hasher.Verify function of github.com/ethereum/ethash
|
||||||
|
// output of this function is either:
|
||||||
|
// true,true (Exists) which means share already exists and it is validShare
|
||||||
|
// true,false (Exists & invalid)which means share already exists and it is invalidShare or it is a block <-- should not ever happen
|
||||||
|
// false,false (stale/invalid)which means share is new, and it is not a block, might be a stale share or invalidShare
|
||||||
|
// false,true (valid)which means share is new, and it is a block or accepted share
|
||||||
|
// When this function finishes, the results is already recorded in the db for valid shares or blocks.
|
||||||
|
exist, validShare := s.processShare(login, id, cs.ip, t, params) |
||||||
|
ok := s.policy.ApplySharePolicy(cs.ip, !exist && validShare) |
||||||
|
|
||||||
|
|
||||||
|
// if true,true or true,false
|
||||||
|
if exist { |
||||||
|
log.Printf("Duplicate share from %s@%s %v", login, cs.ip, params) |
||||||
|
cs.lastErr = errors.New("Duplicate share") |
||||||
|
} |
||||||
|
|
||||||
|
// if false, false
|
||||||
|
if !validShare { |
||||||
|
//MFO: Here we have an invalid share
|
||||||
|
log.Printf("Invalid share from %s@%s", login, cs.ip) |
||||||
|
// Bad shares limit reached, return error and close
|
||||||
|
if !ok { |
||||||
|
cs.lastErr = errors.New("Invalid share") |
||||||
|
} |
||||||
|
} |
||||||
|
//MFO: Here we have a valid share and it is already recorded in DB by miner.go
|
||||||
|
// if false, true
|
||||||
|
log.Printf("Valid share from %s@%s", login, cs.ip) |
||||||
|
|
||||||
|
if !ok { |
||||||
|
cs.lastErr = errors.New("High rate of invalid shares") |
||||||
|
} |
||||||
|
}(s, cs, login, id, params) |
||||||
|
|
||||||
|
return true, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleGetBlockByNumberRPC() *rpc.GetBlockReplyPart { |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
var reply *rpc.GetBlockReplyPart |
||||||
|
if t != nil { |
||||||
|
reply = t.GetPendingBlockCache |
||||||
|
} |
||||||
|
return reply |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleUnknownRPC(cs *Session, m string) *ErrorReply { |
||||||
|
log.Printf("Unknown request method %s from %s", m, cs.ip) |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
return &ErrorReply{Code: -3, Message: "Method not found"} |
||||||
|
} |
||||||
@ -0,0 +1,97 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"math/big" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/etclabscore/go-etchash" |
||||||
|
) |
||||||
|
|
||||||
|
var ecip1099FBlockClassic uint64 = 11700000 // classic mainnet
|
||||||
|
var ecip1099FBlockMordor uint64 = 2520000 // mordor testnet
|
||||||
|
|
||||||
|
var hasher = etchash.New(&ecip1099FBlockClassic) |
||||||
|
|
||||||
|
func (s *ProxyServer) processShare(login, id, ip string, t *BlockTemplate, params []string) (bool, bool) { |
||||||
|
// Now, the function received some work with login id and worker name and all information, ready to be processed
|
||||||
|
// and checked if it is a valid work or not, and if it is a block or not and write to db accordingly
|
||||||
|
nonceHex := params[0] |
||||||
|
hashNoNonce := params[1] |
||||||
|
mixDigest := params[2] |
||||||
|
nonce, _ := strconv.ParseUint(strings.Replace(nonceHex, "0x", "", -1), 16, 64) |
||||||
|
shareDiff := s.config.Proxy.Difficulty |
||||||
|
stratumHostname := s.config.Proxy.StratumHostname |
||||||
|
|
||||||
|
h, ok := t.headers[hashNoNonce] |
||||||
|
if !ok { |
||||||
|
log.Printf("Stale share from %v@%v", login, ip) |
||||||
|
// Here we have a stale share, we need to create a redis function as follows
|
||||||
|
// CASE1: stale Share
|
||||||
|
// s.backend.WriteWorkerShareStatus(login, id, valid bool, stale bool, invalid bool)
|
||||||
|
return false, false |
||||||
|
} |
||||||
|
|
||||||
|
share := Block{ |
||||||
|
number: h.height, |
||||||
|
hashNoNonce: common.HexToHash(hashNoNonce), |
||||||
|
difficulty: big.NewInt(shareDiff), |
||||||
|
nonce: nonce, |
||||||
|
mixDigest: common.HexToHash(mixDigest), |
||||||
|
} |
||||||
|
|
||||||
|
block := Block{ |
||||||
|
number: h.height, |
||||||
|
hashNoNonce: common.HexToHash(hashNoNonce), |
||||||
|
difficulty: h.diff, |
||||||
|
nonce: nonce, |
||||||
|
mixDigest: common.HexToHash(mixDigest), |
||||||
|
} |
||||||
|
|
||||||
|
if !hasher.Verify(share) { |
||||||
|
// THis is an invalid block, record it
|
||||||
|
// CASE2: invalid Share
|
||||||
|
// s.backend.WriteWorkerShareStatus(login, id, valid bool, stale bool, invalid bool)
|
||||||
|
return false, false |
||||||
|
} |
||||||
|
|
||||||
|
if hasher.Verify(block) { |
||||||
|
ok, err := s.rpc().SubmitBlock(params) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Block submission failure at height %v for %v: %v", h.height, t.Header, err) |
||||||
|
} else if !ok { |
||||||
|
log.Printf("Block rejected at height %v for %v", h.height, t.Header) |
||||||
|
return false, false |
||||||
|
} else { |
||||||
|
s.fetchBlockTemplate() |
||||||
|
exist, err := s.backend.WriteBlock(login, id, params, shareDiff, h.diff.Int64(), h.height, s.hashrateExpiration, stratumHostname) |
||||||
|
if exist { |
||||||
|
return true, false |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
log.Println("Failed to insert block candidate into backend:", err) |
||||||
|
} else { |
||||||
|
log.Printf("Inserted block %v to backend", h.height) |
||||||
|
} |
||||||
|
// Here we have a valid share, which is in-fact a block and it is written to db
|
||||||
|
log.Printf("Block found by miner %v@%v at height %d", login, ip, h.height) |
||||||
|
} |
||||||
|
} else { |
||||||
|
exist, err := s.backend.WriteShare(login, id, params, shareDiff, h.height, s.hashrateExpiration, stratumHostname) |
||||||
|
if exist { |
||||||
|
return true, false |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
log.Println("Failed to insert share data into backend:", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Here we have a valid share, which is only a share and it is written to db
|
||||||
|
} |
||||||
|
// This means success, either a valid share or a valid block, in this case, record a valid share for the worker
|
||||||
|
// CASE3: Valid Share
|
||||||
|
// s.backend.WriteWorkerShareStatus(login, id, valid bool, stale bool, invalid bool)
|
||||||
|
|
||||||
|
return false, true |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import "encoding/json" |
||||||
|
|
||||||
|
type JSONRpcReq struct { |
||||||
|
Id json.RawMessage `json:"id"` |
||||||
|
Method string `json:"method"` |
||||||
|
Params json.RawMessage `json:"params"` |
||||||
|
} |
||||||
|
|
||||||
|
type StratumReq struct { |
||||||
|
JSONRpcReq |
||||||
|
Worker string `json:"worker"` |
||||||
|
} |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
type JSONPushMessage struct { |
||||||
|
// FIXME: Temporarily add ID for Claymore compliance
|
||||||
|
Id int64 `json:"id"` |
||||||
|
Version string `json:"jsonrpc"` |
||||||
|
Result interface{} `json:"result"` |
||||||
|
} |
||||||
|
|
||||||
|
type JSONRpcResp struct { |
||||||
|
Id json.RawMessage `json:"id"` |
||||||
|
Version string `json:"jsonrpc"` |
||||||
|
Result interface{} `json:"result"` |
||||||
|
Error interface{} `json:"error,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type SubmitReply struct { |
||||||
|
Status string `json:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
type ErrorReply struct { |
||||||
|
Code int `json:"code"` |
||||||
|
Message string `json:"message"` |
||||||
|
} |
||||||
@ -0,0 +1,311 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gorilla/mux" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/policy" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/rpc" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/storage" |
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
type ProxyServer struct { |
||||||
|
config *Config |
||||||
|
blockTemplate atomic.Value |
||||||
|
upstream int32 |
||||||
|
upstreams []*rpc.RPCClient |
||||||
|
backend *storage.RedisClient |
||||||
|
diff string |
||||||
|
policy *policy.PolicyServer |
||||||
|
hashrateExpiration time.Duration |
||||||
|
failsCount int64 |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
sessionsMu sync.RWMutex |
||||||
|
sessions map[*Session]struct{} |
||||||
|
timeout time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
type Session struct { |
||||||
|
ip string |
||||||
|
enc *json.Encoder |
||||||
|
|
||||||
|
// Stratum
|
||||||
|
sync.Mutex |
||||||
|
conn *net.TCPConn |
||||||
|
login string |
||||||
|
lastErr error |
||||||
|
} |
||||||
|
|
||||||
|
func NewProxy(cfg *Config, backend *storage.RedisClient) *ProxyServer { |
||||||
|
if len(cfg.Name) == 0 { |
||||||
|
log.Fatal("You must set instance name") |
||||||
|
} |
||||||
|
policy := policy.Start(&cfg.Proxy.Policy, backend) |
||||||
|
|
||||||
|
proxy := &ProxyServer{config: cfg, backend: backend, policy: policy} |
||||||
|
proxy.diff = util.GetTargetHex(cfg.Proxy.Difficulty) |
||||||
|
|
||||||
|
proxy.upstreams = make([]*rpc.RPCClient, len(cfg.Upstream)) |
||||||
|
for i, v := range cfg.Upstream { |
||||||
|
proxy.upstreams[i] = rpc.NewRPCClient(v.Name, v.Url, v.Timeout) |
||||||
|
log.Printf("Upstream: %s => %s", v.Name, v.Url) |
||||||
|
} |
||||||
|
log.Printf("Default upstream: %s => %s", proxy.rpc().Name, proxy.rpc().Url) |
||||||
|
|
||||||
|
if cfg.Proxy.Stratum.Enabled { |
||||||
|
proxy.sessions = make(map[*Session]struct{}) |
||||||
|
go proxy.ListenTCP() |
||||||
|
} |
||||||
|
|
||||||
|
proxy.fetchBlockTemplate() |
||||||
|
|
||||||
|
proxy.hashrateExpiration = util.MustParseDuration(cfg.Proxy.HashrateExpiration) |
||||||
|
|
||||||
|
refreshIntv := util.MustParseDuration(cfg.Proxy.BlockRefreshInterval) |
||||||
|
refreshTimer := time.NewTimer(refreshIntv) |
||||||
|
log.Printf("Set block refresh every %v", refreshIntv) |
||||||
|
|
||||||
|
checkIntv := util.MustParseDuration(cfg.UpstreamCheckInterval) |
||||||
|
checkTimer := time.NewTimer(checkIntv) |
||||||
|
|
||||||
|
stateUpdateIntv := util.MustParseDuration(cfg.Proxy.StateUpdateInterval) |
||||||
|
stateUpdateTimer := time.NewTimer(stateUpdateIntv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-refreshTimer.C: |
||||||
|
proxy.fetchBlockTemplate() |
||||||
|
refreshTimer.Reset(refreshIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-checkTimer.C: |
||||||
|
proxy.checkUpstreams() |
||||||
|
checkTimer.Reset(checkIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-stateUpdateTimer.C: |
||||||
|
t := proxy.currentBlockTemplate() |
||||||
|
if t != nil { |
||||||
|
err := backend.WriteNodeState(cfg.Name, t.Height, t.Difficulty) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to write node state to backend: %v", err) |
||||||
|
proxy.markSick() |
||||||
|
} else { |
||||||
|
proxy.markOk() |
||||||
|
} |
||||||
|
} |
||||||
|
stateUpdateTimer.Reset(stateUpdateIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
return proxy |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) Start() { |
||||||
|
log.Printf("Starting proxy on %v", s.config.Proxy.Listen) |
||||||
|
r := mux.NewRouter() |
||||||
|
r.Handle("/{login:0x[0-9a-fA-F]{40}}/{id:[0-9a-zA-Z-_]{1,8}}", s) |
||||||
|
r.Handle("/{login:0x[0-9a-fA-F]{40}}", s) |
||||||
|
srv := &http.Server{ |
||||||
|
Addr: s.config.Proxy.Listen, |
||||||
|
Handler: r, |
||||||
|
MaxHeaderBytes: s.config.Proxy.LimitHeadersSize, |
||||||
|
} |
||||||
|
err := srv.ListenAndServe() |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Failed to start proxy: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) rpc() *rpc.RPCClient { |
||||||
|
i := atomic.LoadInt32(&s.upstream) |
||||||
|
return s.upstreams[i] |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) checkUpstreams() { |
||||||
|
candidate := int32(0) |
||||||
|
backup := false |
||||||
|
|
||||||
|
for i, v := range s.upstreams { |
||||||
|
if v.Check() && !backup { |
||||||
|
candidate = int32(i) |
||||||
|
backup = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if s.upstream != candidate { |
||||||
|
log.Printf("Switching to %v upstream", s.upstreams[candidate].Name) |
||||||
|
atomic.StoreInt32(&s.upstream, candidate) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
||||||
|
if r.Method != "POST" { |
||||||
|
s.writeError(w, 405, "rpc: POST method required, received "+r.Method) |
||||||
|
return |
||||||
|
} |
||||||
|
ip := s.remoteAddr(r) |
||||||
|
if !s.policy.IsBanned(ip) { |
||||||
|
s.handleClient(w, r, ip) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) remoteAddr(r *http.Request) string { |
||||||
|
if s.config.Proxy.BehindReverseProxy { |
||||||
|
ip := r.Header.Get("X-Forwarded-For") |
||||||
|
if len(ip) > 0 && net.ParseIP(ip) != nil { |
||||||
|
return ip |
||||||
|
} |
||||||
|
} |
||||||
|
ip, _, _ := net.SplitHostPort(r.RemoteAddr) |
||||||
|
return ip |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleClient(w http.ResponseWriter, r *http.Request, ip string) { |
||||||
|
if r.ContentLength > s.config.Proxy.LimitBodySize { |
||||||
|
log.Printf("Socket flood from %s", ip) |
||||||
|
s.policy.ApplyMalformedPolicy(ip) |
||||||
|
http.Error(w, "Request too large", http.StatusExpectationFailed) |
||||||
|
return |
||||||
|
} |
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, s.config.Proxy.LimitBodySize) |
||||||
|
defer r.Body.Close() |
||||||
|
|
||||||
|
cs := &Session{ip: ip, enc: json.NewEncoder(w)} |
||||||
|
dec := json.NewDecoder(r.Body) |
||||||
|
for { |
||||||
|
var req JSONRpcReq |
||||||
|
if err := dec.Decode(&req); err == io.EOF { |
||||||
|
break |
||||||
|
} else if err != nil { |
||||||
|
log.Printf("Malformed request from %v: %v", ip, err) |
||||||
|
s.policy.ApplyMalformedPolicy(ip) |
||||||
|
return |
||||||
|
} |
||||||
|
cs.handleMessage(s, r, &req) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) handleMessage(s *ProxyServer, r *http.Request, req *JSONRpcReq) { |
||||||
|
if req.Id == nil { |
||||||
|
log.Printf("Missing RPC id from %s", cs.ip) |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
vars := mux.Vars(r) |
||||||
|
login := strings.ToLower(vars["login"]) |
||||||
|
|
||||||
|
if !util.IsValidHexAddress(login) { |
||||||
|
errReply := &ErrorReply{Code: -1, Message: "Invalid login"} |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
return |
||||||
|
} |
||||||
|
if !s.policy.ApplyLoginPolicy(login, cs.ip) { |
||||||
|
errReply := &ErrorReply{Code: -1, Message: "You are blacklisted"} |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Handle RPC methods
|
||||||
|
switch req.Method { |
||||||
|
case "eth_getWork": |
||||||
|
reply, errReply := s.handleGetWorkRPC(cs) |
||||||
|
if errReply != nil { |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
break |
||||||
|
} |
||||||
|
cs.sendResult(req.Id, &reply) |
||||||
|
case "eth_submitWork": |
||||||
|
if req.Params != nil { |
||||||
|
var params []string |
||||||
|
err := json.Unmarshal(req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Unable to parse params from %v", cs.ip) |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
break |
||||||
|
} |
||||||
|
reply, errReply := s.handleSubmitRPC(cs, login, vars["id"], params) |
||||||
|
if errReply != nil { |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
break |
||||||
|
} |
||||||
|
cs.sendResult(req.Id, &reply) |
||||||
|
} else { |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
errReply := &ErrorReply{Code: -1, Message: "Malformed request"} |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
} |
||||||
|
case "eth_getBlockByNumber": |
||||||
|
reply := s.handleGetBlockByNumberRPC() |
||||||
|
cs.sendResult(req.Id, reply) |
||||||
|
case "eth_submitHashrate": |
||||||
|
cs.sendResult(req.Id, true) |
||||||
|
default: |
||||||
|
errReply := s.handleUnknownRPC(cs, req.Method) |
||||||
|
cs.sendError(req.Id, errReply) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendResult(id json.RawMessage, result interface{}) error { |
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: nil, Result: result} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendError(id json.RawMessage, reply *ErrorReply) error { |
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: reply} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) writeError(w http.ResponseWriter, status int, msg string) { |
||||||
|
w.WriteHeader(status) |
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8") |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) currentBlockTemplate() *BlockTemplate { |
||||||
|
t := s.blockTemplate.Load() |
||||||
|
if t != nil { |
||||||
|
return t.(*BlockTemplate) |
||||||
|
} else { |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) markSick() { |
||||||
|
atomic.AddInt64(&s.failsCount, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) isSick() bool { |
||||||
|
x := atomic.LoadInt64(&s.failsCount) |
||||||
|
if s.config.Proxy.HealthCheck && x >= s.config.Proxy.MaxFails { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) markOk() { |
||||||
|
atomic.StoreInt64(&s.failsCount, 0) |
||||||
|
} |
||||||
@ -0,0 +1,221 @@ |
|||||||
|
package proxy |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
MaxReqSize = 1024 |
||||||
|
) |
||||||
|
|
||||||
|
func (s *ProxyServer) ListenTCP() { |
||||||
|
timeout := util.MustParseDuration(s.config.Proxy.Stratum.Timeout) |
||||||
|
s.timeout = timeout |
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", s.config.Proxy.Stratum.Listen) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Error: %v", err) |
||||||
|
} |
||||||
|
server, err := net.ListenTCP("tcp", addr) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Error: %v", err) |
||||||
|
} |
||||||
|
defer server.Close() |
||||||
|
|
||||||
|
log.Printf("Stratum listening on %s", s.config.Proxy.Stratum.Listen) |
||||||
|
var accept = make(chan int, s.config.Proxy.Stratum.MaxConn) |
||||||
|
n := 0 |
||||||
|
|
||||||
|
for { |
||||||
|
conn, err := server.AcceptTCP() |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
conn.SetKeepAlive(true) |
||||||
|
|
||||||
|
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) |
||||||
|
|
||||||
|
if s.policy.IsBanned(ip) || !s.policy.ApplyLimitPolicy(ip) { |
||||||
|
conn.Close() |
||||||
|
continue |
||||||
|
} |
||||||
|
n += 1 |
||||||
|
cs := &Session{conn: conn, ip: ip} |
||||||
|
|
||||||
|
accept <- n |
||||||
|
go func(cs *Session) { |
||||||
|
err = s.handleTCPClient(cs) |
||||||
|
if err != nil || cs.lastErr != nil { |
||||||
|
s.removeSession(cs) |
||||||
|
conn.Close() |
||||||
|
} |
||||||
|
<-accept |
||||||
|
}(cs) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) handleTCPClient(cs *Session) error { |
||||||
|
cs.enc = json.NewEncoder(cs.conn) |
||||||
|
connbuff := bufio.NewReaderSize(cs.conn, MaxReqSize) |
||||||
|
s.setDeadline(cs.conn) |
||||||
|
|
||||||
|
for { |
||||||
|
data, isPrefix, err := connbuff.ReadLine() |
||||||
|
if isPrefix { |
||||||
|
log.Printf("Socket flood detected from %s", cs.ip) |
||||||
|
s.policy.BanClient(cs.ip) |
||||||
|
return err |
||||||
|
} else if err == io.EOF { |
||||||
|
log.Printf("Client %s disconnected", cs.ip) |
||||||
|
s.removeSession(cs) |
||||||
|
break |
||||||
|
} else if err != nil { |
||||||
|
log.Printf("Error reading from socket: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if len(data) > 1 { |
||||||
|
var req StratumReq |
||||||
|
err = json.Unmarshal(data, &req) |
||||||
|
if err != nil { |
||||||
|
s.policy.ApplyMalformedPolicy(cs.ip) |
||||||
|
log.Printf("Malformed stratum request from %s: %v", cs.ip, err) |
||||||
|
return err |
||||||
|
} |
||||||
|
s.setDeadline(cs.conn) |
||||||
|
err = cs.handleTCPMessage(s, &req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) handleTCPMessage(s *ProxyServer, req *StratumReq) error { |
||||||
|
// Handle RPC methods
|
||||||
|
switch req.Method { |
||||||
|
case "eth_submitLogin": |
||||||
|
var params []string |
||||||
|
err := json.Unmarshal(req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Println("Malformed stratum request params from", cs.ip) |
||||||
|
return err |
||||||
|
} |
||||||
|
reply, errReply := s.handleLoginRPC(cs, params, req.Worker) |
||||||
|
if errReply != nil { |
||||||
|
return cs.sendTCPError(req.Id, errReply) |
||||||
|
} |
||||||
|
return cs.sendTCPResult(req.Id, reply) |
||||||
|
case "eth_getWork": |
||||||
|
reply, errReply := s.handleGetWorkRPC(cs) |
||||||
|
if errReply != nil { |
||||||
|
return cs.sendTCPError(req.Id, errReply) |
||||||
|
} |
||||||
|
return cs.sendTCPResult(req.Id, &reply) |
||||||
|
case "eth_submitWork": |
||||||
|
var params []string |
||||||
|
err := json.Unmarshal(req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Println("Malformed stratum request params from", cs.ip) |
||||||
|
return err |
||||||
|
} |
||||||
|
reply, errReply := s.handleTCPSubmitRPC(cs, req.Worker, params) |
||||||
|
if errReply != nil { |
||||||
|
return cs.sendTCPError(req.Id, errReply) |
||||||
|
} |
||||||
|
return cs.sendTCPResult(req.Id, &reply) |
||||||
|
case "eth_submitHashrate": |
||||||
|
return cs.sendTCPResult(req.Id, true) |
||||||
|
default: |
||||||
|
errReply := s.handleUnknownRPC(cs, req.Method) |
||||||
|
return cs.sendTCPError(req.Id, errReply) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendTCPResult(id json.RawMessage, result interface{}) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
|
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: nil, Result: result} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) pushNewJob(result interface{}) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
// FIXME: Temporarily add ID for Claymore compliance
|
||||||
|
message := JSONPushMessage{Version: "2.0", Result: result, Id: 0} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendTCPError(id json.RawMessage, reply *ErrorReply) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
|
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: reply} |
||||||
|
err := cs.enc.Encode(&message) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return errors.New(reply.Message) |
||||||
|
} |
||||||
|
|
||||||
|
func (self *ProxyServer) setDeadline(conn *net.TCPConn) { |
||||||
|
conn.SetDeadline(time.Now().Add(self.timeout)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) registerSession(cs *Session) { |
||||||
|
s.sessionsMu.Lock() |
||||||
|
defer s.sessionsMu.Unlock() |
||||||
|
s.sessions[cs] = struct{}{} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) removeSession(cs *Session) { |
||||||
|
s.sessionsMu.Lock() |
||||||
|
defer s.sessionsMu.Unlock() |
||||||
|
delete(s.sessions, cs) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *ProxyServer) broadcastNewJobs() { |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
if t == nil || len(t.Header) == 0 || s.isSick() { |
||||||
|
return |
||||||
|
} |
||||||
|
reply := []string{t.Header, t.Seed, s.diff} |
||||||
|
|
||||||
|
s.sessionsMu.RLock() |
||||||
|
defer s.sessionsMu.RUnlock() |
||||||
|
|
||||||
|
count := len(s.sessions) |
||||||
|
log.Printf("Broadcasting new job to %v stratum miners", count) |
||||||
|
|
||||||
|
start := time.Now() |
||||||
|
bcast := make(chan int, 1024) |
||||||
|
n := 0 |
||||||
|
|
||||||
|
for m, _ := range s.sessions { |
||||||
|
n++ |
||||||
|
bcast <- n |
||||||
|
|
||||||
|
go func(cs *Session) { |
||||||
|
err := cs.pushNewJob(&reply) |
||||||
|
<-bcast |
||||||
|
if err != nil { |
||||||
|
log.Printf("Job transmit error to %v@%v: %v", cs.login, cs.ip, err) |
||||||
|
s.removeSession(cs) |
||||||
|
} else { |
||||||
|
s.setDeadline(cs.conn) |
||||||
|
} |
||||||
|
}(m) |
||||||
|
} |
||||||
|
log.Printf("Jobs broadcast finished %s", time.Since(start)) |
||||||
|
} |
||||||
@ -0,0 +1,300 @@ |
|||||||
|
package rpc |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"math/big" |
||||||
|
"net/http" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
|
||||||
|
"github.com/yuriy0803/open-etc-pool-friends/util" |
||||||
|
) |
||||||
|
|
||||||
|
type RPCClient struct { |
||||||
|
sync.RWMutex |
||||||
|
Url string |
||||||
|
Name string |
||||||
|
sick bool |
||||||
|
sickRate int |
||||||
|
successRate int |
||||||
|
client *http.Client |
||||||
|
} |
||||||
|
|
||||||
|
type GetBlockReply struct { |
||||||
|
Number string `json:"number"` |
||||||
|
Hash string `json:"hash"` |
||||||
|
Nonce string `json:"nonce"` |
||||||
|
Miner string `json:"miner"` |
||||||
|
Difficulty string `json:"difficulty"` |
||||||
|
GasLimit string `json:"gasLimit"` |
||||||
|
GasUsed string `json:"gasUsed"` |
||||||
|
Transactions []Tx `json:"transactions"` |
||||||
|
Uncles []string `json:"uncles"` |
||||||
|
// https://github.com/ethereum/EIPs/issues/95
|
||||||
|
SealFields []string `json:"sealFields"` |
||||||
|
} |
||||||
|
|
||||||
|
type GetBlockReplyPart struct { |
||||||
|
Number string `json:"number"` |
||||||
|
Difficulty string `json:"difficulty"` |
||||||
|
} |
||||||
|
|
||||||
|
const receiptStatusSuccessful = "0x1" |
||||||
|
|
||||||
|
type TxReceipt struct { |
||||||
|
TxHash string `json:"transactionHash"` |
||||||
|
GasUsed string `json:"gasUsed"` |
||||||
|
BlockHash string `json:"blockHash"` |
||||||
|
Status string `json:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
func (r *TxReceipt) Confirmed() bool { |
||||||
|
return len(r.BlockHash) > 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Use with previous method
|
||||||
|
func (r *TxReceipt) Successful() bool { |
||||||
|
if len(r.Status) > 0 { |
||||||
|
return r.Status == receiptStatusSuccessful |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
type Tx struct { |
||||||
|
Gas string `json:"gas"` |
||||||
|
GasPrice string `json:"gasPrice"` |
||||||
|
Hash string `json:"hash"` |
||||||
|
} |
||||||
|
|
||||||
|
type JSONRpcResp struct { |
||||||
|
Id *json.RawMessage `json:"id"` |
||||||
|
Result *json.RawMessage `json:"result"` |
||||||
|
Error map[string]interface{} `json:"error"` |
||||||
|
} |
||||||
|
|
||||||
|
func NewRPCClient(name, url, timeout string) *RPCClient { |
||||||
|
rpcClient := &RPCClient{Name: name, Url: url} |
||||||
|
timeoutIntv := util.MustParseDuration(timeout) |
||||||
|
rpcClient.client = &http.Client{ |
||||||
|
Timeout: timeoutIntv, |
||||||
|
} |
||||||
|
return rpcClient |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetWork() ([]string, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_getWork", []string{}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var reply []string |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetPendingBlock() (*GetBlockReplyPart, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_getBlockByNumber", []interface{}{"latest", true}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if rpcResp.Result != nil { |
||||||
|
var reply *GetBlockReplyPart |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetBlockByHeight(height int64) (*GetBlockReply, error) { |
||||||
|
params := []interface{}{fmt.Sprintf("0x%x", height), true} |
||||||
|
return r.getBlockBy("eth_getBlockByNumber", params) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetBlockByHash(hash string) (*GetBlockReply, error) { |
||||||
|
params := []interface{}{hash, true} |
||||||
|
return r.getBlockBy("eth_getBlockByHash", params) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetUncleByBlockNumberAndIndex(height int64, index int) (*GetBlockReply, error) { |
||||||
|
params := []interface{}{fmt.Sprintf("0x%x", height), fmt.Sprintf("0x%x", index)} |
||||||
|
return r.getBlockBy("eth_getUncleByBlockNumberAndIndex", params) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) getBlockBy(method string, params []interface{}) (*GetBlockReply, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, method, params) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if rpcResp.Result != nil { |
||||||
|
var reply *GetBlockReply |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetTxReceipt(hash string) (*TxReceipt, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_getTransactionReceipt", []string{hash}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if rpcResp.Result != nil { |
||||||
|
var reply *TxReceipt |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) SubmitBlock(params []string) (bool, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_submitWork", params) |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
var reply bool |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetBalance(address string) (*big.Int, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_getBalance", []string{address, "latest"}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var reply string |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return util.String2Big(reply), err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) Sign(from string, s string) (string, error) { |
||||||
|
hash := sha256.Sum256([]byte(s)) |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_sign", []string{from, common.ToHex(hash[:])}) |
||||||
|
var reply string |
||||||
|
if err != nil { |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
if err != nil { |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
if util.IsZeroHash(reply) { |
||||||
|
err = errors.New("Can't sign message, perhaps account is locked") |
||||||
|
} |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetPeerCount() (int64, error) { |
||||||
|
rpcResp, err := r.doPost(r.Url, "net_peerCount", nil) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
var reply string |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return strconv.ParseInt(strings.Replace(reply, "0x", "", -1), 16, 64) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) SendTransaction(from, to, gas, gasPrice, value string, autoGas bool) (string, error) { |
||||||
|
params := map[string]string{ |
||||||
|
"from": from, |
||||||
|
"to": to, |
||||||
|
"value": value, |
||||||
|
} |
||||||
|
if !autoGas { |
||||||
|
params["gas"] = gas |
||||||
|
params["gasPrice"] = gasPrice |
||||||
|
} |
||||||
|
rpcResp, err := r.doPost(r.Url, "eth_sendTransaction", []interface{}{params}) |
||||||
|
var reply string |
||||||
|
if err != nil { |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
if err != nil { |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
/* There is an inconsistence in a "standard". Geth returns error if it can't unlock signer account, |
||||||
|
* but Parity returns zero hash 0x000... if it can't send tx, so we must handle this case. |
||||||
|
* https://github.com/ethereum/wiki/wiki/JSON-RPC#returns-22
|
||||||
|
*/ |
||||||
|
if util.IsZeroHash(reply) { |
||||||
|
err = errors.New("transaction is not yet available") |
||||||
|
} |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) doPost(url string, method string, params interface{}) (*JSONRpcResp, error) { |
||||||
|
jsonReq := map[string]interface{}{"jsonrpc": "2.0", "method": method, "params": params, "id": 0} |
||||||
|
data, _ := json.Marshal(jsonReq) |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) |
||||||
|
req.Header.Set("Content-Length", (string)(len(data))) |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
req.Header.Set("Accept", "application/json") |
||||||
|
|
||||||
|
resp, err := r.client.Do(req) |
||||||
|
if err != nil { |
||||||
|
r.markSick() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
var rpcResp *JSONRpcResp |
||||||
|
err = json.NewDecoder(resp.Body).Decode(&rpcResp) |
||||||
|
if err != nil { |
||||||
|
r.markSick() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if rpcResp.Error != nil { |
||||||
|
r.markSick() |
||||||
|
return nil, errors.New(rpcResp.Error["message"].(string)) |
||||||
|
} |
||||||
|
return rpcResp, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) Check() bool { |
||||||
|
_, err := r.GetWork() |
||||||
|
if err != nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
r.markAlive() |
||||||
|
return !r.Sick() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) Sick() bool { |
||||||
|
r.RLock() |
||||||
|
defer r.RUnlock() |
||||||
|
return r.sick |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) markSick() { |
||||||
|
r.Lock() |
||||||
|
r.sickRate++ |
||||||
|
r.successRate = 0 |
||||||
|
if r.sickRate >= 5 { |
||||||
|
r.sick = true |
||||||
|
} |
||||||
|
r.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) markAlive() { |
||||||
|
r.Lock() |
||||||
|
r.successRate++ |
||||||
|
if r.successRate >= 5 { |
||||||
|
r.sick = false |
||||||
|
r.sickRate = 0 |
||||||
|
r.successRate = 0 |
||||||
|
} |
||||||
|
r.Unlock() |
||||||
|
} |
||||||
@ -0,0 +1,2 @@ |
|||||||
|
#!/bin/bash |
||||||
|
./build/bin/open-etc-pool-friends ./configs/stratum2b.json |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
#!/bin/bash |
||||||
|
#will make the services for the pool, based on the pool exe location of /usr/local/bin/poolbin |
||||||
|
user="perklepool" |
||||||
|
coin="prkl" |
||||||
|
config_dir="/home/$user/open-etc-pool-friends/configs" |
||||||
|
poolbinary="/home/$user/open-etc-pool-friends/build/bin/open-etc-pool-friends" |
||||||
|
|
||||||
|
if [ ! -e $config_dir ] || [ ! -e $poolbinary ] |
||||||
|
then |
||||||
|
echo missing config dir or pool binary, exiting |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-api |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/api.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-api.service |
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-stratum2b |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/stratum2b.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-stratum2b.service |
||||||
|
|
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-stratum4b |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/stratum4b.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-stratum4b.service |
||||||
|
|
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-stratum9b |
||||||
|
|
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/stratum9b.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-stratum9b.service |
||||||
|
|
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-unlocker |
||||||
|
|
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/unlocker.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-unlocker.service |
||||||
|
|
||||||
|
echo " |
||||||
|
[Unit] |
||||||
|
Description=$coin-payout |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
ExecStart=$poolbinary $config_dir/payout.json |
||||||
|
|
||||||
|
[Install] |
||||||
|
WantedBy=multi-user.target |
||||||
|
">/etc/systemd/system/$coin-payout.service |
||||||
|
|
||||||
|
systemctl daemon-reload |
||||||
|
|
||||||
|
systemctl enable $coin-api |
||||||
|
systemctl enable $coin-stratum2b |
||||||
|
systemctl enable $coin-stratum4b |
||||||
|
systemctl enable $coin-stratum9b |
||||||
|
#systemctl enable $coin-unlocker |
||||||
|
#systemctl enable $coin-payout |
||||||
|
|
||||||
|
systemctl start $coin-api |
||||||
|
systemctl start $coin-stratum2b |
||||||
|
systemctl start $coin-stratum4b |
||||||
|
systemctl start $coin-stratum9b |
||||||
|
#systemctl start $coin-unlocker |
||||||
|
#systemctl start $coin-payout |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,329 @@ |
|||||||
|
package storage |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"reflect" |
||||||
|
"strconv" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"gopkg.in/redis.v3" |
||||||
|
) |
||||||
|
|
||||||
|
var r *RedisClient |
||||||
|
|
||||||
|
const prefix = "test" |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
r = NewRedisClient(&Config{Endpoint: "127.0.0.1:6379"}, prefix, 3000) |
||||||
|
reset() |
||||||
|
c := m.Run() |
||||||
|
reset() |
||||||
|
os.Exit(c) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWriteShareCheckExist(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
exist, _ := r.WriteShare("x", "x", []string{"0x0", "0x0", "0x0"}, 10, 1008, 0) |
||||||
|
if exist { |
||||||
|
t.Error("PoW must not exist") |
||||||
|
} |
||||||
|
exist, _ = r.WriteShare("x", "x", []string{"0x0", "0x1", "0x0"}, 10, 1008, 0) |
||||||
|
if exist { |
||||||
|
t.Error("PoW must not exist") |
||||||
|
} |
||||||
|
exist, _ = r.WriteShare("x", "x", []string{"0x0", "0x0", "0x1"}, 100, 1010, 0) |
||||||
|
if exist { |
||||||
|
t.Error("PoW must not exist") |
||||||
|
} |
||||||
|
exist, _ = r.WriteShare("z", "x", []string{"0x0", "0x0", "0x1"}, 100, 1016, 0) |
||||||
|
if !exist { |
||||||
|
t.Error("PoW must exist") |
||||||
|
} |
||||||
|
exist, _ = r.WriteShare("x", "x", []string{"0x0", "0x0", "0x1"}, 100, 1025, 0) |
||||||
|
if exist { |
||||||
|
t.Error("PoW must not exist") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetPayees(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
n := 256 |
||||||
|
for i := 0; i < n; i++ { |
||||||
|
r.client.HSet(r.formatKey("miners", strconv.Itoa(i)), "balance", strconv.Itoa(i)) |
||||||
|
} |
||||||
|
|
||||||
|
var payees []string |
||||||
|
payees, _ = r.GetPayees() |
||||||
|
if len(payees) != n { |
||||||
|
t.Error("Must return all payees") |
||||||
|
} |
||||||
|
m := make(map[string]struct{}) |
||||||
|
for _, v := range payees { |
||||||
|
m[v] = struct{}{} |
||||||
|
} |
||||||
|
if len(m) != n { |
||||||
|
t.Error("Must be unique list") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetBalance(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.HSet(r.formatKey("miners:x"), "balance", "750") |
||||||
|
|
||||||
|
v, _ := r.GetBalance("x") |
||||||
|
if v != 750 { |
||||||
|
t.Error("Must return balance") |
||||||
|
} |
||||||
|
|
||||||
|
v, err := r.GetBalance("z") |
||||||
|
if v != 0 { |
||||||
|
t.Error("Must return 0 if account does not exist") |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
t.Error("Must not return error if account does not exist") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestLockPayouts(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.LockPayouts("x", 1000) |
||||||
|
v := r.client.Get("test:payments:lock").Val() |
||||||
|
if v != "x:1000" { |
||||||
|
t.Errorf("Invalid lock amount: %v", v) |
||||||
|
} |
||||||
|
|
||||||
|
err := r.LockPayouts("x", 100) |
||||||
|
if err == nil { |
||||||
|
t.Errorf("Must not overwrite lock") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestUnlockPayouts(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.Set(r.formatKey("payments:lock"), "x:1000", 0) |
||||||
|
|
||||||
|
r.UnlockPayouts() |
||||||
|
err := r.client.Get(r.formatKey("payments:lock")).Err() |
||||||
|
if err != redis.Nil { |
||||||
|
t.Errorf("Must release lock") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestIsPayoutsLocked(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.LockPayouts("x", 1000) |
||||||
|
if locked, _ := r.IsPayoutsLocked(); !locked { |
||||||
|
t.Errorf("Payouts must be locked") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestUpdateBalance(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("miners:x"), |
||||||
|
map[string]string{"paid": "50", "balance": "1000"}, |
||||||
|
) |
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("finances"), |
||||||
|
map[string]string{"paid": "500", "balance": "10000"}, |
||||||
|
) |
||||||
|
|
||||||
|
amount := int64(250) |
||||||
|
r.UpdateBalance("x", amount) |
||||||
|
result := r.client.HGetAllMap(r.formatKey("miners:x")).Val() |
||||||
|
if result["pending"] != "250" { |
||||||
|
t.Error("Must set pending amount") |
||||||
|
} |
||||||
|
if result["balance"] != "750" { |
||||||
|
t.Error("Must deduct balance") |
||||||
|
} |
||||||
|
if result["paid"] != "50" { |
||||||
|
t.Error("Must not touch paid") |
||||||
|
} |
||||||
|
|
||||||
|
result = r.client.HGetAllMap(r.formatKey("finances")).Val() |
||||||
|
if result["pending"] != "250" { |
||||||
|
t.Error("Must set pool pending amount") |
||||||
|
} |
||||||
|
if result["balance"] != "9750" { |
||||||
|
t.Error("Must deduct pool balance") |
||||||
|
} |
||||||
|
if result["paid"] != "500" { |
||||||
|
t.Error("Must not touch pool paid") |
||||||
|
} |
||||||
|
|
||||||
|
rank := r.client.ZRank(r.formatKey("payments:pending"), join("x", amount)).Val() |
||||||
|
if rank != 0 { |
||||||
|
t.Error("Must add pending payment") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestRollbackBalance(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("miners:x"), |
||||||
|
map[string]string{"paid": "100", "balance": "750", "pending": "250"}, |
||||||
|
) |
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("finances"), |
||||||
|
map[string]string{"paid": "500", "balance": "10000", "pending": "250"}, |
||||||
|
) |
||||||
|
r.client.ZAdd(r.formatKey("payments:pending"), redis.Z{Score: 1, Member: "xx"}) |
||||||
|
|
||||||
|
amount := int64(250) |
||||||
|
r.RollbackBalance("x", amount) |
||||||
|
result := r.client.HGetAllMap(r.formatKey("miners:x")).Val() |
||||||
|
if result["paid"] != "100" { |
||||||
|
t.Error("Must not touch paid") |
||||||
|
} |
||||||
|
if result["balance"] != "1000" { |
||||||
|
t.Error("Must increase balance") |
||||||
|
} |
||||||
|
if result["pending"] != "0" { |
||||||
|
t.Error("Must deduct pending") |
||||||
|
} |
||||||
|
|
||||||
|
result = r.client.HGetAllMap(r.formatKey("finances")).Val() |
||||||
|
if result["paid"] != "500" { |
||||||
|
t.Error("Must not touch pool paid") |
||||||
|
} |
||||||
|
if result["balance"] != "10250" { |
||||||
|
t.Error("Must increase pool balance") |
||||||
|
} |
||||||
|
if result["pending"] != "0" { |
||||||
|
t.Error("Must deduct pool pending") |
||||||
|
} |
||||||
|
|
||||||
|
err := r.client.ZRank(r.formatKey("payments:pending"), join("x", amount)).Err() |
||||||
|
if err != redis.Nil { |
||||||
|
t.Errorf("Must remove pending payment") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestWritePayment(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("miners:x"), |
||||||
|
map[string]string{"paid": "50", "balance": "1000", "pending": "250"}, |
||||||
|
) |
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("finances"), |
||||||
|
map[string]string{"paid": "500", "balance": "10000", "pending": "250"}, |
||||||
|
) |
||||||
|
|
||||||
|
amount := int64(250) |
||||||
|
r.WritePayment("x", "0x0", amount) |
||||||
|
result := r.client.HGetAllMap(r.formatKey("miners:x")).Val() |
||||||
|
if result["pending"] != "0" { |
||||||
|
t.Error("Must unset pending amount") |
||||||
|
} |
||||||
|
if result["balance"] != "1000" { |
||||||
|
t.Error("Must not touch balance") |
||||||
|
} |
||||||
|
if result["paid"] != "300" { |
||||||
|
t.Error("Must increase paid") |
||||||
|
} |
||||||
|
|
||||||
|
result = r.client.HGetAllMap(r.formatKey("finances")).Val() |
||||||
|
if result["pending"] != "0" { |
||||||
|
t.Error("Must deduct pool pending amount") |
||||||
|
} |
||||||
|
if result["balance"] != "10000" { |
||||||
|
t.Error("Must not touch pool balance") |
||||||
|
} |
||||||
|
if result["paid"] != "750" { |
||||||
|
t.Error("Must increase pool paid") |
||||||
|
} |
||||||
|
|
||||||
|
err := r.client.Get(r.formatKey("payments:lock")).Err() |
||||||
|
if err != redis.Nil { |
||||||
|
t.Errorf("Must release lock") |
||||||
|
} |
||||||
|
|
||||||
|
err = r.client.ZRank(r.formatKey("payments:pending"), join("x", amount)).Err() |
||||||
|
if err != redis.Nil { |
||||||
|
t.Error("Must remove pending payment") |
||||||
|
} |
||||||
|
err = r.client.ZRank(r.formatKey("payments:all"), join("0x0", "x", amount)).Err() |
||||||
|
if err == redis.Nil { |
||||||
|
t.Error("Must add payment to set") |
||||||
|
} |
||||||
|
err = r.client.ZRank(r.formatKey("payments:x"), join("0x0", amount)).Err() |
||||||
|
if err == redis.Nil { |
||||||
|
t.Error("Must add payment to set") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetPendingPayments(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
r.client.HMSetMap( |
||||||
|
r.formatKey("miners:x"), |
||||||
|
map[string]string{"paid": "100", "balance": "750", "pending": "250"}, |
||||||
|
) |
||||||
|
|
||||||
|
amount := int64(1000) |
||||||
|
r.UpdateBalance("x", amount) |
||||||
|
pending := r.GetPendingPayments() |
||||||
|
|
||||||
|
if len(pending) != 1 { |
||||||
|
t.Error("Must return pending payment") |
||||||
|
} |
||||||
|
if pending[0].Amount != amount { |
||||||
|
t.Error("Must have corrent amount") |
||||||
|
} |
||||||
|
if pending[0].Address != "x" { |
||||||
|
t.Error("Must have corrent account") |
||||||
|
} |
||||||
|
if pending[0].Timestamp <= 0 { |
||||||
|
t.Error("Must have timestamp") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestCollectLuckStats(t *testing.T) { |
||||||
|
reset() |
||||||
|
|
||||||
|
members := []redis.Z{ |
||||||
|
redis.Z{Score: 0, Member: "1:0:0x0:0x0:0:100:100:0"}, |
||||||
|
} |
||||||
|
r.client.ZAdd(r.formatKey("blocks:immature"), members...) |
||||||
|
members = []redis.Z{ |
||||||
|
redis.Z{Score: 1, Member: "1:0:0x2:0x0:0:50:100:0"}, |
||||||
|
redis.Z{Score: 2, Member: "0:1:0x1:0x0:0:100:100:0"}, |
||||||
|
redis.Z{Score: 3, Member: "0:0:0x3:0x0:0:200:100:0"}, |
||||||
|
} |
||||||
|
r.client.ZAdd(r.formatKey("blocks:matured"), members...) |
||||||
|
|
||||||
|
stats, _ := r.CollectLuckStats([]int{1, 2, 5, 10}) |
||||||
|
expectedStats := map[string]interface{}{ |
||||||
|
"1": map[string]float64{ |
||||||
|
"luck": 1, "uncleRate": 1, "orphanRate": 0, |
||||||
|
}, |
||||||
|
"2": map[string]float64{ |
||||||
|
"luck": 0.75, "uncleRate": 0.5, "orphanRate": 0, |
||||||
|
}, |
||||||
|
"4": map[string]float64{ |
||||||
|
"luck": 1.125, "uncleRate": 0.5, "orphanRate": 0.25, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
if !reflect.DeepEqual(stats, expectedStats) { |
||||||
|
t.Error("Stats != expected stats") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func reset() { |
||||||
|
keys := r.client.Keys(r.prefix + ":*").Val() |
||||||
|
for _, k := range keys { |
||||||
|
r.client.Del(k) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
package util |
||||||
|
|
||||||
|
import ( |
||||||
|
"math/big" |
||||||
|
"regexp" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common" |
||||||
|
"github.com/ethereum/go-ethereum/common/math" |
||||||
|
) |
||||||
|
|
||||||
|
var Ether = math.BigPow(10, 18) |
||||||
|
var Shannon = math.BigPow(10, 9) |
||||||
|
|
||||||
|
var pow256 = math.BigPow(2, 256) |
||||||
|
var addressPattern = regexp.MustCompile("^0x[0-9a-fA-F]{40}$") |
||||||
|
var zeroHash = regexp.MustCompile("^0?x?0+$") |
||||||
|
|
||||||
|
func IsValidHexAddress(s string) bool { |
||||||
|
if IsZeroHash(s) || !addressPattern.MatchString(s) { |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func IsZeroHash(s string) bool { |
||||||
|
return zeroHash.MatchString(s) |
||||||
|
} |
||||||
|
|
||||||
|
func MakeTimestamp() int64 { |
||||||
|
return time.Now().UnixNano() / int64(time.Millisecond) |
||||||
|
} |
||||||
|
|
||||||
|
func GetTargetHex(diff int64) string { |
||||||
|
difficulty := big.NewInt(diff) |
||||||
|
diff1 := new(big.Int).Div(pow256, difficulty) |
||||||
|
return string(common.ToHex(diff1.Bytes())) |
||||||
|
} |
||||||
|
|
||||||
|
func TargetHexToDiff(targetHex string) *big.Int { |
||||||
|
targetBytes := common.FromHex(targetHex) |
||||||
|
return new(big.Int).Div(pow256, new(big.Int).SetBytes(targetBytes)) |
||||||
|
} |
||||||
|
|
||||||
|
func ToHex(n int64) string { |
||||||
|
return "0x0" + strconv.FormatInt(n, 16) |
||||||
|
} |
||||||
|
|
||||||
|
func FormatReward(reward *big.Int) string { |
||||||
|
return reward.String() |
||||||
|
} |
||||||
|
|
||||||
|
func FormatRatReward(reward *big.Rat) string { |
||||||
|
wei := new(big.Rat).SetInt(Ether) |
||||||
|
reward = reward.Quo(reward, wei) |
||||||
|
return reward.FloatString(8) |
||||||
|
} |
||||||
|
|
||||||
|
func StringInSlice(a string, list []string) bool { |
||||||
|
for _, b := range list { |
||||||
|
if b == a { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func MustParseDuration(s string) time.Duration { |
||||||
|
value, err := time.ParseDuration(s) |
||||||
|
if err != nil { |
||||||
|
panic("util: Can't parse duration `" + s + "`: " + err.Error()) |
||||||
|
} |
||||||
|
return value |
||||||
|
} |
||||||
|
|
||||||
|
func String2Big(num string) *big.Int { |
||||||
|
n := new(big.Int) |
||||||
|
n.SetString(num, 0) |
||||||
|
return n |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"directory": "bower_components", |
||||||
|
"analytics": false |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
# EditorConfig helps developers define and maintain consistent |
||||||
|
# coding styles between different editors and IDEs |
||||||
|
# editorconfig.org |
||||||
|
|
||||||
|
root = true |
||||||
|
|
||||||
|
|
||||||
|
[*] |
||||||
|
end_of_line = lf |
||||||
|
charset = utf-8 |
||||||
|
trim_trailing_whitespace = true |
||||||
|
insert_final_newline = true |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.js] |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.hbs] |
||||||
|
insert_final_newline = false |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.css] |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.html] |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.{diff,md}] |
||||||
|
trim_trailing_whitespace = false |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
{ |
||||||
|
/** |
||||||
|
Ember CLI sends analytics information by default. The data is completely |
||||||
|
anonymous, but there are times when you might want to disable this behavior. |
||||||
|
|
||||||
|
Setting `disableAnalytics` to true will prevent any data from being sent. |
||||||
|
*/ |
||||||
|
"disableAnalytics": false |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files. |
||||||
|
|
||||||
|
# compiled output |
||||||
|
/dist |
||||||
|
/tmp |
||||||
|
|
||||||
|
# dependencies |
||||||
|
/node_modules |
||||||
|
/bower_components |
||||||
|
|
||||||
|
# misc |
||||||
|
/.sass-cache |
||||||
|
/connect.lock |
||||||
|
/coverage/* |
||||||
|
/libpeerconnection.log |
||||||
|
npm-debug.log |
||||||
|
testem.log |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
{ |
||||||
|
"predef": [ |
||||||
|
"document", |
||||||
|
"window", |
||||||
|
"-Promise", |
||||||
|
"moment" |
||||||
|
], |
||||||
|
"browser": true, |
||||||
|
"boss": true, |
||||||
|
"curly": true, |
||||||
|
"debug": false, |
||||||
|
"devel": true, |
||||||
|
"eqeqeq": true, |
||||||
|
"evil": true, |
||||||
|
"forin": false, |
||||||
|
"immed": false, |
||||||
|
"laxbreak": false, |
||||||
|
"newcap": true, |
||||||
|
"noarg": true, |
||||||
|
"noempty": false, |
||||||
|
"nonew": false, |
||||||
|
"nomen": false, |
||||||
|
"onevar": false, |
||||||
|
"plusplus": false, |
||||||
|
"regexp": false, |
||||||
|
"undef": true, |
||||||
|
"sub": true, |
||||||
|
"strict": false, |
||||||
|
"white": false, |
||||||
|
"eqnull": true, |
||||||
|
"esnext": true, |
||||||
|
"unused": true |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
--- |
||||||
|
language: node_js |
||||||
|
node_js: |
||||||
|
- "0.12" |
||||||
|
|
||||||
|
sudo: false |
||||||
|
|
||||||
|
cache: |
||||||
|
directories: |
||||||
|
- node_modules |
||||||
|
|
||||||
|
before_install: |
||||||
|
- export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH |
||||||
|
- "npm config set spin false" |
||||||
|
- "npm install -g npm@^2" |
||||||
|
|
||||||
|
install: |
||||||
|
- npm install -g bower |
||||||
|
- npm install |
||||||
|
- bower install |
||||||
|
|
||||||
|
script: |
||||||
|
- npm test |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
# Pool |
||||||
|
|
||||||
|
This README outlines the details of collaborating on this Ember application. |
||||||
|
A short introduction of this app could easily go here. |
||||||
|
|
||||||
|
## Prerequisites |
||||||
|
|
||||||
|
You will need the following things properly installed on your computer. |
||||||
|
|
||||||
|
* [Git](http://git-scm.com/) |
||||||
|
* [Node.js](http://nodejs.org/) (with NPM) |
||||||
|
* [Bower](http://bower.io/) |
||||||
|
* [Ember CLI](http://www.ember-cli.com/) |
||||||
|
* [PhantomJS](http://phantomjs.org/) |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
* `git clone <repository-url>` this repository |
||||||
|
* change into the new directory |
||||||
|
* `npm install` |
||||||
|
* `bower install` |
||||||
|
|
||||||
|
## Running / Development |
||||||
|
|
||||||
|
* `ember server` |
||||||
|
* Visit your app at [http://localhost:4200](http://localhost:4200). |
||||||
|
|
||||||
|
### Code Generators |
||||||
|
|
||||||
|
Make use of the many generators for code, try `ember help generate` for more details |
||||||
|
|
||||||
|
### Running Tests |
||||||
|
|
||||||
|
* `ember test` |
||||||
|
* `ember test --server` |
||||||
|
|
||||||
|
### Building |
||||||
|
|
||||||
|
* `ember build` (development) |
||||||
|
* `ember build --environment production` (production) |
||||||
|
|
||||||
|
### Deploying |
||||||
|
|
||||||
|
Specify what it takes to deploy your app. |
||||||
|
|
||||||
|
## Further Reading / Useful Links |
||||||
|
|
||||||
|
* [ember.js](http://emberjs.com/) |
||||||
|
* [ember-cli](http://www.ember-cli.com/) |
||||||
|
* Development Browser Extensions |
||||||
|
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) |
||||||
|
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) |
||||||
|
|
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import Resolver from './resolver'; |
||||||
|
import loadInitializers from 'ember-load-initializers'; |
||||||
|
import config from './config/environment'; |
||||||
|
|
||||||
|
let App; |
||||||
|
|
||||||
|
Ember.MODEL_FACTORY_INJECTIONS = true; |
||||||
|
|
||||||
|
App = Ember.Application.extend({ |
||||||
|
modulePrefix: config.modulePrefix, |
||||||
|
podModulePrefix: config.podModulePrefix, |
||||||
|
Resolver |
||||||
|
}); |
||||||
|
|
||||||
|
loadInitializers(App, config.modulePrefix); |
||||||
|
|
||||||
|
export default App; |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
const { getOwner } = Ember; |
||||||
|
|
||||||
|
export default Ember.Component.extend({ |
||||||
|
tagName: 'li', |
||||||
|
classNameBindings: ['isActive:active:inactive'], |
||||||
|
|
||||||
|
router: function(){ |
||||||
|
return getOwner(this).lookup('router:main'); |
||||||
|
}.property(), |
||||||
|
|
||||||
|
isActive: function(){ |
||||||
|
var currentWhen = this.get('currentWhen'); |
||||||
|
return this.get('router').isActive(currentWhen); |
||||||
|
}.property('router.url', 'currentWhen') |
||||||
|
}); |
||||||
@ -0,0 +1,155 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
applicationController: Ember.inject.controller('application'), |
||||||
|
config: Ember.computed.reads('applicationController.config'), |
||||||
|
stats: Ember.computed.reads('applicationController.model.stats'), |
||||||
|
hashrate: Ember.computed.reads('applicationController.hashrate'), |
||||||
|
chartOptions: Ember.computed("model.hashrate", { |
||||||
|
get() { |
||||||
|
var e = this, |
||||||
|
t = e.getWithDefault("model.minerCharts"), |
||||||
|
a = { |
||||||
|
chart: { |
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.1)", |
||||||
|
type: "spline", |
||||||
|
marginRight: 10, |
||||||
|
height: 400, |
||||||
|
events: { |
||||||
|
load: function() { |
||||||
|
var series = this.series[0]; |
||||||
|
setInterval(function() { |
||||||
|
var x = (new Date()).getTime(), |
||||||
|
y = e.getWithDefault("model.currentHashrate") / 1000000; |
||||||
|
series.addPoint([x, y], true, true); |
||||||
|
}, 109000000); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
title: { |
||||||
|
text: "" |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
ordinal: false, |
||||||
|
type: "datetime", |
||||||
|
dateTimeLabelFormats: { |
||||||
|
millisecond: "%H:%M:%S", |
||||||
|
second: "%H:%M:%S", |
||||||
|
minute: "%H:%M", |
||||||
|
hour: "%H:%M", |
||||||
|
day: "%e. %b", |
||||||
|
week: "%e. %b", |
||||||
|
month: "%b '%y", |
||||||
|
year: "%Y" |
||||||
|
} |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
title: { |
||||||
|
text: "HASHRATE" |
||||||
|
}, |
||||||
|
min: 0 |
||||||
|
}, |
||||||
|
plotLines: [{ |
||||||
|
value: 0, |
||||||
|
width: 1, |
||||||
|
color: "#808080" |
||||||
|
}], |
||||||
|
legend: { |
||||||
|
enabled: true, |
||||||
|
itemStyle: |
||||||
|
{ |
||||||
|
color:"#ffffff" |
||||||
|
}, |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
formatter: function() { |
||||||
|
return this.y > 1000000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s</b>" : this.y > 1000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s</b>" : this.y > 1000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s</b>" : "<b>" + this.point.d + "<b><br>Hashrate <b>" + this.y.toFixed(2) + " H/s</b>"; |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
useHTML: true |
||||||
|
}, |
||||||
|
exporting: { |
||||||
|
enabled: false |
||||||
|
}, |
||||||
|
series: [{ |
||||||
|
color: "#E99002", |
||||||
|
name: "Average hashrate", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].minerLargeHash; |
||||||
|
a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}, { |
||||||
|
name: "Current hashrate", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].minerHash; |
||||||
|
a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}] |
||||||
|
}; |
||||||
|
return a; |
||||||
|
} |
||||||
|
}), |
||||||
|
roundPercent: Ember.computed('stats', 'model', { |
||||||
|
get() { |
||||||
|
var percent = this.get('model.roundShares') / this.get('stats.nShares'); |
||||||
|
if (!percent) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return percent; |
||||||
|
} |
||||||
|
}), |
||||||
|
netHashrate: Ember.computed({ |
||||||
|
get() { |
||||||
|
return this.get('hashrate'); |
||||||
|
} |
||||||
|
}), |
||||||
|
earnPerDay: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
return 24 * 60 * 60 / this.get('config').BlockTime * this.get('config').BlockReward * |
||||||
|
this.getWithDefault('model.hashrate') / this.get('hashrate'); |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
@ -0,0 +1,151 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
applicationController: Ember.inject.controller('application'), |
||||||
|
netstats: Ember.computed.reads('applicationController'), |
||||||
|
stats: Ember.computed.reads('applicationController.model.stats'), |
||||||
|
config: Ember.computed.reads('applicationController.config'), |
||||||
|
|
||||||
|
chartOptions: Ember.computed("model.hashrate", { |
||||||
|
get() { |
||||||
|
var e = this, |
||||||
|
t = e.getWithDefault("model.minerCharts"), |
||||||
|
a = { |
||||||
|
chart: { |
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.1)", |
||||||
|
|
||||||
|
type: "spline", |
||||||
|
marginRight: 10, |
||||||
|
height: 200, |
||||||
|
events: { |
||||||
|
load: function() { |
||||||
|
var series = this.series[0]; |
||||||
|
setInterval(function() { |
||||||
|
var x = (new Date()).getTime(), |
||||||
|
y = e.getWithDefault("model.currentHashrate") / 1000000; |
||||||
|
series.addPoint([x, y], true, true); |
||||||
|
}, 1090000000); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
title: { |
||||||
|
text: "" |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
ordinal: false, |
||||||
|
labels: { |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
}, |
||||||
|
type: "datetime", |
||||||
|
dateTimeLabelFormats: { |
||||||
|
millisecond: "%H:%M:%S", |
||||||
|
second: "%H:%M:%S", |
||||||
|
minute: "%H:%M", |
||||||
|
hour: "%H:%M", |
||||||
|
day: "%e. %b", |
||||||
|
week: "%e. %b", |
||||||
|
month: "%b '%y", |
||||||
|
year: "%Y" |
||||||
|
} |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
title: { |
||||||
|
text: "Hashrate by Account", |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
}, |
||||||
|
}, |
||||||
|
labels: { |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
}, |
||||||
|
//softMin: e.getWithDefault("model.currentHashrate") / 1000000,
|
||||||
|
//softMax: e.getWithDefault("model.currentHashrate") / 1000000,
|
||||||
|
}, |
||||||
|
plotLines: [{ |
||||||
|
value: 0, |
||||||
|
width: 1, |
||||||
|
color: "#aaaaaa" |
||||||
|
}], |
||||||
|
legend: { |
||||||
|
enabled: true, |
||||||
|
itemStyle: |
||||||
|
{ |
||||||
|
color:"#ccc" |
||||||
|
}, |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
formatter: function() { |
||||||
|
return this.y > 1000000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s</b>" : this.y > 1000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s</b>" : this.y > 1000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s</b>" : "<b>" + this.point.d + "<b><br>Hashrate <b>" + this.y.toFixed(2) + " H/s</b>"; |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
useHTML: true |
||||||
|
}, |
||||||
|
exporting: { |
||||||
|
enabled: false |
||||||
|
}, |
||||||
|
series: [{ |
||||||
|
color: "#E99002", |
||||||
|
name: "3 hours average hashrate", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].minerLargeHash; |
||||||
|
a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}, { |
||||||
|
name: "30 minutes average hashrate", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].minerHash; |
||||||
|
a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}] |
||||||
|
}; |
||||||
|
return a; |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
@ -0,0 +1,111 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
applicationController: Ember.inject.controller('application'), |
||||||
|
stats: Ember.computed.reads('applicationController.model.stats'), |
||||||
|
intl: Ember.inject.service(), |
||||||
|
|
||||||
|
chartPaymentText: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
var outText = this.get('model.paymentCharts'); |
||||||
|
if (!outText) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return outText; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
chartPayment: Ember.computed('intl', 'model.paymentCharts', { |
||||||
|
get() { |
||||||
|
var e = this, |
||||||
|
t = e.getWithDefault("model.paymentCharts"), |
||||||
|
a = { |
||||||
|
chart: { |
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.1)", |
||||||
|
type: "column", |
||||||
|
marginRight: 10, |
||||||
|
height: 200, |
||||||
|
events: { |
||||||
|
load: function() { |
||||||
|
var series = this.series[0]; |
||||||
|
setInterval(function() { |
||||||
|
var x = (new Date()).getDate(), |
||||||
|
y = e.getWithDefault("model.paymentCharts"); |
||||||
|
series.addPoint([x, y], true, true); |
||||||
|
}, 1090000000); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
title: { |
||||||
|
text: "" |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
ordinal: false, |
||||||
|
type: "datetime", |
||||||
|
dateTimeLabelFormats: { |
||||||
|
day: "%e. %b", |
||||||
|
week: "%e. %b", |
||||||
|
month: "%b '%y", |
||||||
|
year: "%Y" |
||||||
|
} |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
title: { |
||||||
|
text: "Payment by Account" |
||||||
|
} |
||||||
|
}, |
||||||
|
plotLines: [{ |
||||||
|
value: 0, |
||||||
|
width: 1, |
||||||
|
color: "#808080" |
||||||
|
}], |
||||||
|
legend: { |
||||||
|
enabled: true, |
||||||
|
itemStyle: |
||||||
|
{ |
||||||
|
color:"#ccc" |
||||||
|
}, |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
formatter: function() { |
||||||
|
return "<b>" + Highcharts.dateFormat('%Y-%m-%d', new Date(this.x)) + "<b><br>Payment <b>" + this.y.toFixed(4) + " CLO</b>"; |
||||||
|
}, |
||||||
|
useHTML: true |
||||||
|
}, |
||||||
|
exporting: { |
||||||
|
enabled: false |
||||||
|
}, |
||||||
|
series: [{ |
||||||
|
color: "#E99002", |
||||||
|
name: "Payment Series", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].amount / 1000000000; |
||||||
|
a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}] |
||||||
|
}; |
||||||
|
return a; |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
@ -0,0 +1,105 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
intl: Ember.inject.service(), |
||||||
|
get config() { |
||||||
|
return config.APP; |
||||||
|
}, |
||||||
|
|
||||||
|
height: Ember.computed('model.nodes', { |
||||||
|
get() { |
||||||
|
var node = this.get('bestNode'); |
||||||
|
if (node) { |
||||||
|
return node.height; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
roundShares: Ember.computed('model.stats', { |
||||||
|
get() { |
||||||
|
return parseInt(this.get('model.stats.roundShares')); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
difficulty: Ember.computed('model.nodes', { |
||||||
|
get() { |
||||||
|
var node = this.get('bestNode'); |
||||||
|
if (node) { |
||||||
|
return node.difficulty; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
hashrate: Ember.computed('difficulty', { |
||||||
|
get() { |
||||||
|
return this.getWithDefault('difficulty', 0) / config.APP.BlockTime; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
immatureTotal: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
return this.getWithDefault('model.immatureTotal', 0) + this.getWithDefault('model.candidatesTotal', 0); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
bestNode: Ember.computed('model.nodes', { |
||||||
|
get() { |
||||||
|
var node = null; |
||||||
|
this.get('model.nodes').forEach(function (n) { |
||||||
|
if (!node) { |
||||||
|
node = n; |
||||||
|
} |
||||||
|
if (node.height < n.height) { |
||||||
|
node = n; |
||||||
|
} |
||||||
|
}); |
||||||
|
return node; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
lastBlockFound: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
return parseInt(this.get('model.lastBlockFound')) || 0; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
|
||||||
|
languages: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
return this.get('model.languages'); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
selectedLanguage: Ember.computed({ |
||||||
|
get() { |
||||||
|
var langs = this.get('languages'); |
||||||
|
var lang = Ember.$.cookie('lang'); |
||||||
|
for (var i = 0; i < langs.length; i++) { |
||||||
|
if (langs[i].value == lang) { |
||||||
|
return langs[i].name; |
||||||
|
} |
||||||
|
} |
||||||
|
return lang; |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
roundVariance: Ember.computed('model', { |
||||||
|
get() { |
||||||
|
var percent = this.get('model.stats.roundShares') / this.get('difficulty'); |
||||||
|
if (!percent) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return percent.toFixed(2); |
||||||
|
} |
||||||
|
}), |
||||||
|
|
||||||
|
nextEpoch: Ember.computed('height', { |
||||||
|
get() { |
||||||
|
var epochOffset = (30000 - (this.getWithDefault('height', 1) % 30000)) * 1000 * this.get('config').BlockTime; |
||||||
|
return Date.now() + epochOffset; |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
applicationController: Ember.inject.controller('application'), |
||||||
|
config: Ember.computed.reads('applicationController.config') |
||||||
|
}); |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Controller.extend({ |
||||||
|
applicationController: Ember.inject.controller('application'), |
||||||
|
stats: Ember.computed.reads('applicationController'), |
||||||
|
config: Ember.computed.reads('applicationController.config'), |
||||||
|
|
||||||
|
cachedLogin: Ember.computed('login', { |
||||||
|
get() { |
||||||
|
return this.get('login') || Ember.$.cookie('login'); |
||||||
|
}, |
||||||
|
set(key, value) { |
||||||
|
Ember.$.cookie('login', value); |
||||||
|
this.set('model.login', value); |
||||||
|
return value; |
||||||
|
} |
||||||
|
}), |
||||||
|
chartOptions: Ember.computed("model.hashrate", { |
||||||
|
get() { |
||||||
|
var e = this, |
||||||
|
t = e.getWithDefault("stats.model.poolCharts"), |
||||||
|
a = { |
||||||
|
chart: { |
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.1)", |
||||||
|
type: "spline", |
||||||
|
height: 300, |
||||||
|
marginRight: 10, |
||||||
|
events: { |
||||||
|
load: function() { |
||||||
|
var series = this.series[0]; |
||||||
|
setInterval(function() { |
||||||
|
var x = (new Date()).getTime(), y = e.getWithDefault("model.Hashrate") / 1000000; |
||||||
|
series.addPoint([x, y], true, true); |
||||||
|
}, 1090000000); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
title: { |
||||||
|
text: "Our pool's hashrate", |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
labels: { |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
}, |
||||||
|
ordinal: false, |
||||||
|
type: "datetime" |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
title: { |
||||||
|
text: "HASHRATE", |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
}, |
||||||
|
min: 0, |
||||||
|
labels: { |
||||||
|
style: { |
||||||
|
color: "#ccc" |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
plotLines: [{ |
||||||
|
value: 0, |
||||||
|
width: 1, |
||||||
|
color: "#ccc" |
||||||
|
}], |
||||||
|
legend: { |
||||||
|
enabled: false |
||||||
|
}, |
||||||
|
tooltip: { |
||||||
|
formatter: function() { |
||||||
|
return this.y > 1000000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000000).toFixed(2) + " TH/s</b>" : this.y > 1000000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000000).toFixed(2) + " GH/s</b>" : this.y > 1000000 ? "<b>" + this.point.d + "<b><br>Hashrate " + (this.y / 1000000).toFixed(2) + " MH/s</b>" : "<b>" + this.point.d + "<b><br>Hashrate<b> " + this.y.toFixed(2) + " H/s</b>"; |
||||||
|
}, |
||||||
|
useHTML: true |
||||||
|
}, |
||||||
|
exporting: { |
||||||
|
enabled: false |
||||||
|
}, |
||||||
|
series: [{ |
||||||
|
color: "#15BD27", |
||||||
|
name: "Hashrate", |
||||||
|
data: function() { |
||||||
|
var e, a = []; |
||||||
|
if (null != t) { |
||||||
|
for (e = 0; e <= t.length - 1; e += 1) { |
||||||
|
var n = 0, |
||||||
|
r = 0, |
||||||
|
l = 0; |
||||||
|
r = new Date(1e3 * t[e].x); |
||||||
|
l = r.toLocaleString(); |
||||||
|
n = t[e].y; a.push({ |
||||||
|
x: r, |
||||||
|
d: l, |
||||||
|
y: n |
||||||
|
}); |
||||||
|
} |
||||||
|
} else { |
||||||
|
a.push({ |
||||||
|
x: 0, |
||||||
|
d: 0, |
||||||
|
y: 0 |
||||||
|
}); |
||||||
|
} |
||||||
|
return a; |
||||||
|
}() |
||||||
|
}] |
||||||
|
}; |
||||||
|
return a; |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
var hhmmss = { |
||||||
|
hour: 'numeric', |
||||||
|
minute: 'numeric', |
||||||
|
second: 'numeric' |
||||||
|
}; |
||||||
|
|
||||||
|
export default { |
||||||
|
time: { |
||||||
|
hhmmss: hhmmss |
||||||
|
}, |
||||||
|
date: { |
||||||
|
hhmmss: hhmmss |
||||||
|
}, |
||||||
|
number: { |
||||||
|
EUR: { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }, |
||||||
|
USD: { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 } |
||||||
|
} |
||||||
|
}; |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function formatBalance(value) { |
||||||
|
value = value * 0.000000001; |
||||||
|
return value.toFixed(8); |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(formatBalance); |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function formatDateLocale(ts) { |
||||||
|
var date = new Date(ts * 1000); |
||||||
|
return date.toLocaleString(); |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(formatDateLocale); |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function formatDifficulty(value) { |
||||||
|
value = value / 1000000000 |
||||||
|
return Ember.String.htmlSafe('<span class="label label-success">' + value + 'b</span>'); |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(formatDifficulty); |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function formatHashrate(params/*, hash*/) { |
||||||
|
var hashrate = params[0]; |
||||||
|
var i = 0; |
||||||
|
var units = ['H', 'KH', 'MH', 'GH', 'TH', 'PH']; |
||||||
|
while (hashrate > 1000) { |
||||||
|
hashrate = hashrate / 1000; |
||||||
|
i++; |
||||||
|
} |
||||||
|
return hashrate.toFixed(2) + ' ' + units[i]; |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(formatHashrate); |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function formatTx(value) { |
||||||
|
return value[0].substring(2, 26) + "..." + value[0].substring(42); |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(formatTx); |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function secondsToMs(value) { |
||||||
|
return value * 1000; |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(secondsToMs); |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function stringToInt(value) { |
||||||
|
return parseInt(value); |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(stringToInt); |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function withMetricPrefix(params/*, hash*/) { |
||||||
|
var n = params[0]; |
||||||
|
|
||||||
|
if (n < 1000) { |
||||||
|
return n; |
||||||
|
} |
||||||
|
|
||||||
|
var i = 0; |
||||||
|
var units = ['K', 'M', 'G', 'T', 'P']; |
||||||
|
while (n > 1000) { |
||||||
|
n = n / 1000; |
||||||
|
i++; |
||||||
|
} |
||||||
|
return n.toFixed(3) + ' ' + units[i - 1]; |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(withMetricPrefix); |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export function workerColorizer(value) { |
||||||
|
let class_name; |
||||||
|
let difference_seconds = (Date.now() / 1000) - value; |
||||||
|
|
||||||
|
if (difference_seconds >= (60 * 15)) { |
||||||
|
class_name = "offline-1"; |
||||||
|
} |
||||||
|
|
||||||
|
if (difference_seconds >= (60 * 17)) { |
||||||
|
class_name = "offline-2"; |
||||||
|
} |
||||||
|
|
||||||
|
if (difference_seconds >= (60 * 20)) { |
||||||
|
class_name = "offline-3"; |
||||||
|
} |
||||||
|
|
||||||
|
if (difference_seconds >= (60 * 25)) { |
||||||
|
class_name = "offline-4"; |
||||||
|
} |
||||||
|
|
||||||
|
if (difference_seconds >= (60 * 28)) { |
||||||
|
class_name = "offline-5"; |
||||||
|
} |
||||||
|
|
||||||
|
return class_name; |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(workerColorizer); |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
|
||||||
|
export function workerEarnperday(hashrates) { |
||||||
|
return 24 * 60 * 60 / config.APP.BlockTime * (hashrates[0] / hashrates[1]) * config.APP.BlockReward; |
||||||
|
} |
||||||
|
|
||||||
|
export default Ember.Helper.helper(workerEarnperday); |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
|
<title>Official etc Pool | https://t.me/poolnode</title> |
||||||
|
<meta name="description" content="Highly reliable mining pool"/> |
||||||
|
<meta name="keywords" content="Mining pool, mining, cryptocurrency"/> |
||||||
|
<script src="https://cdn.polyfill.io/v1/polyfill.min.js?features=Intl.~locale.en"></script> |
||||||
|
{{content-for "head"}} |
||||||
|
<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/> |
||||||
|
<script src="https://kit.fontawesome.com/20c576d59b.js" crossorigin="anonymous"></script> |
||||||
|
<link href="https://fonts.googleapis.com/css?family=Noto+Sans|Roboto" rel="stylesheet"> |
||||||
|
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css"> |
||||||
|
<link rel="stylesheet" href="{{rootURL}}assets/open-social-pool.css"> |
||||||
|
{{content-for "head-footer"}} |
||||||
|
</head> |
||||||
|
<body class="bg-dark"> |
||||||
|
{{content-for "body"}} |
||||||
|
<script src="{{rootURL}}assets/vendor.js"></script> |
||||||
|
<script src="{{rootURL}}assets/open-social-pool.js"></script> |
||||||
|
{{content-for "body-footer"}} |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
var Block = Ember.Object.extend({ |
||||||
|
variance: Ember.computed('difficulty', 'shares', function() { |
||||||
|
var percent = this.get('shares') / this.get('difficulty'); |
||||||
|
if (!percent) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
return percent; |
||||||
|
}), |
||||||
|
|
||||||
|
isLucky: Ember.computed('variance', function() { |
||||||
|
return this.get('variance') <= 1.0; |
||||||
|
}), |
||||||
|
|
||||||
|
isOk: Ember.computed('orphan', 'uncle', function() { |
||||||
|
return !this.get('orphan'); |
||||||
|
}), |
||||||
|
|
||||||
|
formatReward: Ember.computed('reward', function() { |
||||||
|
if (!this.get('orphan')) { |
||||||
|
var value = parseInt(this.get('reward')) * 0.000000000000000001; |
||||||
|
return value.toFixed(6); |
||||||
|
} else { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
export default Block; |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
var Payment = Ember.Object.extend({ |
||||||
|
formatAmount: Ember.computed('amount', function() { |
||||||
|
var value = parseInt(this.get('amount')) * 0.000000001; |
||||||
|
return value.toFixed(8); |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
export default Payment; |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
import Resolver from 'ember-resolver'; |
||||||
|
|
||||||
|
export default Resolver; |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from './config/environment'; |
||||||
|
|
||||||
|
var Router = Ember.Router.extend({ |
||||||
|
location: config.locationType |
||||||
|
}); |
||||||
|
|
||||||
|
Router.map(function() { |
||||||
|
this.route('account', { path: '/account/:login' }, function() { |
||||||
|
this.route('payouts'); |
||||||
|
this.route('rewards'); |
||||||
|
}); |
||||||
|
this.route('not-found'); |
||||||
|
|
||||||
|
this.route('blocks', function() { |
||||||
|
this.route('immature'); |
||||||
|
this.route('pending'); |
||||||
|
}); |
||||||
|
|
||||||
|
this.route('help'); |
||||||
|
//this.route('help-ar');
|
||||||
|
this.route('payments'); |
||||||
|
this.route('miners'); |
||||||
|
this.route('about'); |
||||||
|
//this.route('about-ar');
|
||||||
|
}); |
||||||
|
|
||||||
|
export default Router; |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
model: function(params) { |
||||||
|
var url = config.APP.ApiUrl + 'api/accounts/' + params.login; |
||||||
|
return Ember.$.getJSON(url).then(function(data) { |
||||||
|
data.login = params.login; |
||||||
|
return Ember.Object.create(data); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setupController: function(controller, model) { |
||||||
|
this._super(controller, model); |
||||||
|
Ember.run.later(this, this.refresh, 5000); |
||||||
|
}, |
||||||
|
|
||||||
|
actions: { |
||||||
|
error(error) { |
||||||
|
if (error.status === 404) { |
||||||
|
return this.transitionTo('not-found'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
function selectLocale(selected) { |
||||||
|
// FIXME
|
||||||
|
let supported = ['en', 'ar-sa', 'en-us']; |
||||||
|
const language = navigator.languages[0] || navigator.language || navigator.userLanguage; |
||||||
|
|
||||||
|
let locale = selected; |
||||||
|
|
||||||
|
if (locale == null) { |
||||||
|
// default locale
|
||||||
|
locale = language; |
||||||
|
if (supported.indexOf(locale) < 0) { |
||||||
|
locale = locale.replace(/\-[a-zA-Z]*$/, ''); |
||||||
|
} |
||||||
|
} |
||||||
|
if (supported.indexOf(locale) >= 0) { |
||||||
|
if (locale === 'en') { |
||||||
|
locale = 'en-us'; |
||||||
|
} |
||||||
|
} else { |
||||||
|
locale = 'en-us'; |
||||||
|
} |
||||||
|
return locale; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
intl: Ember.inject.service(), |
||||||
|
selectedLanguage: null, |
||||||
|
languages: null, |
||||||
|
beforeModel() { |
||||||
|
let locale = this.get('selectedLanguage'); |
||||||
|
if (!locale) { |
||||||
|
// read cookie
|
||||||
|
locale = Ember.$.cookie('lang'); |
||||||
|
// pick a locale
|
||||||
|
locale = selectLocale(locale); |
||||||
|
|
||||||
|
this.get('intl').setLocale(locale); |
||||||
|
Ember.$.cookie('lang', locale); |
||||||
|
console.log('INFO: locale selected - ' + locale); |
||||||
|
this.set('selectedLanguage', locale); |
||||||
|
} |
||||||
|
let intl = this.get('intl'); |
||||||
|
this.set('languages', [ |
||||||
|
{ name: intl.t('lang.arabic'), value: 'ar-sa'}, |
||||||
|
{ name: intl.t('lang.english'), value: 'en-us'} |
||||||
|
]); |
||||||
|
}, |
||||||
|
|
||||||
|
actions: { |
||||||
|
selectLanguage: function(lang) { |
||||||
|
let selected = lang; |
||||||
|
if (typeof selected === 'undefined') { |
||||||
|
return true; |
||||||
|
} |
||||||
|
let locale = selectLocale(selected); |
||||||
|
this.get('intl').setLocale(locale); |
||||||
|
this.set('selectedLanguage', locale); |
||||||
|
Ember.$.cookie('lang', locale); |
||||||
|
let languages = this.get('languages'); |
||||||
|
for (var i = 0; i < languages.length; i++) { |
||||||
|
if (languages[i].value == locale) { |
||||||
|
Ember.$('#selectedLanguage').html(languages[i].name + '<b class="caret"></b>'); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
model: function() { |
||||||
|
var url = config.APP.ApiUrl + 'api/stats'; |
||||||
|
return Ember.$.getJSON(url).then(function(data) { |
||||||
|
return Ember.Object.create(data); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setupController: function(controller, model) { |
||||||
|
this._super(controller, model); |
||||||
|
Ember.run.later(this, this.refresh, 5000); |
||||||
|
model.languages = this.get('languages'); |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import Block from "../models/block"; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
model: function() { |
||||||
|
var url = config.APP.ApiUrl + 'api/blocks'; |
||||||
|
return Ember.$.getJSON(url).then(function(data) { |
||||||
|
if (data.candidates) { |
||||||
|
data.candidates = data.candidates.map(function(b) { |
||||||
|
return Block.create(b); |
||||||
|
}); |
||||||
|
} |
||||||
|
if (data.immature) { |
||||||
|
data.immature = data.immature.map(function(b) { |
||||||
|
return Block.create(b); |
||||||
|
}); |
||||||
|
} |
||||||
|
if (data.matured) { |
||||||
|
data.matured = data.matured.map(function(b) { |
||||||
|
return Block.create(b); |
||||||
|
}); |
||||||
|
} |
||||||
|
return data; |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setupController: function(controller, model) { |
||||||
|
this._super(controller, model); |
||||||
|
Ember.run.later(this, this.refresh, 5000); |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
actions: { |
||||||
|
lookup(login) { |
||||||
|
if (!Ember.isEmpty(login)) { |
||||||
|
return this.transitionTo('account', login); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
model: function() { |
||||||
|
var url = config.APP.ApiUrl + 'api/miners'; |
||||||
|
return Ember.$.getJSON(url).then(function(data) { |
||||||
|
if (data.miners) { |
||||||
|
// Convert map to array
|
||||||
|
data.miners = Object.keys(data.miners).map((value) => { |
||||||
|
let m = data.miners[value]; |
||||||
|
m.login = value; |
||||||
|
return m; |
||||||
|
}); |
||||||
|
// Sort miners by hashrate
|
||||||
|
data.miners = data.miners.sort((a, b) => { |
||||||
|
if (a.hr < b.hr) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
if (a.hr > b.hr) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
return 0; |
||||||
|
}); |
||||||
|
} |
||||||
|
return data; |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setupController: function(controller, model) { |
||||||
|
this._super(controller, model); |
||||||
|
Ember.run.later(this, this.refresh, 5000); |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import Ember from 'ember'; |
||||||
|
import Payment from "../models/payment"; |
||||||
|
import config from '../config/environment'; |
||||||
|
|
||||||
|
export default Ember.Route.extend({ |
||||||
|
model: function() { |
||||||
|
var url = config.APP.ApiUrl + 'api/payments'; |
||||||
|
return Ember.$.getJSON(url).then(function(data) { |
||||||
|
if (data.payments) { |
||||||
|
data.payments = data.payments.map(function(p) { |
||||||
|
return Payment.create(p); |
||||||
|
}); |
||||||
|
} |
||||||
|
return data; |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
setupController: function(controller, model) { |
||||||
|
this._super(controller, model); |
||||||
|
Ember.run.later(this, this.refresh, 5000); |
||||||
|
} |
||||||
|
}); |
||||||
@ -0,0 +1,364 @@ |
|||||||
|
/* Sticky footer styles |
||||||
|
-------------------------------------------------- */ |
||||||
|
html { |
||||||
|
position: relative; |
||||||
|
min-height: 100%; |
||||||
|
} |
||||||
|
body { |
||||||
|
background: url('/bg.png'); |
||||||
|
} |
||||||
|
|
||||||
|
caption, th { |
||||||
|
text-align: inherit; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* Custom page CSS |
||||||
|
-------------------------------------------------- */ |
||||||
|
/* Not required for template or sticky footer method. */ |
||||||
|
|
||||||
|
body { |
||||||
|
padding-top: 20px; |
||||||
|
padding-bottom: 0px; |
||||||
|
color: #ddd; |
||||||
|
} |
||||||
|
|
||||||
|
body > .container { |
||||||
|
padding: 0px 15px 0; |
||||||
|
} |
||||||
|
.container .text-muted { |
||||||
|
margin: 20px 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* doesn't work -------- */ |
||||||
|
#coin_calculator iframe form{ |
||||||
|
border:none !important; |
||||||
|
border-radius: 0px !important; |
||||||
|
background-color: transparent !important; |
||||||
|
} |
||||||
|
|
||||||
|
/* Menu CSS Stuff */ |
||||||
|
|
||||||
|
.navbar-fixed-bottom .navbar-collapse, .navbar-fixed-top .navbar-collapse { |
||||||
|
max-height: unset; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-collapse.in{ |
||||||
|
overflow: visible; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: 36px; |
||||||
|
line-height: 42px; |
||||||
|
} |
||||||
|
|
||||||
|
h2 { |
||||||
|
font-size: 24px; |
||||||
|
line-height: 32px; |
||||||
|
} |
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 { |
||||||
|
color: #fff; |
||||||
|
margin: 0px 0px 15px 0px; |
||||||
|
font-weight: 400; |
||||||
|
font-family: 'Poppins', sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-title{ |
||||||
|
color:#333; |
||||||
|
} |
||||||
|
|
||||||
|
.space-medium { |
||||||
|
padding-top: 40px; |
||||||
|
padding-bottom: 40px; |
||||||
|
} |
||||||
|
|
||||||
|
.jumbotron { |
||||||
|
margin: 0; |
||||||
|
padding: 40px 0 15px 0; |
||||||
|
margin-bottom: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
code { |
||||||
|
font-size: 80%; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-default { |
||||||
|
background-color: #060820; |
||||||
|
border-color: transparent; |
||||||
|
} |
||||||
|
.navbar-default .navbar-brand { |
||||||
|
color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
.navbar-default .navbar-text { |
||||||
|
color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav > li > a { |
||||||
|
color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { |
||||||
|
color: #ffffff; |
||||||
|
background-color: #0a6c9d; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { |
||||||
|
color: #ffffff; |
||||||
|
background-color: #69102b; |
||||||
|
} |
||||||
|
.navbar-default .navbar-toggle { |
||||||
|
border-color: #0a6c9d; |
||||||
|
} |
||||||
|
.navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { |
||||||
|
background-color: #0a6c9d; |
||||||
|
} |
||||||
|
.navbar-default .navbar-toggle .icon-bar { |
||||||
|
background-color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-collapse, |
||||||
|
.navbar-default .navbar-form { |
||||||
|
border-color: #00ff58; |
||||||
|
} |
||||||
|
.navbar-default .navbar-link { |
||||||
|
color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-link:hover { |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 767px) { |
||||||
|
.navbar-default .navbar-nav .open .dropdown-menu > li > a { |
||||||
|
color: #dadada; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { |
||||||
|
color: #ffffff; |
||||||
|
} |
||||||
|
.navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { |
||||||
|
color: #ffffff; |
||||||
|
background-color: #0a6c9d; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
span.logo-1 { |
||||||
|
font-weight: 700; |
||||||
|
color: #53ad2d; |
||||||
|
} |
||||||
|
|
||||||
|
span.logo-2 { |
||||||
|
font-weight: 500; |
||||||
|
font-size:smaller; |
||||||
|
color: #FFF; |
||||||
|
} |
||||||
|
|
||||||
|
span.logo-3 { |
||||||
|
color: #FFF; |
||||||
|
font-weight: 100; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-collapse { |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 200; |
||||||
|
background-color: rgb(0, 12, 34); |
||||||
|
} |
||||||
|
|
||||||
|
.note { |
||||||
|
margin: 0 0 20px 0; |
||||||
|
padding: 15px 30px 15px 15px; |
||||||
|
border-left: 5px solid #eee; |
||||||
|
border-radius: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
.note-info { |
||||||
|
background-color: #E8F6FC; |
||||||
|
border-color: #57b5e3; |
||||||
|
color: #333333; |
||||||
|
} |
||||||
|
|
||||||
|
.note-danger { |
||||||
|
background-color: #ff9999; |
||||||
|
border-color: #ff0000; |
||||||
|
} |
||||||
|
|
||||||
|
h4.note { |
||||||
|
margin-top: 0; |
||||||
|
font-weight: 300 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.hash { |
||||||
|
font-family: 'Inconsolata', monospace; |
||||||
|
} |
||||||
|
|
||||||
|
/* Stats */ |
||||||
|
|
||||||
|
.stats-box { |
||||||
|
padding: 24px 30px; |
||||||
|
background: #0e102f; |
||||||
|
border-radius: 15px; |
||||||
|
margin-bottom: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.stats-box > h3 > i { |
||||||
|
width: 21px; |
||||||
|
} |
||||||
|
|
||||||
|
.stats-box > div > .fa { |
||||||
|
width: 25px; |
||||||
|
} |
||||||
|
.stats-box > div > span:first-of-type{ |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
.bg-dark { |
||||||
|
background-image: linear-gradient(to bottom, #0b0c22 0%, #0e122f 70%); |
||||||
|
} |
||||||
|
|
||||||
|
.worker-class.warning.offline-1 > td{ |
||||||
|
background-color: #ffd1d1; |
||||||
|
} |
||||||
|
|
||||||
|
.worker-class.warning.offline-2 > td{ |
||||||
|
background-color: #f99393; |
||||||
|
} |
||||||
|
|
||||||
|
.worker-class.warning.offline-3 > td{ |
||||||
|
background-color: #ff5959; |
||||||
|
color:#ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
.worker-class.warning.offline-4 > td{ |
||||||
|
background-color: #ff3e3e; |
||||||
|
color:#ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
.worker-class.warning.offline-5 > td{ |
||||||
|
background-color: #ff1e1e; |
||||||
|
color:#ffffff; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.jumbotron{ |
||||||
|
background: transparent; |
||||||
|
color:#bbbbbb; |
||||||
|
} |
||||||
|
|
||||||
|
.jumbotron small{ |
||||||
|
color:#919191; |
||||||
|
margin-left:30px; |
||||||
|
} |
||||||
|
|
||||||
|
.alert{ |
||||||
|
border-radius:20px; |
||||||
|
} |
||||||
|
.nav-tabs{ |
||||||
|
margin-bottom:20px; |
||||||
|
} |
||||||
|
.nav-tabs>li>a { |
||||||
|
border-radius: 15px 15px 0 0; |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
color: #5ea2ff; |
||||||
|
} |
||||||
|
|
||||||
|
.table-striped>tbody>tr:nth-of-type(odd) { |
||||||
|
background-color: rgba(0,0,0,0.5); |
||||||
|
} |
||||||
|
.table { |
||||||
|
margin-bottom: 0px; |
||||||
|
} |
||||||
|
.table-responsive { |
||||||
|
color: #444; |
||||||
|
background-color: rgba(255,255,255,0.9); |
||||||
|
padding: 5px 5px 0px; |
||||||
|
border-radius: 20px 20px 0px 0px; |
||||||
|
margin-bottom:20px; |
||||||
|
} |
||||||
|
.table-responsive a { |
||||||
|
color: #004bb1; |
||||||
|
} |
||||||
|
|
||||||
|
.table-striped>tbody>tr:nth-of-type(odd) { |
||||||
|
background-color: rgba(0,0,0,.2); |
||||||
|
} |
||||||
|
|
||||||
|
.highcharts-wrapper{ |
||||||
|
margin-bottom:20px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.command_lines{ |
||||||
|
margin: 15px 0; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines ul{ |
||||||
|
margin-bottom:0px; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines h5{ |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
.command_lines .tab-content { |
||||||
|
background-color: #E8F6FC; |
||||||
|
border-left: 5px solid #eee; |
||||||
|
border-color: #57b5e3; |
||||||
|
color: #ddd; |
||||||
|
border-radius: 0 0 15px 15px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.command_lines .nav-tabs { |
||||||
|
border-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines .nav-tabs>li.active>a, |
||||||
|
.command_lines .nav-tabs>li.active>a:focus, |
||||||
|
.command_lines .nav-tabs>li.active>a:hover { |
||||||
|
border-left: 5px solid #eee; |
||||||
|
border-color: #57b5e3; |
||||||
|
border-bottom: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.command_lines .panel-group .panel+.panel { |
||||||
|
margin-top: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines .panel-group .panel { |
||||||
|
margin-bottom: 0; |
||||||
|
border-radius: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines .panel-default>.panel-heading { |
||||||
|
color: #dddddd; |
||||||
|
background-color: #0e102f; |
||||||
|
border: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.command_lines .panel-default { |
||||||
|
border: 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.join_telegram{ |
||||||
|
border-radius: 5px; |
||||||
|
font-size: x-large; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.footer { background: #020207;} |
||||||
|
.footer-section { text-align: center; } |
||||||
|
.footer-title { margin-bottom: 40px; } |
||||||
|
.footer > .container { |
||||||
|
padding-right: 15px; |
||||||
|
padding-left: 15px; |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
<div class="container"> |
||||||
|
<div class="page-header"> |
||||||
|
<h1>About</h1> |
||||||
|
</div> |
||||||
|
<h3>Terms of Service</h3> |
||||||
|
<p>By using the pool you accept all possible risks related to experimental software usage.<br/> |
||||||
|
Pool owner can't compensate any irreversible losses, but will do his best to prevent worst case. |
||||||
|
</p> |
||||||
|
<h3>Details</h3> |
||||||
|
<p> |
||||||
|
<ul> |
||||||
|
<li>Written in Go it's a rocket highly concurrent and low RAM consuming piece of code</li> |
||||||
|
<li>High performance proxy</li> |
||||||
|
<li>Payouts and block unlocking module</li> |
||||||
|
<li>Designed for 100% distributed setup</li> |
||||||
|
<li>Strict policy module</li> |
||||||
|
<li>Beautiful modern Ember.js frontend</li> |
||||||
|
</ul> |
||||||
|
</p> |
||||||
|
</div> |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
{{outlet 'error' }} |
||||||
|
<div class="jumbotron"> |
||||||
|
<div class="container"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-4 stats"> |
||||||
|
<div class="stats-box"> |
||||||
|
<div style="display: block;"> |
||||||
|
<i class="fa fa-cloud"></i> {{t "account.immature.balance"}}: <span>{{format-balance model.stats.immature}} {{config.Unit}}</span><br> |
||||||
|
<small>{{t "account.immature.description"}}</small> |
||||||
|
</div> |
||||||
|
<div style="display: block;"> |
||||||
|
<i class="fa fa-bank"></i> {{t "account.pending.balance"}}: <span>{{format-balance model.stats.balance}} {{config.Unit}}</span><br> |
||||||
|
<small>{{t "account.pending.description"}}</small> |
||||||
|
</div> |
||||||
|
{{#if model.stats.pending}} |
||||||
|
<div style="display: block;"> |
||||||
|
<i class="fa fa-clock-o"></i> {{t "account.current"}}: <span>{{format-balance model.stats.pending}} {{config.Unit}}</span><br> |
||||||
|
</div> |
||||||
|
{{/if}} |
||||||
|
<div style="display: block;"><i class="fa fa-btc"></i> {{t "account.total.paid"}}: <span>{{format-balance model.stats.paid}} {{config.Unit}}</span></div> |
||||||
|
<div style="display: block;"><i class="fa fa-money"></i> Last 24h Reward: <span>{{format-balance model.24hreward}}</span></div> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="col-md-4 stats"> |
||||||
|
<div class="stats-box"> |
||||||
|
|
||||||
|
{{#if model.stats.lastShare}} |
||||||
|
<div style="display: block;"><i class="fa fa-hourglass-half"></i> |
||||||
|
{{t "account.last_share_submitted"}}: <span>{{format-relative (seconds-to-ms (string-to-int model.stats.lastShare))}}</span> |
||||||
|
</div> |
||||||
|
{{/if}} |
||||||
|
<div style="display: block;"><i class="fa fa-cogs"></i> {{t "account.online"}}: <span>{{format-number model.workersOnline}}</span></div> |
||||||
|
<div style="display: block;"><i class="fa fa-tachometer"></i> {{t "account.hashrate"}} ({{t "account.short_average_abbrv"}}): <span>{{format-hashrate model.currentHashrate}}</span></div> |
||||||
|
<div style="display: block;"><i class="fa fa-tachometer"></i> {{t "account.hashrate"}} ({{t "account.long_average_abbrv"}}): <span>{{format-hashrate model.hashrate}}</span></div> |
||||||
|
<div style="display: block;"><i class="fa fa-calculator"></i> {{t "account.earnings.miner"}}: <span>{{format-number earnPerDay}} {{config.Unit}}</span></div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-4 stats"> |
||||||
|
<div class="stats-box"> |
||||||
|
|
||||||
|
<div style="display: block;"><i class="fa fa-tachometer"></i> {{t "account.blocks.found"}}: <span>{{format-number model.stats.blocksFound fallback='0'}}</span></div> |
||||||
|
<div style="display: block;"><i class="fa fa-paper-plane-o"></i> {{t "account.total.payments"}}: <span>{{format-number model.paymentsTotal}}</span></div> |
||||||
|
<div style="display: block;"> |
||||||
|
<i class="fa fa-percent"></i> {{t "account.round_share"}}: <span>{{format-number roundPercent style='percent' maximumFractionDigits='6'}}</span><br/> |
||||||
|
<small>{{t "account.round_share_description"}}</small> |
||||||
|
</div> |
||||||
|
<div style="display: block;"> |
||||||
|
<i class="fa fa-hourglass"></i> |
||||||
|
{{t "account.epoch_switch"}}: <span>{{format-relative applicationController.nextEpoch units="hour"}}</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="container"> |
||||||
|
|
||||||
|
<ul class="nav nav-tabs"> |
||||||
|
{{#active-li currentWhen='account.index' role='presentation'}} |
||||||
|
{{#link-to 'account.index'}}{{t "account.workers"}} <span class="badge alert-danger">{{model.workersOffline}}</span>{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='account.rewards' role='presentation'}} |
||||||
|
{{#link-to 'account.rewards'}}Rewards{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
|
||||||
|
{{#active-li currentWhen='account.payouts' role='presentation'}} |
||||||
|
{{#link-to 'account.payouts'}}{{t "account.payouts"}}{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{outlet}} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
<div class="container"> |
||||||
|
{{high-charts mode=chartMode chartOptions=chartOptions content=chartData}} |
||||||
|
{{#if model.workers}} |
||||||
|
<h4>{{t "account.your_workers"}}</h4> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>ID</th> |
||||||
|
<th>{{t "account.hashrate"}} ({{t "account.short_average_abbrv"}} / {{t "account.short_average_abbrv"}})</th> |
||||||
|
<th>{{t "account.earnings.worker"}}</th> |
||||||
|
<th>{{t "account.difficulty"}}</th> |
||||||
|
<th>{{t "account.server"}}</th> |
||||||
|
<th>{{t "account.last_share"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each-in model.workers as |k v|}} |
||||||
|
<tr class="worker-class {{if v.offline "warning" "success"}} {{worker-colorizer v.lastBeat}}"> |
||||||
|
<td>{{k}}</td> |
||||||
|
<td>{{format-hashrate v.hr}} / {{format-hashrate v.hr2}}</td> |
||||||
|
<td>{{format-number (worker-earnperday v.hr netstats.hashrate)}} {{config.Unit}}</td> |
||||||
|
<td>{{format-difficulty v.difficulty}}</td> |
||||||
|
<td>{{v.hostname}}</td> |
||||||
|
<td>{{format-relative (seconds-to-ms v.lastBeat)}}</td> |
||||||
|
</tr> |
||||||
|
{{/each-in}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "account.no_workers_online"}}</h3> |
||||||
|
{{/if}} |
||||||
|
<div class="alert alert-info" role="alert"> |
||||||
|
<span class="sr-only">{{t "account.notice"}}:</span> |
||||||
|
{{format-html-message "account.notice_html"}} |
||||||
|
</div> |
||||||
|
<div class="alert alert-info" role="alert"> |
||||||
|
<strong>{{t "account.json_api_url"}}:</strong> <a href="{{config.ApiUrl}}api/accounts/{{model.login}}">/api/accounts/{{model.login}}</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
<div class="container"> |
||||||
|
{{high-charts mode=chartMode chartOptions=chartPayment content=chartData}} |
||||||
|
{{#if model.payments}} |
||||||
|
<h3>{{t "payout.latest_payouts"}}</h3> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "payout.time"}}</th> |
||||||
|
<th>{{t "payout.txid"}}</th> |
||||||
|
<th>{{t "payout.amount"}} {{config.Unit}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.payments as |tx|}} |
||||||
|
<tr> |
||||||
|
<td>{{format-date-locale tx.timestamp}}</td> |
||||||
|
<td> |
||||||
|
<a href="{{t "links.blockExplorerLink_tx"}}{{tx.tx}}" class="hash" rel="nofollow" target="_blank">{{tx.tx}}</a> |
||||||
|
</td> |
||||||
|
<td>{{format-balance tx.amount}}</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "payout.no_payouts_yet"}}</h3> |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
<div class="container"> |
||||||
|
{{#if model.rewards}} |
||||||
|
<h4>Your Latest Rewards</h4> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
{{#each model.sumrewards as |sumreward|}} |
||||||
|
<tr> |
||||||
|
<td>{{sumreward.name}}</td> |
||||||
|
<td>{{format-balance sumreward.reward}}</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</table> |
||||||
|
<span class="label label-default">Immature</span> <span class="label label-success">Matured</span> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Block Height</th> |
||||||
|
<th>Reward</th> |
||||||
|
<th>Round Share</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.rewards as |tx|}} |
||||||
|
<tr> |
||||||
|
<td>{{format-number tx.blockheight}}</td> |
||||||
|
<td> |
||||||
|
{{#if tx.immature}} |
||||||
|
<span class="label label-default">{{format-balance tx.reward}}</span> |
||||||
|
{{else}} |
||||||
|
<span class="label label-success">{{format-balance tx.reward}}</span> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
<td>{{format-number tx.percent style='percent' maximumFractionDigits='6'}}</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
{{else}} |
||||||
|
<h3>No rewards yet</h3> |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<div class="container"> |
||||||
|
<div class="page-header"> |
||||||
|
<h1>{{t "error.heading"}}</h1> |
||||||
|
<p>{{t "error.message"}}</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
<!-- Fixed navbar --> |
||||||
|
<!-- Modal --> |
||||||
|
{{!-- <div class="modal fade" id="coin_calculator" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> |
||||||
|
<h4 class="modal-title" id="myModalLabel">{{t "calculator.title"}}</h4> |
||||||
|
</div> |
||||||
|
<div class="modal-body text-center"> |
||||||
|
<iframe src=https://www.coincalculators.io/widget.aspx?crypto=perkle height ="270" width="325" frameBorder="0" style="overflow:hidden;" scrolling="no" ></iframe> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button type="button" class="btn btn-danger btn-block" data-dismiss="modal">{{t "calculator.close"}}</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> --}} |
||||||
|
|
||||||
|
<div class="navbar navbar-default navbar-fixed-top" role="navigation"> |
||||||
|
<div class="container"> |
||||||
|
<div class="navbar-header"> |
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> |
||||||
|
<span class="sr-only">Toggle navigation</span> |
||||||
|
<span class="icon-bar"></span> |
||||||
|
<span class="icon-bar"></span> |
||||||
|
<span class="icon-bar"></span> |
||||||
|
</button> |
||||||
|
<a class="navbar-brand" href="{{config.HttpHost}}"><span class="logo-1">{{config.PoolName}}</span><span class="logo-2"> @ {{config.CompanyName}}</span></a> |
||||||
|
</div> |
||||||
|
<div class="collapse navbar-collapse"> |
||||||
|
<ul class="nav navbar-nav text-center pull-right"> |
||||||
|
{{!-- <li> |
||||||
|
<a href="#" data-toggle="modal" data-target="#coin_calculator"> <i class="fa fa-calculator"></i> {{t "menu.calculator"}}</a> |
||||||
|
</li> --}} |
||||||
|
{{#active-li currentWhen='blocks'}} |
||||||
|
{{#link-to 'blocks'}} |
||||||
|
<i class="fa fa-cubes"></i> {{t 'menu.pool_blocks'}} |
||||||
|
{{#if immatureTotal}} |
||||||
|
<span class="badge alert-success">{{immatureTotal}}</span> |
||||||
|
{{/if}} |
||||||
|
{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='payments'}} |
||||||
|
{{#link-to 'payments'}} |
||||||
|
<i class="fa fa-paper-plane-o"></i> {{t 'menu.payments'}} |
||||||
|
{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='miners'}} |
||||||
|
{{#link-to 'miners'}} |
||||||
|
<i class="fa fa-users"></i> {{t 'menu.miners'}} |
||||||
|
{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='about'}} |
||||||
|
{{#link-to (t 'menu.i18n.about')}} |
||||||
|
<i class="fa fa-comments"></i> {{t 'menu.about'}} |
||||||
|
{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='help'}} |
||||||
|
{{#link-to (t 'menu.i18n.help')}} |
||||||
|
<i class="fa fa-rocket"></i> {{t 'menu.help'}} |
||||||
|
{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
<li> |
||||||
|
<ul class="nav navbar-nav navbar-right"> |
||||||
|
<li> |
||||||
|
<a class="nav-item nav-link dropdown-toggle" data-toggle="dropdown" id="selectedLanguage"> |
||||||
|
{{selectedLanguage}} |
||||||
|
<b class="caret"></b> |
||||||
|
</a> |
||||||
|
<ul class="dropdown-menu dropdown-menu-right"> |
||||||
|
{{#each languages as |lang|}} |
||||||
|
<li><a href="#" {{action 'selectLanguage' lang.value on='click'}}>{{lang.name}}</a></li> |
||||||
|
{{/each}} |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div dir="{{t "general.direction"}}"> |
||||||
|
|
||||||
|
|
||||||
|
{{outlet}} |
||||||
|
|
||||||
|
</div> |
||||||
|
<footer class="footer"> |
||||||
|
<div class="container"> |
||||||
|
<p class="text-muted text-center">{{format-html-message "footer.copyrights"}}</p> |
||||||
|
</div> |
||||||
|
</footer> |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<div class="jumbotron"> |
||||||
|
<div class="container"> |
||||||
|
<p class="lead">{{t "block.pool_rewards"}}</p> |
||||||
|
<span> |
||||||
|
{{format-html-message "block.pool_notice.html" success=520}} |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="container"> |
||||||
|
{{#if model.luck}} |
||||||
|
{{partial "luck"}} |
||||||
|
{{/if}} |
||||||
|
<ul class="nav nav-tabs"> |
||||||
|
{{#active-li currentWhen='blocks.index' role='presentation'}} |
||||||
|
{{#link-to 'blocks.index'}}{{t "block.blocks"}} <span class="badge alert-success">{{format-number model.maturedTotal}}</span>{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='blocks.immature' role='presentation'}} |
||||||
|
{{#link-to 'blocks.immature'}}{{t "block.immature"}} <span class="badge alert-success">{{format-number model.immatureTotal}}</span>{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
{{#active-li currentWhen='blocks.pending' role='presentation'}} |
||||||
|
{{#link-to 'blocks.pending'}}{{t "block.new"}} <span class="badge alert-info">{{format-number model.candidatesTotal}}</span>{{/link-to}} |
||||||
|
{{/active-li}} |
||||||
|
</ul> |
||||||
|
{{outlet}} |
||||||
|
</div> |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
<tr> |
||||||
|
<td> |
||||||
|
{{#if block.uncle}} |
||||||
|
<a href="{{t "links.blockExplorerLink_uncle"}}{{block.height}}" rel="nofollow" target="_blank">{{format-number block.height}}</a> |
||||||
|
{{else}} |
||||||
|
<a href="{{t "links.blockExplorerLink_block"}}{{block.height}}" rel="nofollow" target="_blank">{{format-number block.height}}</a> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
{{#if block.uncle}} |
||||||
|
<a href="{{t "links.blockExplorerLink_uncle"}}{{block.hash}}" class="hash" rel="nofollow" target="_blank">{{block.hash}}</a> |
||||||
|
{{else if block.orphan}} |
||||||
|
<span class="label label-danger">{{t "block.orphan"}}</span> |
||||||
|
{{else}} |
||||||
|
<a href="{{t "links.blockExplorerLink_block"}}{{block.hash}}" class="hash" rel="nofollow" target="_blank">{{block.hash}}</a> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
<td>{{format-date-locale block.timestamp}}</td> |
||||||
|
<td> |
||||||
|
{{#if block.isLucky}} |
||||||
|
<span class="label label-success">{{format-number block.variance style='percent'}}</span> |
||||||
|
{{else}} |
||||||
|
<span class="label label-info">{{format-number block.variance style='percent'}}</span> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
{{#if block.uncle}} |
||||||
|
<span class="label label-default">{{block.formatReward}}</span> |
||||||
|
{{else if block.isOk}} |
||||||
|
<span class="label label-primary">{{block.formatReward}}</span> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
</tr> |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
{{#if model.immature}} |
||||||
|
<h3>{{t "block.immature_blocks"}}</h3> |
||||||
|
|
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "block.height"}}</th> |
||||||
|
<th>{{t "block.hash"}}</th> |
||||||
|
<th>{{t "block.time_found"}}</th> |
||||||
|
<th>{{t "block.variance"}}</th> |
||||||
|
<th>{{t "block.reward"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.immature as |block|}} |
||||||
|
{{partial "blocks/block"}} |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "block.no_immature_blocks_yet"}}</h3> |
||||||
|
{{/if}} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
{{#if model.matured}} |
||||||
|
<h3>{{t "block.matured"}}</h3> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "block.height"}}</th> |
||||||
|
<th>{{t "block.hash"}}</th> |
||||||
|
<th>{{t "block.time_found"}}</th> |
||||||
|
<th>{{t "block.variance"}}</th> |
||||||
|
<th>{{t "block.reward"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.matured as |block|}} |
||||||
|
{{partial "blocks/block"}} |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "block.no_matured_blocks_yet"}}</h3> |
||||||
|
{{/if}} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
{{#if model.candidates}} |
||||||
|
<h3>{{t "block.recently_found_blocks"}}</h3> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "block.height"}}</th> |
||||||
|
<th>{{t "block.time_found"}}</th> |
||||||
|
<th>{{t "block.variance"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.candidates as |block|}} |
||||||
|
<tr> |
||||||
|
<td><a href="{{t "links.blockExplorerLink_block"}}{{block.height}}" rel="nofollow" target="_blank">{{format-number block.height}}</a></td> |
||||||
|
<td>{{format-date-locale block.timestamp}}</td> |
||||||
|
<td> |
||||||
|
{{#if block.isLucky}} |
||||||
|
<span class="label label-success">{{format-number block.variance style='percent'}}</span> |
||||||
|
{{else}} |
||||||
|
<span class="label label-info">{{format-number block.variance style='percent'}}</span> |
||||||
|
{{/if}} |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "block.no_new_blocks_yet"}}</h3> |
||||||
|
{{/if}} |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
<div class="container"> |
||||||
|
<div class="page-header"> |
||||||
|
<h2>Getting Started with {{config.PoolName}} mining</h2> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="stats-box"> |
||||||
|
<h3>GPU Miner Softwares</h3> |
||||||
|
|
||||||
|
<p>Download one of the following GPU miners. |
||||||
|
<ul> |
||||||
|
<li>Claymore Dual Miner (AMD/NVIDIA) : <a href="https://bitcointalk.org/index.php?topic=1433925.0">[ANN] Bitcointalk.org</a></li> |
||||||
|
<li>Ethminer (AMD/NVIDIA): <a href="https://github.com/ethereum-mining/ethminer/releasesi/latest">Ethminer source/binary at Github</a></li> |
||||||
|
</ul> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="stats-box"> |
||||||
|
<h3>Prepare Wallet Address</h3> |
||||||
|
<div> |
||||||
|
<h4>Using geth</h4> |
||||||
|
<ul> |
||||||
|
<li>Unix/Linux shell <code>$ geth account new</code></li> |
||||||
|
<li>Windows command line using cmd.exe (command shell) <code>> geth account new</code></li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<h4>Using online wallet</h4> |
||||||
|
<ul> |
||||||
|
<li>{{format-html-message "wallet.online_html"}}</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<h4>Wallet Dapp (aka. Mist)</h4> |
||||||
|
<ul> |
||||||
|
<li>Wallet dapp: {{format-html-message "wallet.dapp_html"}}</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Usage examples</h3> |
||||||
|
<dl class="dl-horizontal"> |
||||||
|
<dt><em>0x0000000000000000000000000000000000000000</em></dt> |
||||||
|
<dd>This is your address for payouts<br/> |
||||||
|
<strong>Example:</strong> <code>0x8b92c50e1c39466f900a578edb20a49356c4fe24</code>. |
||||||
|
</dd> |
||||||
|
<dt><em>your-worker-1</em><br /><em>your-worker-2</em></dt> |
||||||
|
<dd> |
||||||
|
ID of your PC/mining-rig to distinguish it from your other rigs. If you have just one rig, feel free to omit this param.<br /> |
||||||
|
This param must be short alphanumeric string with optional dashes and underscores.<br/> |
||||||
|
<strong>Example:</strong> <code>worker-1</code> |
||||||
|
</dd> |
||||||
|
</dl> |
||||||
|
|
||||||
|
<h4>Claymore Example</h4> |
||||||
|
<p><strong>Download Claymore Dual Miner (AMD/NVIDIA)</strong>: <a href="https://bitcointalk.org/index.php?topic=1433925.0">[ANN] Bitcointalk.org</a></p> |
||||||
|
<p> |
||||||
|
<code><em>EthDcrMiner64</em> -epool <em>{{config.StratumHost}}</em>:<em>{{config.StratumPort}}</em> <em>-esm 0</em> -ewal <em>0x0000000000000000000000000000000000000000</em> -eworker <em>your-worker-1</em> -allcoins 1 -allpools 1</code> |
||||||
|
</p> |
||||||
|
<ul> |
||||||
|
<li><code><em>EthDcrMiner64</em></code> - executable name under Windows. use <code><em>./ethdcrminer64</em></code> under Linux/Ubuntu</li> |
||||||
|
<li><code><em>{{config.StratumHost}}</em></code> - Stratum Server name</li> |
||||||
|
<li>use <code><em>-esm 0</em></code> and <code><em>{{config.StratumPort}}</em></code> port number for Stratum Server.</li> |
||||||
|
<li><code><em>0x0000000000000000000000000000000000000000</em></code> - Your wallet address</li> |
||||||
|
<li><code><em>your-worker-1</em></code> - Your worker name</li> |
||||||
|
</ul> |
||||||
|
|
||||||
|
<h4>Ethminer Examples</h4> |
||||||
|
<p> |
||||||
|
<strong>Download Ethminer (AMD/NVIDIA)</strong>: <a href="https://github.com/ethereum-mining/ethminer/releasesi/latest">Ethminer source/binary at Github</a> |
||||||
|
</p> |
||||||
|
<h5>Stratum method</h5> |
||||||
|
<p> |
||||||
|
<code>ethminer -SP 1 <em>-U</em> -S <em>{{config.StratumHost}}</em>:<em>{{config.StratumPort}}</em> -O <em>0x0000000000000000000000000000000000000000</em>.<em>your-worker-1</em> <em>--farm-recheck 2000</em></code> |
||||||
|
</p> |
||||||
|
<ul> |
||||||
|
<li><code>-SP 1</code> - option for Stratum server</li> |
||||||
|
<li><code><em>-U</em></code> - NVIDIA GPU or <code><em>-G</em></code> for AMD GPU</li> |
||||||
|
<li><code>-S <em>{{config.StratumHost}}</em>:<em>{{config.StratumPort}}</em></code> - stratum_server_name:stratum_port_number</li> |
||||||
|
<li><code>-O <em>0x0000000000000000000000000000000000000000</em>.<em>your-worker-1</em></code> - your_wallet_address.your_worker_name</li> |
||||||
|
<li><code><em>--farm-recheck 2000</em></code> - Leave 2000 ms between checks for changed work (default 500ms. use higher value to use stratum for stability)</li> |
||||||
|
</ul> |
||||||
|
<h5>Legacy HTTP method</h5> |
||||||
|
<p> |
||||||
|
<code>ethminer <em>-U</em> -F <em>{{config.HttpHost}}</em>:<em>{{config.HttpPort}}</em>/<em>0x0000000000000000000000000000000000000000</em>/<em>your-worker-1</em> <em>--farm-recheck 200</em></code> |
||||||
|
</p> |
||||||
|
<ul> |
||||||
|
<li><code><em>-U</em></code> - NVIDIA GPU or <code><em>-G</em></code> for AMD GPU</li> |
||||||
|
<li><code>-F <em>{{config.HttpHost}}</em>:<em>{{config.HttpPort}}</em>/<em>0x0000000000000000000000000000000000000000</em>/<em>your-worker-1</em></code> |
||||||
|
<ul><li>Legacy HTTP method. <em>http_server_name</em>:<em>http_port_number</em></li> |
||||||
|
<li><code><em>0x0000000000000000000000000000000000000000</em>/<em>your-worker-1</em></code> - <em>your_wallet_address</em>.<em>your_worker_name</em></li> |
||||||
|
</ul> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
@ -0,0 +1,172 @@ |
|||||||
|
<div class="jumbotron"> |
||||||
|
<div class="container"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-12"> |
||||||
|
<div class="alert alert-info text-center join_telegram"> |
||||||
|
<a href="https://t.me/poolnode" target="_blank"><i class="fab fa-telegram"></i> Telegram</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-4"> |
||||||
|
<strong>{{t "home.min_payout_threshold"}}: {{config.PayoutThreshold}} {{config.Unit}}</strong> / {{t "home.payouts_run" interval=config.PayoutInterval}}<br/> |
||||||
|
<span class="label label-success">PPLNS</span> {{t "home.payout_scheme_detail"}} |
||||||
|
</div> |
||||||
|
<div class="col-md-4 stats"> |
||||||
|
<div class="stats-box"> |
||||||
|
<div><i class="fa fa-users"></i> {{t "home.miners_online"}}: <span id="poolHashrate">{{format-number stats.model.minersTotal}}</span></div> |
||||||
|
<div><i class="fa fa-tachometer"></i> {{t "home.pool_hashrate"}}: <span id="poolHashrate">{{format-hashrate stats.model.hashrate}}</span></div> |
||||||
|
<div><i class="fa fa-money"></i> {{t "home.pool_fee"}}: <span id="poolFee" class="label label-success">{{config.PoolFee}}</span></div> |
||||||
|
{{#if stats.model.stats.lastBlockFound}} |
||||||
|
<div><i class="fa fa-clock-o"></i> {{t "home.last_block_found"}}: <span>{{format-relative (seconds-to-ms stats.model.stats.lastBlockFound)}}</span></div> |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-4 stats"> |
||||||
|
<div class="stats-box"> |
||||||
|
<div><i class="fa fa-unlock-alt"></i> {{t "home.network_difficulty"}}: <span>{{with-metric-prefix stats.difficulty}}</span></div> |
||||||
|
<div><i class="fa fa-tachometer"></i> {{t "home.network_hashrate"}}: <span>{{format-hashrate stats.hashrate}}</span></div> |
||||||
|
<div><i class="fa fa-bars"></i> {{t "home.blockchain_height"}}: <span>{{format-number stats.height}}</span></div> |
||||||
|
<div><i class="fa fa-clock-o"></i> {{t "home.current_round_variance"}}: <span>{{format-number stats.roundVariance style='percent'}}</span></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="container"> |
||||||
|
<div> |
||||||
|
{{high-charts mode=chartMode chartOptions=chartOptions content=chartData}} |
||||||
|
</div> |
||||||
|
<div class="stats"> |
||||||
|
<h3>{{t "home.query_history"}}</h3> |
||||||
|
<div class="input-group"> |
||||||
|
{{input value=cachedLogin class="form-control" placeholder=(t "home.input.enter_your_wallet_address")}} |
||||||
|
<span class="input-group-btn"> |
||||||
|
<button class="btn btn-primary" type="button" {{action 'lookup' cachedLogin}}> |
||||||
|
<span style="display: inline;"><i class="fa fa-search"></i> {{t "home.button.lookup"}}</span> |
||||||
|
</button> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<hr/> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-4 col-sm-12"> |
||||||
|
<h5 class="note note-info text-center"> |
||||||
|
<span class="label label-success">2b</span> <a href="./#/help" title={{t "home.settings.help"}}><b>{{t "home.settings.title" hashes="< 150"}}</b></a> |
||||||
|
<br> |
||||||
|
{{t "home.settings.difficulty" diff="2"}} |
||||||
|
|
||||||
|
<br><br> |
||||||
|
<b>{{t "home.settings.stratum_mining"}}:</b> |
||||||
|
<br><br> |
||||||
|
<code>stratum+tcp://{{config.StratumHost}}:8002</code> |
||||||
|
</h5> |
||||||
|
</div> |
||||||
|
<div class="col-md-4 col-sm-12"> |
||||||
|
<h5 class="note note-info text-center"> |
||||||
|
<span class="label label-success">4b</span> <a href="./#/help" title={{t "home.settings.help"}}><b>{{t "home.settings.title" hashes="150 - 800"}}</b></a> |
||||||
|
<br> |
||||||
|
{{t "home.settings.difficulty" diff="4"}} |
||||||
|
<br><br> |
||||||
|
<b>{{t "home.settings.stratum_mining"}}:</b> |
||||||
|
<br><br> |
||||||
|
<code>stratum+tcp://{{config.StratumHost}}:8004</code> |
||||||
|
</h5> |
||||||
|
|
||||||
|
</div> |
||||||
|
<div class="col-md-4 col-sm-12"> |
||||||
|
<h5 class="note note-info text-center"> |
||||||
|
<span class="label label-success">9b</span> <a href="./#/help" title={{t "home.settings.help"}}><b>{{t "home.settings.title" hashes="> 800"}} & NiceHash</b></a> |
||||||
|
<br> |
||||||
|
{{t "home.settings.difficulty" diff="9"}} |
||||||
|
<br><br> |
||||||
|
<b>{{t "home.settings.stratum_mining"}}:</b> |
||||||
|
<br><br> |
||||||
|
<code>stratum+tcp://{{config.StratumHost}}:8009</code> |
||||||
|
</h5> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row command_lines"> |
||||||
|
<!-- Nav tabs --> |
||||||
|
<ul class="nav nav-tabs" role="tablist"> |
||||||
|
<li role="presentation" class="active"><a href="#windows_settings" aria-controls="home" role="tab" data-toggle="tab">{{t "home.settings.commands.windows"}}</a></li> |
||||||
|
<li role="presentation"><a href="#smos_settings" aria-controls="profile" role="tab" data-toggle="tab">{{t "home.settings.commands.smos"}}</a></li> |
||||||
|
</ul> |
||||||
|
<!-- Tab panes --> |
||||||
|
<div class="tab-content"> |
||||||
|
<div role="tabpanel" class="tab-pane active" id="windows_settings"> |
||||||
|
<div style="padding:15px;"> |
||||||
|
<h5>{{t "home.settings.commands.windows_instruction"}}</h5> |
||||||
|
<div class="panel-group" id="windows_settings_windows_tab" role="tablist" aria-multiselectable="true"> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-heading" role="tab" id="win_headingOne"> |
||||||
|
<h4 class="panel-title"> |
||||||
|
<a role="button" data-toggle="collapse" data-parent="#windows_settings_windows_tab" href="#win_collapseOne" aria-expanded="true" aria-controls="win_collapseOne"> |
||||||
|
Claymore |
||||||
|
</a> |
||||||
|
</h4> |
||||||
|
</div> |
||||||
|
<div id="win_collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne"> |
||||||
|
<div class="panel-body"> |
||||||
|
<input type="text" class="form-control" value="EthDcrMiner64.exe -epool stratum+tcp://perkle-pool.esprezzo.io:8002 -esm 0 -ewal <address> -eworker <worker> -allcoins 1 -allpools 1"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-heading" role="tab" id="win_headingTwo"> |
||||||
|
<h4 class="panel-title"> |
||||||
|
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#windows_settings_windows_tab" href="#win_collapseTwo" aria-expanded="false" aria-controls="win_collapseTwo"> |
||||||
|
Ethminer 0.14 |
||||||
|
</a> |
||||||
|
</h4> |
||||||
|
</div> |
||||||
|
<div id="win_collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo"> |
||||||
|
<div class="panel-body"> |
||||||
|
<input type="text" class="form-control" value="ethminer.exe -U -SP 1 -S perkle-pool.esprezzo.io:8004 -O <address>.<worker> --farm-recheck 1000 -v 2"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div role="tabpanel" class="tab-pane" id="smos_settings"> |
||||||
|
<div style="padding:15px;"> |
||||||
|
<h5>{{t "home.settings.commands.windows_instruction"}}</h5> |
||||||
|
<div class="panel-group" id="smos_settings_smos_tab" role="tablist" aria-multiselectable="true"> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-heading" role="tab" id="smos_headingOne"> |
||||||
|
<h4 class="panel-title"> |
||||||
|
<a role="button" data-toggle="collapse" data-parent="#smos_settings_smos_tab" href="#smos_collapseOne" aria-expanded="true" aria-controls="smos_collapseOne"> |
||||||
|
Claymore |
||||||
|
</a> |
||||||
|
</h4> |
||||||
|
</div> |
||||||
|
<div id="smos_collapseOne" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingOne"> |
||||||
|
<div class="panel-body"> |
||||||
|
<input type="text" class="form-control" value="-wd 1 -r 1 -epool stratum+tcp://perkle-pool.esprezzo.io:8002 -ewal <address> -eworker $rigName -esm 0 -epsw x -allpools 1 -asm 1 -mode 1"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="panel panel-default"> |
||||||
|
<div class="panel-heading" role="tab" id="smos_headingTwo"> |
||||||
|
<h4 class="panel-title"> |
||||||
|
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#smos_settings_smos_tab" href="#smos_collapseTwo" aria-expanded="false" aria-controls="smos_collapseTwo"> |
||||||
|
Ethminer 0.14 |
||||||
|
</a> |
||||||
|
</h4> |
||||||
|
</div> |
||||||
|
<div id="smos_collapseTwo" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingTwo"> |
||||||
|
<div class="panel-body"> |
||||||
|
<input type="text" class="form-control" value="-U -P stratum1+tcp://<address>.$rigName@perkle-pool.esprezzo.io:8002 --farm-recheck 200 --api-port -3333"> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "luck.blocks"}}</th> |
||||||
|
<th>{{t "luck.shares_diff"}}</th> |
||||||
|
<th>{{t "luck.uncle_rate"}}</th> |
||||||
|
<th>{{t "luck.orphan_rate"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each-in model.luck as |total row|}} |
||||||
|
<tr> |
||||||
|
<td>{{total}}</td> |
||||||
|
<td>{{format-number row.luck style='percent'}}</td> |
||||||
|
<td>{{format-number row.uncleRate style='percent'}}</td> |
||||||
|
<td>{{format-number row.orphanRate style='percent'}}</td> |
||||||
|
</tr> |
||||||
|
{{/each-in}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
<div class="jumbotron"> |
||||||
|
<div class="container"> |
||||||
|
<p class="lead">{{t "miners.total_hashrate"}}: {{format-hashrate model.hashrate}}.</p> |
||||||
|
<strong>{{t "miners.total_miners"}}:</strong> <span class="label label-info">{{model.minersTotal}}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="container"> |
||||||
|
{{#if model.miners}} |
||||||
|
<h3>{{t "miners.miners"}}</h3> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "miners.login"}}</th> |
||||||
|
<th>{{t "miners.hashrate"}}</th> |
||||||
|
<th>{{t "miners.last_beat"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.miners as |m|}} |
||||||
|
<tr class="{{if m.offline "warning"}}"> |
||||||
|
<td>{{#link-to 'account' m.login class='hash'}}{{m.login}}{{/link-to}}</td> |
||||||
|
<td>{{format-hashrate m.hr}}</td> |
||||||
|
<td>{{format-date-locale m.lastBeat}}</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "miners.no_miners"}}</h3> |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<div class="container"> |
||||||
|
<div class="page-header"> |
||||||
|
<h1>No Account Data Available</h1> |
||||||
|
<p>If you are looking for your account stats, you need to submit at least a single share.</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<div class="jumbotron"> |
||||||
|
<div class="container"> |
||||||
|
<p class="lead">{{t "payments.pay_tx"}}</p> |
||||||
|
<strong>{{t "payments.total_payments_sent"}}:</strong> <span class="label label-info">{{model.paymentsTotal}}</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="container"> |
||||||
|
{{#if model.payments}} |
||||||
|
<h3>{{t "payments.latest_payouts"}}</h3> |
||||||
|
<div class="table-responsive"> |
||||||
|
<table class="table table-condensed table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>{{t "payments.time"}}</th> |
||||||
|
<th>{{t "payments.amount"}}</th> |
||||||
|
<th>{{t "payments.address"}}</th> |
||||||
|
<th>{{t "payments.txid"}}</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
{{#each model.payments as |tx|}} |
||||||
|
<tr> |
||||||
|
<td>{{format-date-locale tx.timestamp}}</td> |
||||||
|
<td>{{format-number tx.formatAmount}}</td> |
||||||
|
<td> |
||||||
|
<a href="{{t "links.blockExplorerLink_address"}}{{tx.address}}" class="hash" rel="nofollow" target="_blank">{{tx.address}}</a> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<a href="{{t "links.blockExplorerLink_tx"}}{{tx.tx}}" class="hash" rel="nofollow" target="_blank">{{format-tx tx.tx}}</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
{{/each}} |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
{{else}} |
||||||
|
<h3>{{t "payments.no_payouts_yet"}}</h3> |
||||||
|
{{/if}} |
||||||
|
</div> |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue