commit f8afc172c6437eb578f248de0f7febde59f817cb Author: nolash Date: Mon Jun 28 07:48:36 2021 +0200 Initial commit; receive all eth code from chainlib package diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c556a5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +gmon.out +*.pyc +dist/ +build/ +*.egg-info diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..eda22ae --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +- 0.0.5-pending + * Receive all ethereum components from chainlib package diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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 + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..829a7e6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include *requirements.txt LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d51634 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# chainlib-eth + +Ethereum implementation of the chainlib blockchain interface tooling + +See https://gitlab.com/chaintool/chainlib for more information diff --git a/chainlib/eth/address.py b/chainlib/eth/address.py new file mode 100644 index 0000000..ea8818b --- /dev/null +++ b/chainlib/eth/address.py @@ -0,0 +1,13 @@ +# third-party imports +import sha3 +from hexathon import ( + strip_0x, + uniform, + ) +from crypto_dev_signer.encoding import ( + is_address, + is_checksum_address, + to_checksum_address, + ) + +to_checksum = to_checksum_address diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py new file mode 100644 index 0000000..e02e5fa --- /dev/null +++ b/chainlib/eth/block.py @@ -0,0 +1,79 @@ +# third-party imports +from chainlib.jsonrpc import JSONRPCRequest +from chainlib.eth.tx import Tx +from hexathon import ( + add_0x, + strip_0x, + even, + ) + + +def block_latest(id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_blockNumber' + return j.finalize(o) + + +def block_by_hash(hsh, include_tx=True, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getBlockByHash' + o['params'].append(hsh) + o['params'].append(include_tx) + return j.finalize(o) + + +def block_by_number(n, include_tx=True, id_generator=None): + nhx = add_0x(even(hex(n)[2:])) + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getBlockByNumber' + o['params'].append(nhx) + o['params'].append(include_tx) + return j.finalize(o) + + +def transaction_count(block_hash, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getBlockTransactionCountByHash' + o['params'].append(block_hash) + return j.finalize(o) + + +class Block: + + def __init__(self, src): + self.hash = src['hash'] + try: + self.number = int(strip_0x(src['number']), 16) + except TypeError: + self.number = int(src['number']) + self.txs = src['transactions'] + self.block_src = src + try: + self.timestamp = int(strip_0x(src['timestamp']), 16) + except TypeError: + self.timestamp = int(src['timestamp']) + + + def src(self): + return self.block_src + + + def tx(self, i): + return Tx(self.txs[i], self) + + + def tx_src(self, i): + return self.txs[i] + + + def __str__(self): + return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs)) + + + @staticmethod + def from_src(src): + return Block(src) diff --git a/chainlib/eth/chain.py b/chainlib/eth/chain.py new file mode 100644 index 0000000..c68f1c8 --- /dev/null +++ b/chainlib/eth/chain.py @@ -0,0 +1,8 @@ +from chainlib.jsonrpc import JSONRPCRequest + + +def network_id(id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'net_version' + return j.finalize(o) diff --git a/chainlib/eth/connection.py b/chainlib/eth/connection.py new file mode 100644 index 0000000..4bc284b --- /dev/null +++ b/chainlib/eth/connection.py @@ -0,0 +1,140 @@ +# standard imports +import copy +import logging +import json +import datetime +import time +import socket +from urllib.request import ( + Request, + urlopen, + ) + +# third-party imports +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from .error import ( + DefaultErrorParser, + RevertEthException, + ) +from .sign import ( + sign_transaction, + ) +from chainlib.connection import ( + ConnType, + RPCConnection, + JSONRPCHTTPConnection, + JSONRPCUnixConnection, + error_parser, + ) +from chainlib.jsonrpc import ( + JSONRPCRequest, + jsonrpc_result, + ) +from chainlib.eth.tx import ( + unpack, + ) + +logg = logging.getLogger(__name__) + + +class EthHTTPConnection(JSONRPCHTTPConnection): + + def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser, id_generator=None): + t = datetime.datetime.utcnow() + i = 0 + while True: + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] ='eth_getTransactionReceipt' + o['params'].append(add_0x(tx_hash_hex)) + o = j.finalize(o) + req = Request( + self.location, + method='POST', + ) + req.add_header('Content-Type', 'application/json') + data = json.dumps(o) + logg.debug('(HTTP) poll receipt attempt {} {}'.format(i, data)) + res = urlopen(req, data=data.encode('utf-8')) + r = json.load(res) + + e = jsonrpc_result(r, error_parser) + if e != None: + logg.debug('(HTTP) poll receipt completed {}'.format(r)) + logg.debug('e {}'.format(strip_0x(e['status']))) + if strip_0x(e['status']) == '00': + raise RevertEthException(tx_hash_hex) + return e + + if timeout > 0.0: + delta = (datetime.datetime.utcnow() - t) + datetime.timedelta(seconds=delay) + if delta.total_seconds() >= timeout: + raise TimeoutError(tx_hash) + + time.sleep(delay) + i += 1 + + + def check_rpc(self, id_generator=None): + j = JSONRPCRequest(id_generator) + req = j.template() + req['method'] = 'net_version' + req = j.finalize(req) + r = self.do(req) + + +class EthUnixConnection(JSONRPCUnixConnection): + + def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser): + raise NotImplementedError('Not yet implemented for unix socket') + + +def sign_transaction_to_rlp(chain_spec, doer, tx): + txs = tx.serialize() + logg.debug('serializing {}'.format(txs)) + # TODO: because some rpc servers may fail when chainId is included, we are forced to spend cpu here on this + chain_id = txs.get('chainId') or 1 + if chain_spec != None: + chain_id = chain_spec.chain_id() + txs['chainId'] = add_0x(chain_id.to_bytes(2, 'big').hex()) + txs['from'] = add_0x(tx.sender) + o = sign_transaction(txs) + r = doer(o) + logg.debug('sig got {}'.format(r)) + return bytes.fromhex(strip_0x(r)) + + +def sign_message(doer, msg): + o = sign_message(msg) + return doer(o) + + +class EthUnixSignerConnection(EthUnixConnection): + + def sign_transaction_to_rlp(self, tx): + return sign_transaction_to_rlp(self.chain_spec, self.do, tx) + + + def sign_message(self, tx): + return sign_message(self.do, tx) + + +class EthHTTPSignerConnection(EthHTTPConnection): + + def sign_transaction_to_rlp(self, tx): + return sign_transaction_to_rlp(self.chain_spec, self.do, tx) + + + def sign_message(self, tx): + return sign_message(self.do, tx) + + + +RPCConnection.register_constructor(ConnType.HTTP, EthHTTPConnection, tag='eth_default') +RPCConnection.register_constructor(ConnType.HTTP_SSL, EthHTTPConnection, tag='eth_default') +RPCConnection.register_constructor(ConnType.UNIX, EthUnixConnection, tag='eth_default') diff --git a/chainlib/eth/constant.py b/chainlib/eth/constant.py new file mode 100644 index 0000000..1e10a2a --- /dev/null +++ b/chainlib/eth/constant.py @@ -0,0 +1,5 @@ +ZERO_ADDRESS = '0x{:040x}'.format(0) +ZERO_CONTENT = '0x{:064x}'.format(0) +MINIMUM_FEE_UNITS = 21000 +MINIMUM_FEE_PRICE = 1000000000 +MAX_UINT = int('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16) diff --git a/chainlib/eth/contract.py b/chainlib/eth/contract.py new file mode 100644 index 0000000..a609334 --- /dev/null +++ b/chainlib/eth/contract.py @@ -0,0 +1,328 @@ +# standard imports +import enum +import re +import logging + +# external imports +from hexathon import ( + strip_0x, + pad, + ) + +# local imports +from chainlib.hash import keccak256_string_to_hex +from chainlib.block import BlockSpec +from chainlib.jsonrpc import JSONRPCRequest +from .address import to_checksum_address + +#logg = logging.getLogger(__name__) +logg = logging.getLogger() + + +re_method = r'^[a-zA-Z0-9_]+$' + +class ABIContractType(enum.Enum): + + BYTES32 = 'bytes32' + BYTES4 = 'bytes4' + UINT256 = 'uint256' + ADDRESS = 'address' + STRING = 'string' + BOOLEAN = 'bool' + +dynamic_contract_types = [ + ABIContractType.STRING, + ] + + +class ABIContract: + + def __init__(self): + self.types = [] + self.contents = [] + + +class ABIMethodEncoder(ABIContract): + + def __init__(self): + super(ABIMethodEncoder, self).__init__() + self.method_name = None + self.method_contents = [] + + + def method(self, m): + if re.match(re_method, m) == None: + raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method)) + self.method_name = m + self.__log_method() + + + def get_method(self): + if self.method_name == None: + return '' + return '{}({})'.format(self.method_name, ','.join(self.method_contents)) + + + def typ(self, v): + if self.method_name == None: + raise AttributeError('method name must be set before adding types') + if not isinstance(v, ABIContractType): + raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) + self.method_contents.append(v.value) + self.__log_method() + + + def __log_method(self): + logg.debug('method set to {}'.format(self.get_method())) + + + +class ABIContractDecoder: + + + def typ(self, v): + if not isinstance(v, ABIContractType): + raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) + self.types.append(v.value) + self.__log_typ() + + + def val(self, v): + self.contents.append(v) + logg.debug('content is now {}'.format(self.contents)) + + + def uint256(self, v): + return int(v, 16) + + + def bytes32(self, v): + return v + + + def bool(self, v): + return bool(self.uint256(v)) + + + def boolean(self, v): + return bool(self.uint256(v)) + + + def address(self, v): + a = strip_0x(v)[64-40:] + return to_checksum_address(a) + + + def string(self, v): + s = strip_0x(v) + b = bytes.fromhex(s) + cursor = 0 + offset = int.from_bytes(b[cursor:cursor+32], 'big') + cursor += 32 + length = int.from_bytes(b[cursor:cursor+32], 'big') + cursor += 32 + content = b[cursor:cursor+length] + logg.debug('parsing string offset {} length {} content {}'.format(offset, length, content)) + return content.decode('utf-8') + + + def __log_typ(self): + logg.debug('types set to ({})'.format(','.join(self.types))) + + + def decode(self): + r = [] + logg.debug('contents {}'.format(self.contents)) + for i in range(len(self.types)): + m = getattr(self, self.types[i]) + s = self.contents[i] + logg.debug('{} {} {} {} {}'.format(i, m, self.types[i], self.contents[i], s)) + r.append(m(s.hex())) + return r + + + def get(self): + return self.decode() + + + def __str__(self): + return self.decode() + + +class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder): + + def __init__(self): + super(ABIContractLogDecoder, self).__init__() + self.method_name = None + self.indexed_content = [] + + + def topic(self, event): + self.method(event) + + + def get_method_signature(self): + s = self.get_method() + return keccak256_string_to_hex(s) + + + def typ(self, v): + super(ABIContractLogDecoder, self).typ(v) + self.types.append(v.value) + + + def apply(self, topics, data): + t = self.get_method_signature() + if topics[0] != t: + raise ValueError('topic mismatch') + for i in range(len(topics) - 1): + self.contents.append(topics[i+1]) + self.contents += data + + +class ABIContractEncoder(ABIMethodEncoder): + + def __log_latest(self, v): + l = len(self.types) - 1 + logg.debug('Encoder added {} -> {} ({})'.format(v, self.contents[l], self.types[l].value)) + + + def uint256(self, v): + v = int(v) + b = v.to_bytes(32, 'big') + self.contents.append(b.hex()) + self.types.append(ABIContractType.UINT256) + self.__log_latest(v) + + + def bool(self, v): + return self.boolean(v) + + + def boolean(self, v): + if bool(v): + return self.uint256(1) + return self.uint256(0) + + + def address(self, v): + self.bytes_fixed(32, v, 20) + self.types.append(ABIContractType.ADDRESS) + self.__log_latest(v) + + + def bytes32(self, v): + self.bytes_fixed(32, v) + self.types.append(ABIContractType.BYTES32) + self.__log_latest(v) + + + def bytes4(self, v): + self.bytes_fixed(4, v) + self.types.append(ABIContractType.BYTES4) + self.__log_latest(v) + + + + def string(self, v): + b = v.encode('utf-8') + l = len(b) + contents = l.to_bytes(32, 'big') + contents += b + padlen = 32 - (l % 32) + contents += padlen * b'\x00' + self.bytes_fixed(len(contents), contents) + self.types.append(ABIContractType.STRING) + self.__log_latest(v) + return contents + + + def bytes_fixed(self, mx, v, exact=0): + typ = type(v).__name__ + if typ == 'str': + v = strip_0x(v) + l = len(v) + if exact > 0 and l != exact * 2: + raise ValueError('value wrong size; expected {}, got {})'.format(mx, l)) + if l > mx * 2: + raise ValueError('value too long ({})'.format(l)) + v = pad(v, mx) + elif typ == 'bytes': + l = len(v) + if exact > 0 and l != exact: + raise ValueError('value wrong size; expected {}, got {})'.format(mx, l)) + b = bytearray(mx) + b[mx-l:] = v + v = pad(b.hex(), mx) + else: + raise ValueError('invalid input {}'.format(typ)) + self.contents.append(v.ljust(64, '0')) + + + + def get_method_signature(self): + s = self.get_method() + if s == '': + return s + return keccak256_string_to_hex(s)[:8] + + + def get_contents(self): + direct_contents = '' + pointer_contents = '' + l = len(self.types) + pointer_cursor = 32 * l + for i in range(l): + if self.types[i] in dynamic_contract_types: + content_length = len(self.contents[i]) + pointer_contents += self.contents[i] + direct_contents += pointer_cursor.to_bytes(32, 'big').hex() + pointer_cursor += int(content_length / 2) + else: + direct_contents += self.contents[i] + s = ''.join(direct_contents + pointer_contents) + for i in range(0, len(s), 64): + l = len(s) - i + if l > 64: + l = 64 + logg.debug('code word {} {}'.format(int(i / 64), s[i:i+64])) + return s + + + def get(self): + return self.encode() + + + def encode(self): + m = self.get_method_signature() + c = self.get_contents() + return m + c + + + def __str__(self): + return self.encode() + + + +def abi_decode_single(typ, v): + d = ABIContractDecoder() + d.typ(typ) + d.val(v) + r = d.decode() + return r[0] + + +def code(address, block_spec=BlockSpec.LATEST, id_generator=None): + block_height = None + if block_spec == BlockSpec.LATEST: + block_height = 'latest' + elif block_spec == BlockSpec.PENDING: + block_height = 'pending' + else: + block_height = int(block_spec) + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getCode' + o['params'].append(address) + o['params'].append(block_height) + return j.finalize(o) diff --git a/chainlib/eth/error.py b/chainlib/eth/error.py new file mode 100644 index 0000000..50e8e51 --- /dev/null +++ b/chainlib/eth/error.py @@ -0,0 +1,23 @@ +# local imports +from chainlib.error import ExecutionError + +class EthException(Exception): + pass + + +class RevertEthException(EthException, ExecutionError): + pass + + +class NotFoundEthException(EthException): + pass + + +class RequestMismatchException(EthException): + pass + + +class DefaultErrorParser: + + def translate(self, error): + return EthException('default parser code {}'.format(error)) diff --git a/chainlib/eth/gas.py b/chainlib/eth/gas.py new file mode 100644 index 0000000..0b86858 --- /dev/null +++ b/chainlib/eth/gas.py @@ -0,0 +1,149 @@ +# standard imports +import logging + +# third-party imports +from hexathon import ( + add_0x, + strip_0x, + ) +from crypto_dev_signer.eth.transaction import EIP155Transaction + +# local imports +from chainlib.hash import keccak256_hex_to_hex +from chainlib.jsonrpc import JSONRPCRequest +from chainlib.eth.tx import ( + TxFactory, + TxFormat, + raw, + ) +from chainlib.eth.constant import ( + MINIMUM_FEE_UNITS, + ) + +logg = logging.getLogger(__name__) + + +def price(id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_gasPrice' + return j.finalize(o) + + +def balance(address, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getBalance' + o['params'].append(address) + o['params'].append('latest') + return j.finalize(o) + + +def parse_balance(balance): + try: + r = int(balance, 10) + except ValueError: + r = int(balance, 16) + return r + + +class Gas(TxFactory): + + def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC, id_generator=None): + tx = self.template(sender_address, recipient_address, use_nonce=True) + tx['value'] = value + txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) + tx_raw = self.signer.sign_transaction_to_rlp(txe) + tx_raw_hex = add_0x(tx_raw.hex()) + tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex)) + + o = None + if tx_format == TxFormat.JSONRPC: + o = raw(tx_raw_hex, id_generator=id_generator) + elif tx_format == TxFormat.RLP_SIGNED: + o = tx_raw_hex + + return (tx_hash_hex, o) + + + +class RPCGasOracle: + + def __init__(self, conn, code_callback=None, min_price=1, id_generator=None): + self.conn = conn + self.code_callback = code_callback + self.min_price = min_price + self.id_generator = id_generator + + + def get_gas(self, code=None): + gas_price = 0 + if self.conn != None: + o = price(id_generator=self.id_generator) + r = self.conn.do(o) + n = strip_0x(r) + gas_price = int(n, 16) + fee_units = MINIMUM_FEE_UNITS + if self.code_callback != None: + fee_units = self.code_callback(code) + if gas_price < self.min_price: + logg.debug('adjusting price {} to set minimum {}'.format(gas_price, self.min_price)) + gas_price = self.min_price + return (gas_price, fee_units) + + +class RPCPureGasOracle(RPCGasOracle): + + def __init__(self, conn, code_callback=None, id_generator=None): + super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator) + + +class OverrideGasOracle(RPCGasOracle): + + def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None): + self.conn = None + self.code_callback = None + self.limit = limit + self.price = price + + price_conn = None + + if self.limit == None or self.price == None: + if self.price == None: + price_conn = conn + logg.debug('override gas oracle with rpc fallback; price {} limit {}'.format(self.price, self.limit)) + + super(OverrideGasOracle, self).__init__(price_conn, code_callback, id_generator=id_generator) + + + def get_gas(self, code=None): + r = None + fee_units = None + fee_price = None + + rpc_results = super(OverrideGasOracle, self).get_gas(code) + + if self.limit != None: + fee_units = self.limit + if self.price != None: + fee_price = self.price + + if fee_price == None: + if rpc_results != None: + fee_price = rpc_results[0] + logg.debug('override gas oracle without explicit price, setting from rpc {}'.format(fee_price)) + else: + fee_price = MINIMUM_FEE_PRICE + logg.debug('override gas oracle without explicit price, setting default {}'.format(fee_price)) + if fee_units == None: + if rpc_results != None: + fee_units = rpc_results[1] + logg.debug('override gas oracle without explicit limit, setting from rpc {}'.format(fee_units)) + else: + fee_units = MINIMUM_FEE_UNITS + logg.debug('override gas oracle without explicit limit, setting default {}'.format(fee_units)) + + return (fee_price, fee_units) + + +DefaultGasOracle = RPCGasOracle diff --git a/chainlib/eth/jsonrpc.py b/chainlib/eth/jsonrpc.py new file mode 100644 index 0000000..03db2c4 --- /dev/null +++ b/chainlib/eth/jsonrpc.py @@ -0,0 +1,16 @@ +# proposed custom errors +# source: https://eth.wiki/json-rpc/json-rpc-error-codes-improvement-proposal + +#1 Unauthorized Should be used when some action is not authorized, e.g. sending from a locked account. +#2 Action not allowed Should be used when some action is not allowed, e.g. preventing an action, while another depending action is processing on, like sending again when a confirmation popup is shown to the user (?). +#3 Execution error Will contain a subset of custom errors in the data field. See below. + +#100 X doesn’t exist Should be used when something which should be there is not found. (Doesn’t apply to eth_getTransactionBy_ and eth_getBlock_. They return a success with value null) +#101 Requires ether Should be used for actions which require somethin else, e.g. gas or a value. +#102 Gas too low Should be used when a to low value of gas was given. +#103 Gas limit exceeded Should be used when a limit is exceeded, e.g. for the gas limit in a block. +#104 Rejected Should be used when an action was rejected, e.g. because of its content (too long contract code, containing wrong characters ?, should differ from -32602 - Invalid params). +#105 Ether too low Should be used when a to low value of Ether was given. + +#106 Timeout Should be used when an action timedout. +#107 Conflict Should be used when an action conflicts with another (ongoing?) action. diff --git a/chainlib/eth/log.py b/chainlib/eth/log.py new file mode 100644 index 0000000..919d8e0 --- /dev/null +++ b/chainlib/eth/log.py @@ -0,0 +1,24 @@ +# external imports +import sha3 + + +class LogBloom: + + def __init__(self): + self.content = bytearray(256) + + + def add(self, element): + if not isinstance(element, bytes): + raise ValueError('element must be bytes') + h = sha3.keccak_256() + h.update(element) + z = h.digest() + + for j in range(3): + c = j * 2 + v = int.from_bytes(z[c:c+2], byteorder='big') + v &= 0x07ff + m = 255 - int(v / 8) + n = v % 8 + self.content[m] |= (1 << n) diff --git a/chainlib/eth/nonce.py b/chainlib/eth/nonce.py new file mode 100644 index 0000000..34f17dc --- /dev/null +++ b/chainlib/eth/nonce.py @@ -0,0 +1,63 @@ +# third-party imports +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.jsonrpc import JSONRPCRequest + + +def nonce(address, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_getTransactionCount' + o['params'].append(address) + o['params'].append('pending') + return j.finalize(o) + + +class NonceOracle: + + def __init__(self, address, id_generator=None): + self.address = address + self.id_generator = id_generator + self.nonce = self.get_nonce() + + + def get_nonce(self): + raise NotImplementedError('Class must be extended') + + + def next_nonce(self): + n = self.nonce + self.nonce += 1 + return n + + +class RPCNonceOracle(NonceOracle): + + def __init__(self, address, conn, id_generator=None): + self.conn = conn + super(RPCNonceOracle, self).__init__(address, id_generator=id_generator) + + + def get_nonce(self): + o = nonce(self.address, id_generator=self.id_generator) + r = self.conn.do(o) + n = strip_0x(r) + return int(n, 16) + + +class OverrideNonceOracle(NonceOracle): + + def __init__(self, address, nonce): + self.nonce = nonce + super(OverrideNonceOracle, self).__init__(address) + + + def get_nonce(self): + return self.nonce + + +DefaultNonceOracle = RPCNonceOracle diff --git a/chainlib/eth/pytest/__init__.py b/chainlib/eth/pytest/__init__.py new file mode 100644 index 0000000..8388db6 --- /dev/null +++ b/chainlib/eth/pytest/__init__.py @@ -0,0 +1,3 @@ +from .fixtures_ethtester import * +from .fixtures_chain import * +from .fixtures_signer import * diff --git a/chainlib/eth/pytest/fixtures_chain.py b/chainlib/eth/pytest/fixtures_chain.py new file mode 100644 index 0000000..e223192 --- /dev/null +++ b/chainlib/eth/pytest/fixtures_chain.py @@ -0,0 +1,17 @@ +# external imports +import pytest + +# local imports +from chainlib.chain import ChainSpec + + +@pytest.fixture(scope='session') +def default_chain_spec(): + return ChainSpec('evm', 'foo', 42) + + +@pytest.fixture(scope='session') +def default_chain_config(): + return { + 'foo': 42, + } diff --git a/chainlib/eth/pytest/fixtures_ethtester.py b/chainlib/eth/pytest/fixtures_ethtester.py new file mode 100644 index 0000000..61b0b44 --- /dev/null +++ b/chainlib/eth/pytest/fixtures_ethtester.py @@ -0,0 +1,105 @@ +# standard imports +import os +import logging + +# external imports +import eth_tester +import pytest +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.keystore.dict import DictKeystore + +# local imports +from chainlib.eth.unittest.base import * +from chainlib.connection import ( + RPCConnection, + ConnType, + ) +from chainlib.eth.unittest.ethtester import create_tester_signer +from chainlib.eth.address import to_checksum_address + +logg = logging.getLogger() #__name__) + + +@pytest.fixture(scope='function') +def eth_keystore(): + return DictKeystore() + + +@pytest.fixture(scope='function') +def init_eth_tester( + eth_keystore, + ): + return create_tester_signer(eth_keystore) + + +@pytest.fixture(scope='function') +def call_sender( + eth_accounts, + ): + return eth_accounts[0] +# +# +#@pytest.fixture(scope='function') +#def eth_signer( +# init_eth_tester, +# ): +# return init_eth_tester + + +@pytest.fixture(scope='function') +def eth_rpc( + default_chain_spec, + init_eth_rpc, + ): + return RPCConnection.connect(default_chain_spec, 'default') + + +@pytest.fixture(scope='function') +def eth_accounts( + init_eth_tester, + ): + addresses = list(init_eth_tester.get_accounts()) + for address in addresses: + balance = init_eth_tester.get_balance(address) + logg.debug('prefilled account {} balance {}'.format(address, balance)) + return addresses + + +@pytest.fixture(scope='function') +def eth_empty_accounts( + eth_keystore, + init_eth_tester, + ): + a = [] + for i in range(10): + #address = init_eth_tester.new_account() + address = eth_keystore.new() + checksum_address = add_0x(to_checksum_address(address)) + a.append(checksum_address) + logg.info('added address {}'.format(checksum_address)) + return a + + +@pytest.fixture(scope='function') +def eth_signer( + eth_keystore, + ): + return EIP155Signer(eth_keystore) + + +@pytest.fixture(scope='function') +def init_eth_rpc( + default_chain_spec, + init_eth_tester, + eth_signer, + ): + + rpc_conn = TestRPCConnection(None, init_eth_tester, eth_signer) + def rpc_with_tester(url=None, chain_spec=default_chain_spec): + return rpc_conn + + RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='default') + RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='signer') + RPCConnection.register_location('custom', default_chain_spec, tag='default', exist_ok=True) + RPCConnection.register_location('custom', default_chain_spec, tag='signer', exist_ok=True) + return None diff --git a/chainlib/eth/pytest/fixtures_signer.py b/chainlib/eth/pytest/fixtures_signer.py new file mode 100644 index 0000000..484a52b --- /dev/null +++ b/chainlib/eth/pytest/fixtures_signer.py @@ -0,0 +1,18 @@ +# standard imports +#import os + +# external imports +import pytest +#from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer + + +@pytest.fixture(scope='function') +def agent_roles( + eth_accounts, + ): + return { + 'ALICE': eth_accounts[20], + 'BOB': eth_accounts[21], + 'CAROL': eth_accounts[23], + 'DAVE': eth_accounts[24], + } diff --git a/chainlib/eth/runnable/__init__.py b/chainlib/eth/runnable/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py new file mode 100644 index 0000000..461acf9 --- /dev/null +++ b/chainlib/eth/runnable/balance.py @@ -0,0 +1,102 @@ +#!python3 + +"""Token balance query script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import os +import json +import argparse +import logging + +# third-party imports +from hexathon import ( + add_0x, + strip_0x, + even, + ) +import sha3 +from eth_abi import encode_single + +# local imports +from chainlib.eth.address import to_checksum +from chainlib.jsonrpc import ( + jsonrpc_result, + IntSequenceGenerator, + ) +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.gas import ( + OverrideGasOracle, + balance, + ) +from chainlib.chain import ChainSpec + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('address', type=str, help='Account address') +args = argparser.parse_args() + + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +conn = EthHTTPConnection(args.p, auth=auth) + +gas_oracle = OverrideGasOracle(conn) + +address = to_checksum(args.address) +if not args.u and address != add_0x(args.address): + raise ValueError('invalid checksum address') + +chain_spec = ChainSpec.from_chain_str(args.i) + +def main(): + r = None + decimals = 18 + + o = balance(address, id_generator=rpc_id_generator) + r = conn.do(o) + + hx = strip_0x(r) + balance_value = int(hx, 16) + logg.debug('balance {} = {} decimals {}'.format(even(hx), balance_value, decimals)) + + balance_str = str(balance_value) + balance_len = len(balance_str) + if balance_len < decimals + 1: + print('0.{}'.format(balance_str.zfill(decimals))) + else: + offset = balance_len-decimals + print('{}.{}'.format(balance_str[:offset],balance_str[offset:])) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/checksum.py b/chainlib/eth/runnable/checksum.py new file mode 100644 index 0000000..8a9daca --- /dev/null +++ b/chainlib/eth/runnable/checksum.py @@ -0,0 +1,33 @@ +# standard imports +import sys +import select + +# external imports +from hexathon import strip_0x + +# local imports +from chainlib.eth.address import to_checksum_address + +v = None +if len(sys.argv) > 1: + v = sys.argv[1] +else: + h = select.select([sys.stdin], [], [], 0) + if len(h[0]) > 0: + v = h[0][0].read() + v = v.rstrip() + +if v == None: + sys.stderr.write('input missing\n') + sys.exit(1) + +def main(): + try: + print(to_checksum_address(strip_0x(v))) + except ValueError as e: + sys.stderr.write('invalid input: {}\n'.format(e)) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py new file mode 100644 index 0000000..039fc2f --- /dev/null +++ b/chainlib/eth/runnable/count.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import select + +# local imports +from chainlib.eth.address import to_checksum +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.tx import count +from chainlib.chain import ChainSpec +from chainlib.jsonrpc import IntSequenceGenerator +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from hexathon import add_0x + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +def stdin_arg(): + h = select.select([sys.stdin], [], [], 0) + if len(h[0]) > 0: + v = h[0][0].read() + return v.rstrip() + return None + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') +argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('address', nargs='?', type=str, default=stdin_arg(), help='Ethereum address of recipient') +args = argparser.parse_args() + +if args.address == None: + argparser.error('need first positional argument or value from stdin') + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + + +signer_address = None +keystore = DictKeystore() +if args.y != None: + logg.debug('loading keystore file {}'.format(args.y)) + signer_address = keystore.import_keystore_file(args.y, passphrase) + logg.debug('now have key for signer address {}'.format(signer_address)) +signer = EIP155Signer(keystore) + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +rpc = EthHTTPConnection(args.p, auth=auth) + +def main(): + recipient = to_checksum(args.address) + if not args.u and recipient != add_0x(args.address): + raise ValueError('invalid checksum address') + + o = count(recipient, id_generator=rpc_id_generator) + r = rpc.do(o) + count_result = None + try: + count_result = int(r, 16) + except ValueError: + count_result = int(r, 10) + print(count_result) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/decode.py b/chainlib/eth/runnable/decode.py new file mode 100644 index 0000000..b598fcf --- /dev/null +++ b/chainlib/eth/runnable/decode.py @@ -0,0 +1,65 @@ +#!python3 + +"""Decode raw transaction + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import select + +# external imports +from chainlib.eth.tx import unpack +from chainlib.chain import ChainSpec + +# local imports +from chainlib.eth.runnable.util import decode_for_puny_humans + + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +def stdin_arg(t=0): + h = select.select([sys.stdin], [], [], t) + if len(h[0]) > 0: + v = h[0][0].read() + return v.rstrip() + return None + +argparser = argparse.ArgumentParser() +argparser.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('tx', type=str, nargs='?', default=stdin_arg(), help='hex-encoded signed raw transaction') +args = argparser.parse_args() + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +argp = args.tx +logg.debug('txxxx {}'.format(args.tx)) +if argp == None: + argp = stdin_arg(t=3) + if argp == None: + argparser.error('need first positional argument or value from stdin') + +chain_spec = ChainSpec.from_chain_str(args.i) + + +def main(): + tx_raw = argp + decode_for_puny_humans(tx_raw, chain_spec, sys.stdout) + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/gas.py b/chainlib/eth/runnable/gas.py new file mode 100644 index 0000000..09ad3da --- /dev/null +++ b/chainlib/eth/runnable/gas.py @@ -0,0 +1,180 @@ +#!python3 + +"""Gas transfer script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import io +import sys +import os +import json +import argparse +import logging +import urllib + +# external imports +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.keystore.dict import DictKeystore +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.eth.address import to_checksum +from chainlib.eth.connection import EthHTTPConnection +from chainlib.jsonrpc import ( + JSONRPCRequest, + IntSequenceGenerator, + ) +from chainlib.eth.nonce import ( + RPCNonceOracle, + OverrideNonceOracle, + ) +from chainlib.eth.gas import ( + RPCGasOracle, + OverrideGasOracle, + Gas, + ) +from chainlib.eth.gas import balance as gas_balance +from chainlib.chain import ChainSpec +from chainlib.eth.runnable.util import decode_for_puny_humans + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') +argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') +argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('--nonce', type=int, help='override nonce') +argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price') +argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') +argparser.add_argument('recipient', type=str, help='ethereum address of recipient') +argparser.add_argument('amount', type=int, help='gas value in wei') +args = argparser.parse_args() + + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +block_all = args.ww +block_last = args.w or block_all + +passphrase_env = 'ETH_PASSPHRASE' +if args.env_prefix != None: + passphrase_env = args.env_prefix + '_' + passphrase_env +passphrase = os.environ.get(passphrase_env) +if passphrase == None: + logg.warning('no passphrase given') + passphrase='' + +signer_address = None +keystore = DictKeystore() +if args.y != None: + logg.debug('loading keystore file {}'.format(args.y)) + signer_address = keystore.import_keystore_file(args.y, password=passphrase) + logg.debug('now have key for signer address {}'.format(signer_address)) +signer = EIP155Signer(keystore) + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +conn = EthHTTPConnection(args.p, auth=auth) + +nonce_oracle = None +if args.nonce != None: + nonce_oracle = OverrideNonceOracle(signer_address, args.nonce, id_generator=rpc_id_generator) +else: + nonce_oracle = RPCNonceOracle(signer_address, conn, id_generator=rpc_id_generator) + +gas_oracle = None +if args.gas_price or args.gas_limit != None: + gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator) +else: + gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator) + + +chain_spec = ChainSpec.from_chain_str(args.i) + +value = args.amount + +send = args.s + +g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) + + +def balance(address, id_generator): + o = gas_balance(address, id_generator=id_generator) + r = conn.do(o) + hx = strip_0x(r) + return int(hx, 16) + + +def main(): + recipient = to_checksum(args.recipient) + if not args.u and recipient != add_0x(args.recipient): + raise ValueError('invalid checksum address') + + logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value)) + if logg.isEnabledFor(logging.DEBUG): + try: + sender_balance = balance(signer_address, rpc_id_generator) + recipient_balance = balance(recipient, rpc_id_generator) + logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance)) + logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance)) + except urllib.error.URLError: + pass + + (tx_hash_hex, o) = g.create(signer_address, recipient, value, id_generator=rpc_id_generator) + + if send: + conn.do(o) + if block_last: + r = conn.wait(tx_hash_hex) + if logg.isEnabledFor(logging.DEBUG): + sender_balance = balance(signer_address, rpc_id_generator) + recipient_balance = balance(recipient, rpc_id_generator) + logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance)) + logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance)) + if r['status'] == 0: + logg.critical('VM revert. Wish I could tell you more') + sys.exit(1) + print(tx_hash_hex) + else: + if logg.isEnabledFor(logging.INFO): + io_str = io.StringIO() + decode_for_puny_humans(o['params'][0], chain_spec, io_str) + print(io_str.getvalue()) + else: + print(o['params'][0]) + + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py new file mode 100644 index 0000000..8516b75 --- /dev/null +++ b/chainlib/eth/runnable/get.py @@ -0,0 +1,158 @@ +#!python3 + +"""Data retrieval script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging +import enum +import select + +# external imports +from hexathon import ( + add_0x, + strip_0x, + ) +import sha3 + +# local imports +from chainlib.eth.address import to_checksum +from chainlib.jsonrpc import ( + JSONRPCRequest, + jsonrpc_result, + IntSequenceGenerator, + ) +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.tx import ( + Tx, + pack, + ) +from chainlib.eth.address import to_checksum_address +from chainlib.eth.block import Block +from chainlib.chain import ChainSpec +from chainlib.status import Status +from chainlib.eth.runnable.util import decode_for_puny_humans + +logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s') +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +def stdin_arg(t=0): + h = select.select([sys.stdin], [], [], t) + if len(h[0]) > 0: + v = h[0][0].read() + return v.rstrip() + return None + +argparser = argparse.ArgumentParser('eth-get', description='display information about an Ethereum address or transaction', epilog='address/transaction can be provided as an argument or from standard input') +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('--rlp', action='store_true', help='Display transaction as raw rlp') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('item', nargs='?', default=stdin_arg(), type=str, help='Item to get information for (address og transaction)') +args = argparser.parse_args() + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +argp = args.item +if argp == None: + argp = stdin_arg(None) + if argsp == None: + argparser.error('need first positional argument or value from stdin') + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +conn = EthHTTPConnection(args.p, auth=auth) + +chain_spec = ChainSpec.from_chain_str(args.i) + +item = add_0x(args.item) +as_rlp = bool(args.rlp) + + +def get_transaction(conn, tx_hash, id_generator): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getTransactionByHash' + o['params'].append(tx_hash) + o = j.finalize(o) + tx_src = conn.do(o) + if tx_src == None: + logg.error('Transaction {} not found'.format(tx_hash)) + sys.exit(1) + + if as_rlp: + tx_src = Tx.src_normalize(tx_src) + return pack(tx_src, chain_spec).hex() + + tx = None + status = -1 + rcpt = None + + o = j.template() + o['method'] = 'eth_getTransactionReceipt' + o['params'].append(tx_hash) + o = j.finalize(o) + rcpt = conn.do(o) + #status = int(strip_0x(rcpt['status']), 16) + + if tx == None: + tx = Tx(tx_src) + if rcpt != None: + tx.apply_receipt(rcpt) + tx.generate_wire(chain_spec) + return tx + + +def get_address(conn, address, id_generator): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getCode' + o['params'].append(address) + o['params'].append('latest') + o = j.finalize(o) + code = conn.do(o) + + content = strip_0x(code, allow_empty=True) + if len(content) == 0: + return None + + return content + + +def main(): + r = None + if len(item) > 42: + r = get_transaction(conn, item, rpc_id_generator).to_human() + elif args.u or to_checksum_address(item): + r = get_address(conn, item, rpc_id_generator) + print(r) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/info.py b/chainlib/eth/runnable/info.py new file mode 100644 index 0000000..407c8c2 --- /dev/null +++ b/chainlib/eth/runnable/info.py @@ -0,0 +1,170 @@ +#!python3 + +"""Token balance query script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import datetime +import sys +import os +import json +import argparse +import logging + +# third-party imports +from hexathon import ( + add_0x, + strip_0x, + even, + ) +import sha3 +from eth_abi import encode_single + +# local imports +from chainlib.eth.address import ( + to_checksum_address, + is_checksum_address, + ) +from chainlib.eth.chain import network_id +from chainlib.eth.block import ( + block_latest, + block_by_number, + Block, + ) +from chainlib.eth.tx import count +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.gas import ( + OverrideGasOracle, + balance, + price, + ) +from chainlib.jsonrpc import ( + IntSequenceGenerator, + ) +from chainlib.chain import ChainSpec + +BLOCK_SAMPLES = 10 + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('-l', '--long', dest='l', action='store_true', help='Calculate averages through sampling of blocks and txs') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Include summary for keyfile') +argparser.add_argument('address', nargs='?', type=str, help='Include summary for address (conflicts with -y)') +args = argparser.parse_args() + + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +signer = None +holder_address = None +if args.address != None: + if not args.u and not is_checksum_address(args.address): + raise ValueError('invalid checksum address {}'.format(args.address)) + holder_address = add_0x(args.address) +elif args.y != None: + f = open(args.y, 'r') + o = json.load(f) + f.close() + holder_address = add_0x(to_checksum_address(o['address'])) + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +conn = EthHTTPConnection(args.p, auth=auth) + +gas_oracle = OverrideGasOracle(conn) + +token_symbol = 'eth' + +chain_spec = ChainSpec.from_chain_str(args.i) + +human = args.human + +longmode = args.l + +def main(): + o = network_id(id_generator=rpc_id_generator) + r = conn.do(o) + #if human: + # n = format(n, ',') + sys.stdout.write('Network id: {}\n'.format(r)) + + o = block_latest(id_generator=rpc_id_generator) + r = conn.do(o) + n = int(r, 16) + first_block_number = n + if human: + n = format(n, ',') + sys.stdout.write('Block: {}\n'.format(n)) + + o = block_by_number(first_block_number, False, id_generator=rpc_id_generator) + r = conn.do(o) + last_block = Block(r) + last_timestamp = last_block.timestamp + + if longmode: + aggr_time = 0.0 + aggr_gas = 0 + for i in range(BLOCK_SAMPLES): + o = block_by_number(first_block_number-i, False, id_generator=rpc_id_generator) + r = conn.do(o) + block = Block(r) + aggr_time += last_block.timestamp - block.timestamp + + gas_limit = int(r['gasLimit'], 16) + aggr_gas += gas_limit + + last_block = block + last_timestamp = block.timestamp + + n = int(aggr_gas / BLOCK_SAMPLES) + if human: + n = format(n, ',') + + sys.stdout.write('Gaslimit: {}\n'.format(n)) + sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES)) + + o = price(id_generator=rpc_id_generator) + r = conn.do(o) + n = int(r, 16) + if human: + n = format(n, ',') + sys.stdout.write('Gasprice: {}\n'.format(n)) + + if holder_address != None: + o = count(holder_address) + r = conn.do(o) + n = int(r, 16) + sys.stdout.write('Address: {}\n'.format(holder_address)) + sys.stdout.write('Nonce: {}\n'.format(n)) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/raw.py b/chainlib/eth/runnable/raw.py new file mode 100644 index 0000000..887fa0f --- /dev/null +++ b/chainlib/eth/runnable/raw.py @@ -0,0 +1,189 @@ +#!python3 + +"""Gas transfer script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import io +import sys +import os +import json +import argparse +import logging +import urllib + +# external imports +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.keystore.dict import DictKeystore +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.eth.address import to_checksum +from chainlib.eth.connection import EthHTTPConnection +from chainlib.jsonrpc import ( + JSONRPCRequest, + IntSequenceGenerator, + ) +from chainlib.eth.nonce import ( + RPCNonceOracle, + OverrideNonceOracle, + ) +from chainlib.eth.gas import ( + RPCGasOracle, + OverrideGasOracle, + ) +from chainlib.eth.tx import ( + TxFactory, + raw, + ) +from chainlib.chain import ChainSpec +from chainlib.eth.runnable.util import decode_for_puny_humans + +logging.basicConfig(level=logging.WARNING) +logg = logging.getLogger() + +default_eth_provider = os.environ.get('RPC_PROVIDER') +if default_eth_provider == None: + default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') + +argparser = argparse.ArgumentParser() +argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') +argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') +argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') +argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') +argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('--nonce', type=int, help='override nonce') +argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price') +argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit') +argparser.add_argument('-a', '--recipient', dest='a', type=str, help='recipient address (None for contract creation)') +argparser.add_argument('-value', type=int, help='gas value of transaction in wei') +argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') +argparser.add_argument('-v', action='store_true', help='Be verbose') +argparser.add_argument('-vv', action='store_true', help='Be more verbose') +argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') +argparser.add_argument('-l', '--local', dest='l', action='store_true', help='Local contract call') +argparser.add_argument('data', nargs='?', type=str, help='Transaction data') +args = argparser.parse_args() + + +if args.vv: + logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) + +block_all = args.ww +block_last = args.w or block_all + +passphrase_env = 'ETH_PASSPHRASE' +if args.env_prefix != None: + passphrase_env = args.env_prefix + '_' + passphrase_env +passphrase = os.environ.get(passphrase_env) +if passphrase == None: + logg.warning('no passphrase given') + passphrase='' + +signer_address = None +keystore = DictKeystore() +if args.y != None: + logg.debug('loading keystore file {}'.format(args.y)) + signer_address = keystore.import_keystore_file(args.y, password=passphrase) + logg.debug('now have key for signer address {}'.format(signer_address)) +signer = EIP155Signer(keystore) + +rpc_id_generator = None +if args.seq: + rpc_id_generator = IntSequenceGenerator() + +auth = None +if os.environ.get('RPC_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) +conn = EthHTTPConnection(args.p, auth=auth) + +send = args.s + +local = args.l +if local: + send = False + +nonce_oracle = None +gas_oracle = None +if signer_address != None and not local: + if args.nonce != None: + nonce_oracle = OverrideNonceOracle(signer_address, args.nonce) + else: + nonce_oracle = RPCNonceOracle(signer_address, conn) + + if args.gas_price or args.gas_limit != None: + gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator) + else: + gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator) + +chain_spec = ChainSpec.from_chain_str(args.i) + +value = args.value + + +g = TxFactory(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) + +def main(): + recipient = None + if args.a != None: + recipient = add_0x(to_checksum(args.a)) + if not args.u and recipient != add_0x(recipient): + raise ValueError('invalid checksum address') + + if local: + j = JSONRPCRequest(id_generator=rpc_id_generator) + o = j.template() + o['method'] = 'eth_call' + o['params'].append({ + 'to': recipient, + 'from': signer_address, + 'value': '0x00', + 'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit + 'gasPrice': '0x01', + 'data': add_0x(args.data), + }) + o['params'].append('latest') + o = j.finalize(o) + r = conn.do(o) + print(strip_0x(r)) + return + + elif signer_address != None: + tx = g.template(signer_address, recipient, use_nonce=True) + if args.data != None: + tx = g.set_code(tx, add_0x(args.data)) + + (tx_hash_hex, o) = g.finalize(tx, id_generator=rpc_id_generator) + + if send: + r = conn.do(o) + print(r) + else: + print(o) + print(tx_hash_hex) + + else: + o = raw(args.data, id_generator=rpc_id_generator) + if send: + r = conn.do(o) + print(r) + else: + print(o) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/runnable/subscribe.py b/chainlib/eth/runnable/subscribe.py new file mode 100644 index 0000000..c31d988 --- /dev/null +++ b/chainlib/eth/runnable/subscribe.py @@ -0,0 +1,21 @@ +import json + +import websocket + +ws = websocket.create_connection('ws://localhost:8545') + +o = { + "jsonrpc": "2.0", + "method": "eth_subscribe", + "params": [ + "newHeads", + ], + "id": 0, + } + +ws.send(json.dumps(o).encode('utf-8')) + +while True: + print(ws.recv()) + +ws.close() diff --git a/chainlib/eth/runnable/util.py b/chainlib/eth/runnable/util.py new file mode 100644 index 0000000..1ae6cc9 --- /dev/null +++ b/chainlib/eth/runnable/util.py @@ -0,0 +1,31 @@ +# local imports +from chainlib.eth.tx import unpack +from hexathon import ( + strip_0x, + add_0x, + ) + +def decode_out(tx, writer, skip_keys=[]): + for k in tx.keys(): + if k in skip_keys: + continue + x = None + if k == 'value': + x = '{:.18f} eth'.format(tx[k] / (10**18)) + elif k == 'gasPrice': + x = '{} gwei'.format(int(tx[k] / (10**9))) + elif k == 'value': + k = 'gas-value' + if x != None: + writer.write('{}: {} ({})\n'.format(k, tx[k], x)) + else: + writer.write('{}: {}\n'.format(k, tx[k])) + + +def decode_for_puny_humans(tx_raw, chain_spec, writer, skip_keys=[]): + tx_raw = strip_0x(tx_raw) + tx_raw_bytes = bytes.fromhex(tx_raw) + tx = unpack(tx_raw_bytes, chain_spec) + decode_out(tx, writer, skip_keys=skip_keys) + writer.write('src: {}\n'.format(add_0x(tx_raw))) + diff --git a/chainlib/eth/sign.py b/chainlib/eth/sign.py new file mode 100644 index 0000000..2822ed2 --- /dev/null +++ b/chainlib/eth/sign.py @@ -0,0 +1,26 @@ +# local imports +from chainlib.jsonrpc import JSONRPCRequest + + +def new_account(passphrase='', id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'personal_newAccount' + o['params'] = [passphrase] + return j.finalize(o) + + +def sign_transaction(payload, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_signTransaction' + o['params'] = [payload] + return j.finalize(o) + + +def sign_message(address, payload, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_sign' + o['params'] = [address, payload] + return j.finalize(o) diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py new file mode 100644 index 0000000..085c25a --- /dev/null +++ b/chainlib/eth/tx.py @@ -0,0 +1,520 @@ +# standard imports +import logging +import enum +import re + +# external imports +import coincurve +import sha3 +from hexathon import ( + strip_0x, + add_0x, + ) +from rlp import decode as rlp_decode +from rlp import encode as rlp_encode +from crypto_dev_signer.eth.transaction import EIP155Transaction +from crypto_dev_signer.encoding import public_key_to_address +from crypto_dev_signer.eth.encoding import chain_id_to_v +from potaahto.symbols import snake_and_camel + + +# local imports +from chainlib.hash import keccak256_hex_to_hex +from chainlib.status import Status +from .address import to_checksum +from .constant import ( + MINIMUM_FEE_UNITS, + MINIMUM_FEE_PRICE, + ZERO_ADDRESS, + ) +from .contract import ABIContractEncoder +from chainlib.jsonrpc import JSONRPCRequest + +logg = logging.getLogger().getChild(__name__) + + + +class TxFormat(enum.IntEnum): + DICT = 0x00 + RAW = 0x01 + RAW_SIGNED = 0x02 + RAW_ARGS = 0x03 + RLP = 0x10 + RLP_SIGNED = 0x11 + JSONRPC = 0x10 + + +field_debugs = [ + 'nonce', + 'gasPrice', + 'gas', + 'to', + 'value', + 'data', + 'v', + 'r', + 's', + ] + +def count(address, confirmed=False, id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getTransactionCount' + o['params'].append(address) + if confirmed: + o['params'].append('latest') + else: + o['params'].append('pending') + return j.finalize(o) + +count_pending = count + +def count_confirmed(address): + return count(address, True) + + +def pack(tx_src, chain_spec): + if isinstance(tx_src, Tx): + tx_src = tx_src.as_dict() + tx_src = Tx.src_normalize(tx_src) + tx = EIP155Transaction(tx_src, tx_src['nonce'], chain_spec.chain_id()) + + signature = bytearray(65) + cursor = 0 + for a in [ + tx_src['r'], + tx_src['s'], + ]: + for b in bytes.fromhex(strip_0x(a)): + signature[cursor] = b + cursor += 1 + + #signature[cursor] = chainv_to_v(chain_spec.chain_id(), tx_src['v']) + tx.apply_signature(chain_spec.chain_id(), signature, v=tx_src['v']) + logg.debug('tx {}'.format(tx.serialize())) + return tx.rlp_serialize() + + +def unpack(tx_raw_bytes, chain_spec): + chain_id = chain_spec.chain_id() + tx = __unpack_raw(tx_raw_bytes, chain_id) + tx['nonce'] = int.from_bytes(tx['nonce'], 'big') + tx['gasPrice'] = int.from_bytes(tx['gasPrice'], 'big') + tx['gas'] = int.from_bytes(tx['gas'], 'big') + tx['value'] = int.from_bytes(tx['value'], 'big') + return tx + + +def unpack_hex(tx_raw_bytes, chain_spec): + chain_id = chain_spec.chain_id() + tx = __unpack_raw(tx_raw_bytes, chain_id) + tx['nonce'] = add_0x(hex(tx['nonce'])) + tx['gasPrice'] = add_0x(hex(tx['gasPrice'])) + tx['gas'] = add_0x(hex(tx['gas'])) + tx['value'] = add_0x(hex(tx['value'])) + tx['chainId'] = add_0x(hex(tx['chainId'])) + return tx + + +def __unpack_raw(tx_raw_bytes, chain_id=1): + d = rlp_decode(tx_raw_bytes) + + logg.debug('decoding using chain id {}'.format(str(chain_id))) + + j = 0 + for i in d: + v = i.hex() + if j != 3 and v == '': + v = '00' + logg.debug('decoded {}: {}'.format(field_debugs[j], v)) + j += 1 + vb = chain_id + if chain_id != 0: + v = int.from_bytes(d[6], 'big') + vb = v - (chain_id * 2) - 35 + r = bytearray(32) + r[32-len(d[7]):] = d[7] + s = bytearray(32) + s[32-len(d[8]):] = d[8] + logg.debug('vb {}'.format(vb)) + sig = b''.join([r, s, bytes([vb])]) + #so = KeyAPI.Signature(signature_bytes=sig) + + h = sha3.keccak_256() + h.update(rlp_encode(d)) + signed_hash = h.digest() + + d[6] = chain_id + d[7] = b'' + d[8] = b'' + + h = sha3.keccak_256() + h.update(rlp_encode(d)) + unsigned_hash = h.digest() + + #p = so.recover_public_key_from_msg_hash(unsigned_hash) + #a = p.to_checksum_address() + pubk = coincurve.PublicKey.from_signature_and_message(sig, unsigned_hash, hasher=None) + a = public_key_to_address(pubk) + logg.debug('decoded recovery byte {}'.format(vb)) + logg.debug('decoded address {}'.format(a)) + logg.debug('decoded signed hash {}'.format(signed_hash.hex())) + logg.debug('decoded unsigned hash {}'.format(unsigned_hash.hex())) + + to = d[3].hex() or None + if to != None: + to = to_checksum(to) + + data = d[5].hex() + try: + data = add_0x(data) + except: + data = '0x' + + return { + 'from': a, + 'to': to, + 'nonce': d[0], + 'gasPrice': d[1], + 'gas': d[2], + 'value': d[4], + 'data': data, + 'v': v, + 'recovery_byte': vb, + 'r': add_0x(sig[:32].hex()), + 's': add_0x(sig[32:64].hex()), + 'chainId': chain_id, + 'hash': add_0x(signed_hash.hex()), + 'hash_unsigned': add_0x(unsigned_hash.hex()), + } + + +def transaction(hsh, id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getTransactionByHash' + o['params'].append(add_0x(hsh)) + return j.finalize(o) + + +def transaction_by_block(hsh, idx, id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getTransactionByBlockHashAndIndex' + o['params'].append(add_0x(hsh)) + o['params'].append(hex(idx)) + return j.finalize(o) + + +def receipt(hsh, id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_getTransactionReceipt' + o['params'].append(add_0x(hsh)) + return j.finalize(o) + + +def raw(tx_raw_hex, id_generator=None): + j = JSONRPCRequest(id_generator=id_generator) + o = j.template() + o['method'] = 'eth_sendRawTransaction' + o['params'].append(add_0x(tx_raw_hex)) + return j.finalize(o) + + +class TxFactory: + + fee = 8000000 + + def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None): + self.gas_oracle = gas_oracle + self.nonce_oracle = nonce_oracle + self.chain_spec = chain_spec + self.signer = signer + + + def build_raw(self, tx): + if tx['to'] == None or tx['to'] == '': + tx['to'] = '0x' + txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) + tx_raw = self.signer.sign_transaction_to_rlp(txe) + tx_raw_hex = add_0x(tx_raw.hex()) + tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex)) + return (tx_hash_hex, tx_raw_hex) + + + def build(self, tx, id_generator=None): + (tx_hash_hex, tx_raw_hex) = self.build_raw(tx) + o = raw(tx_raw_hex, id_generator=id_generator) + return (tx_hash_hex, o) + + + def template(self, sender, recipient, use_nonce=False): + gas_price = MINIMUM_FEE_PRICE + gas_limit = MINIMUM_FEE_UNITS + if self.gas_oracle != None: + (gas_price, gas_limit) = self.gas_oracle.get_gas() + logg.debug('using gas price {} limit {}'.format(gas_price, gas_limit)) + nonce = 0 + o = { + 'from': sender, + 'to': recipient, + 'value': 0, + 'data': '0x', + 'gasPrice': gas_price, + 'gas': gas_limit, + 'chainId': self.chain_spec.chain_id(), + } + if self.nonce_oracle != None and use_nonce: + nonce = self.nonce_oracle.next_nonce() + logg.debug('using nonce {} for address {}'.format(nonce, sender)) + o['nonce'] = nonce + return o + + + def normalize(self, tx): + txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) + txes = txe.serialize() + return { + 'from': tx['from'], + 'to': txes['to'], + 'gasPrice': txes['gasPrice'], + 'gas': txes['gas'], + 'data': txes['data'], + } + + + def finalize(self, tx, tx_format=TxFormat.JSONRPC, id_generator=None): + if tx_format == TxFormat.JSONRPC: + return self.build(tx, id_generator=id_generator) + elif tx_format == TxFormat.RLP_SIGNED: + return self.build_raw(tx) + raise NotImplementedError('tx formatting {} not implemented'.format(tx_format)) + + + def set_code(self, tx, data, update_fee=True): + tx['data'] = data + if update_fee: + tx['gas'] = TxFactory.fee + if self.gas_oracle != None: + (price, tx['gas']) = self.gas_oracle.get_gas(code=data) + else: + logg.debug('using hardcoded gas limit of 8000000 until we have reliable vm executor') + return tx + + + def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + enc.method(method) + data = enc.get() + tx = self.template(sender_address, contract_address, use_nonce=True) + tx = self.set_code(tx, data) + tx = self.finalize(tx, tx_format) + return tx + + + def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, id_generator=None): + j = JSONRPCRequest(id_generator) + o = j.template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method(method) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address) + tx = self.set_code(tx, data) + o['params'].append(self.normalize(tx)) + o['params'].append('latest') + o = j.finalize(o) + return o + + +class Tx: + + # TODO: force tx type schema parser (whether expect hex or int etc) + def __init__(self, src, block=None, rcpt=None): + self.tx_src = self.src_normalize(src) + self.index = -1 + tx_hash = add_0x(src['hash']) + if block != None: + i = 0 + for tx in block.txs: + tx_hash_block = None + try: + tx_hash_block = tx['hash'] + except TypeError: + tx_hash_block = add_0x(tx) + if tx_hash_block == tx_hash: + self.index = i + break + i += 1 + if self.index == -1: + raise AttributeError('tx {} not found in block {}'.format(tx_hash, block.hash)) + self.block = block + self.hash = strip_0x(tx_hash) + try: + self.value = int(strip_0x(src['value']), 16) + except TypeError: + self.value = int(src['value']) + try: + self.nonce = int(strip_0x(src['nonce']), 16) + except TypeError: + self.nonce = int(src['nonce']) + address_from = strip_0x(src['from']) + try: + self.gas_price = int(strip_0x(src['gasPrice']), 16) + except TypeError: + self.gas_price = int(src['gasPrice']) + try: + self.gas_limit = int(strip_0x(src['gas']), 16) + except TypeError: + self.gas_limit = int(src['gas']) + self.outputs = [to_checksum(address_from)] + self.contract = None + + try: + inpt = src['input'] + except KeyError: + inpt = src['data'] + + if inpt != '0x': + inpt = strip_0x(inpt) + else: + inpt = '' + self.payload = inpt + + to = src['to'] + if to == None: + to = ZERO_ADDRESS + self.inputs = [to_checksum(strip_0x(to))] + + self.block = block + try: + self.wire = src['raw'] + except KeyError: + logg.warning('no inline raw tx src, and no raw rendering implemented, field will be "None"') + + self.status = Status.PENDING + self.logs = None + + if rcpt != None: + self.apply_receipt(rcpt) + + self.v = src.get('v') + self.r = src.get('r') + self.s = src.get('s') + + self.wire = None + + + def src(self): + return self.tx_src + + + @classmethod + def src_normalize(self, src): + src = snake_and_camel(src) + + if isinstance(src.get('v'), str): + try: + src['v'] = int(src['v']) + except ValueError: + src['v'] = int(src['v'], 16) + return src + + + def as_dict(self): + return self.src() + + + def apply_receipt(self, rcpt): + rcpt = self.src_normalize(rcpt) + logg.debug('rcpt {}'.format(rcpt)) + try: + status_number = int(rcpt['status'], 16) + except TypeError: + status_number = int(rcpt['status']) + if status_number == 1: + self.status = Status.SUCCESS + elif status_number == 0: + self.status = Status.ERROR + # TODO: replace with rpc receipt/transaction translator when available + contract_address = rcpt.get('contractAddress') + if contract_address == None: + contract_address = rcpt.get('contract_address') + if contract_address != None: + self.contract = contract_address + self.logs = rcpt['logs'] + try: + self.gas_used = int(rcpt['gasUsed'], 16) + except TypeError: + self.gas_used = int(rcpt['gasUsed']) + + + def apply_block(self, block): + #block_src = self.src_normalize(block_src) + self.block = block + + + def generate_wire(self, chain_spec): + b = pack(self.src(), chain_spec) + self.wire = add_0x(b.hex()) + + + @staticmethod + def from_src(src, block=None): + return Tx(src, block=block) + + + def __str__(self): + if self.block != None: + return 'tx {} status {} block {} index {}'.format(add_0x(self.hash), self.status.name, self.block.number, self.index) + else: + return 'tx {} status {}'.format(add_0x(self.hash), self.status.name) + + + def __repr__(self): + return self.__str__() + + + def to_human(self): + s = """hash {} +from {} +to {} +value {} +nonce {} +gasPrice {} +gasLimit {} +input {} +""".format( + self.hash, + self.outputs[0], + self.inputs[0], + self.value, + self.nonce, + self.gas_price, + self.gas_limit, + self.payload, + ) + + if self.status != Status.PENDING: + s += """gasUsed {} +""".format( + self.gas_used, + ) + + s += 'status ' + self.status.name + '\n' + + if self.contract != None: + s += """contract {} +""".format( + self.contract, + ) + + if self.wire != None: + s += """src {} +""".format( + self.wire, + ) + + return s + diff --git a/chainlib/eth/unittest/base.py b/chainlib/eth/unittest/base.py new file mode 100644 index 0000000..48eb811 --- /dev/null +++ b/chainlib/eth/unittest/base.py @@ -0,0 +1,218 @@ +# standard imports +import os +import logging + +# external imports +import eth_tester +import coincurve +from chainlib.connection import ( + RPCConnection, + error_parser, + ) +from chainlib.eth.address import ( + to_checksum_address, + ) +from chainlib.jsonrpc import ( + jsonrpc_response, + jsonrpc_error, + jsonrpc_result, + ) +from hexathon import ( + unpad, + add_0x, + strip_0x, + ) + +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.encoding import private_key_to_address + + +logg = logging.getLogger().getChild(__name__) + +test_pk = bytes.fromhex('5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6') + + +class EthTesterSigner(eth_tester.EthereumTester): + + def __init__(self, backend, keystore): + super(EthTesterSigner, self).__init__(backend) + logg.debug('accounts {}'.format(self.get_accounts())) + + self.keystore = keystore + self.backend = backend + self.backend.add_account(test_pk) + for pk in self.backend.account_keys: + pubk = pk.public_key + address = pubk.to_checksum_address() + logg.debug('test keystore have pk {} pubk {} addr {}'.format(pk, pk.public_key, address)) + self.keystore.import_raw_key(pk._raw_key) + + + def new_account(self): + pk = os.urandom(32) + address = self.keystore.import_raw_key(pk) + checksum_address = add_0x(to_checksum_address(address)) + self.backend.add_account(pk) + return checksum_address + + +class TestRPCConnection(RPCConnection): + + def __init__(self, location, backend, signer): + super(TestRPCConnection, self).__init__(location) + self.backend = backend + self.signer = signer + + + def do(self, o, error_parser=error_parser): + logg.debug('testrpc do {}'.format(o)) + m = getattr(self, o['method']) + if m == None: + raise ValueError('unhandled method {}'.format(o['method'])) + r = None + try: + result = m(o['params']) + logg.debug('result {}'.format(result)) + r = jsonrpc_response(o['id'], result) + except Exception as e: + logg.exception(e) + r = jsonrpc_error(o['id'], message=str(e)) + return jsonrpc_result(r, error_parser) + + + def eth_blockNumber(self, p): + block = self.backend.get_block_by_number('latest') + return block['number'] + + + def eth_getBlockByNumber(self, p): + b = bytes.fromhex(strip_0x(p[0])) + n = int.from_bytes(b, 'big') + block = self.backend.get_block_by_number(n) + return block + + + def eth_getBlockByHash(self, p): + block = self.backend.get_block_by_hash(p[0]) + return block + + + def eth_getTransactionByBlock(self, p): + block = self.eth_getBlockByHash(p) + try: + tx_index = int(p[1], 16) + except TypeError: + tx_index = int(p[1]) + tx_hash = block['transactions'][tx_index] + tx = self.eth_getTransactionByHash([tx_hash]) + return tx + + def eth_getBalance(self, p): + balance = self.backend.get_balance(p[0]) + hx = balance.to_bytes(32, 'big').hex() + return add_0x(unpad(hx)) + + + def eth_getTransactionCount(self, p): + nonce = self.backend.get_nonce(p[0]) + hx = nonce.to_bytes(4, 'big').hex() + return add_0x(unpad(hx)) + + + def eth_getTransactionByHash(self, p): + tx = self.backend.get_transaction_by_hash(p[0]) + return tx + + + def eth_getTransactionByBlockHashAndIndex(self, p): + #logg.debug('p {}'.format(p)) + #block = self.eth_getBlockByHash(p[0]) + #tx = block.transactions[p[1]] + #return eth_getTransactionByHash(tx[0]) + return self.eth_getTransactionByBlock(p) + + + def eth_getTransactionReceipt(self, p): + rcpt = self.backend.get_transaction_receipt(p[0]) + if rcpt.get('block_number') == None: + rcpt['block_number'] = rcpt['blockNumber'] + else: + rcpt['blockNumber'] = rcpt['block_number'] + return rcpt + + + def eth_getCode(self, p): + r = self.backend.get_code(p[0]) + return r + + + def eth_call(self, p): + tx_ethtester = to_ethtester_call(p[0]) + r = self.backend.call(tx_ethtester) + return r + + + def eth_gasPrice(self, p): + return hex(1000000000) + + + def personal_newAccount(self, passphrase): + a = self.backend.new_account() + return a + + + def eth_sign(self, p): + r = self.signer.sign_ethereum_message(strip_0x(p[0]), strip_0x(p[1])) + return r + + + def eth_sendRawTransaction(self, p): + r = self.backend.send_raw_transaction(p[0]) + return r + + + def eth_signTransaction(self, p): + raise NotImplementedError('needs transaction deserializer for EIP155Transaction') + tx_dict = p[0] + tx = EIP155Transaction(tx_dict, tx_dict['nonce'], tx_dict['chainId']) + passphrase = p[1] + r = self.signer.sign_transaction_to_rlp(tx, passphrase) + return r + + + def __verify_signer(self, tx, passphrase=''): + pk_bytes = self.backend.keystore.get(tx.sender) + pk = coincurve.PrivateKey(secret=pk_bytes) + result_address = private_key_to_address(pk) + assert strip_0x(result_address) == strip_0x(tx.sender) + + + def sign_transaction(self, tx, passphrase=''): + self.__verify_signer(tx, passphrase) + return self.signer.sign_transaction(tx, passphrase) + + + def sign_transaction_to_rlp(self, tx, passphrase=''): + self.__verify_signer(tx, passphrase) + return self.signer.sign_transaction_to_rlp(tx, passphrase) + + + def disconnect(self): + pass + + +def to_ethtester_call(tx): + if tx['gas'] == '': + tx['gas'] = '0x00' + + if tx['gasPrice'] == '': + tx['gasPrice'] = '0x00' + + tx = { + 'to': tx['to'], + 'from': tx['from'], + 'gas': int(tx['gas'], 16), + 'gas_price': int(tx['gasPrice'], 16), + 'data': tx['data'], + } + return tx diff --git a/chainlib/eth/unittest/ethtester.py b/chainlib/eth/unittest/ethtester.py new file mode 100644 index 0000000..ed3eba2 --- /dev/null +++ b/chainlib/eth/unittest/ethtester.py @@ -0,0 +1,80 @@ +# standard imports +import os +import unittest +import logging + +# external imports +import eth_tester +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.keystore.dict import DictKeystore +from hexathon import ( + strip_0x, + add_0x, + ) +from eth import constants +from eth.vm.forks.byzantium import ByzantiumVM + +# local imports +from .base import ( + EthTesterSigner, + TestRPCConnection, + ) +from chainlib.connection import ( + RPCConnection, + ConnType, + ) +from chainlib.eth.address import to_checksum_address +from chainlib.chain import ChainSpec + +logg = logging.getLogger(__name__) + +test_address = bytes.fromhex('Eb3907eCad74a0013c259D5874AE7f22DcBcC95C') + + +def create_tester_signer(keystore): + genesis_params = eth_tester.backends.pyevm.main.get_default_genesis_params({ + 'gas_limit': 8000000, + 'coinbase': test_address, # doesn't seem to work + }) + vm_configuration = ( + (constants.GENESIS_BLOCK_NUMBER, ByzantiumVM), + ) + genesis_state = eth_tester.PyEVMBackend._generate_genesis_state(num_accounts=30) + eth_backend = eth_tester.PyEVMBackend( + genesis_state=genesis_state, + genesis_parameters=genesis_params, + vm_configuration=vm_configuration, + ) + return EthTesterSigner(eth_backend, keystore) + + +class EthTesterCase(unittest.TestCase): + + def __init__(self, foo): + super(EthTesterCase, self).__init__(foo) + self.accounts = [] + + + def setUp(self): + self.chain_spec = ChainSpec('evm', 'foochain', 42) + self.keystore = DictKeystore() + eth_tester_instance = create_tester_signer(self.keystore) + self.signer = EIP155Signer(self.keystore) + self.helper = eth_tester_instance + self.backend = self.helper.backend + self.rpc = TestRPCConnection(None, eth_tester_instance, self.signer) + for a in self.keystore.list(): + self.accounts.append(add_0x(to_checksum_address(a))) + + def rpc_with_tester(chain_spec=self.chain_spec, url=None): + return self.rpc + + RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='default') + RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='signer') + RPCConnection.register_location('custom', self.chain_spec, tag='default', exist_ok=True) + RPCConnection.register_location('custom', self.chain_spec, tag='signer', exist_ok=True) + + + + def tearDown(self): + pass diff --git a/example/call_balance.py b/example/call_balance.py new file mode 100644 index 0000000..10a0438 --- /dev/null +++ b/example/call_balance.py @@ -0,0 +1,25 @@ +# standard imports +import os + +# external imports +from hexathon import strip_0x, add_0x + +# local imports +from chainlib.eth.gas import balance, parse_balance +from chainlib.eth.connection import EthHTTPConnection + + +# create a random address to check +address_bytes = os.urandom(20) +address = add_0x(address_bytes.hex()) + +# connect to rpc node and send request for balance +rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545') +rpc = EthHTTPConnection(rpc_provider) +o = balance(address) +r = rpc.do(o) + +clean_address = strip_0x(address) +clean_balance = parse_balance(r) + +print('address {} has balance {}'.format(clean_address, clean_balance)) diff --git a/example/contract_transaction.py b/example/contract_transaction.py new file mode 100644 index 0000000..7ef0b64 --- /dev/null +++ b/example/contract_transaction.py @@ -0,0 +1,75 @@ +# standard imports +import os + +# external imports +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.chain import ChainSpec +from chainlib.eth.nonce import OverrideNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.tx import ( + TxFactory, + TxFormat, + unpack, + pack, + raw, + ) +from chainlib.eth.contract import ( + ABIContractEncoder, + ABIContractDecoder, + ABIContractType, + ) + +# eth transactions need an explicit chain parameter as part of their signature +chain_spec = ChainSpec.from_chain_str('evm:ethereum:1') + +# create keystore and signer +keystore = DictKeystore() +signer = EIP155Signer(keystore) +sender_address = keystore.new() +recipient_address = keystore.new() + +# explicitly set nonce and gas parameters on this transaction +nonce_oracle = OverrideNonceOracle(sender_address, 0) +gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + +# encode the contract parameters +enc = ABIContractEncoder() +enc.method('fooBar') +enc.typ(ABIContractType.ADDRESS) +enc.typ(ABIContractType.UINT256) +enc.address(recipient_address) +enc.uint256(42) +data = enc.get() + +# create a new transaction, but output in raw rlp format +tx_factory = TxFactory(chain_spec, signer=signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) +tx = tx_factory.template(sender_address, recipient_address, use_nonce=True) +tx = tx_factory.set_code(tx, data) +(tx_hash, tx_signed_raw) = tx_factory.finalize(tx, tx_format=TxFormat.RLP_SIGNED) + +print('contract transaction: {}'.format(tx)) + +# retrieve the input data from the transaction +tx_src = unpack(bytes.fromhex(strip_0x(tx_signed_raw)), chain_spec) +data_recovered = strip_0x(tx_src['data']) + +# decode the contract parameters +dec = ABIContractDecoder() +dec.typ(ABIContractType.ADDRESS) +dec.typ(ABIContractType.UINT256) +# (yes, this interface needs to be vastly improved, it should take the whole buffer and advance with cursor itself) +cursor = 8 # the method signature is 8 characters long. input data to the solidity function starts after that +dec.val(data_recovered[cursor:cursor+64]) +cursor += 64 +dec.val(data_recovered[cursor:cursor+64]) +r = dec.decode() + +print('contract param 1 {}'.format(r[0])) +print('contract param 2 {}'.format(r[1])) diff --git a/example/jsonrpc.py b/example/jsonrpc.py new file mode 100644 index 0000000..0e6d326 --- /dev/null +++ b/example/jsonrpc.py @@ -0,0 +1,29 @@ +# standard imports +import os +import sys + +# local imports +from chainlib.jsonrpc import jsonrpc_template +from chainlib.eth.connection import EthHTTPConnection + +# set up node connection and execute rpc call +rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545') +rpc = EthHTTPConnection(rpc_provider) + +# check the connection +if not rpc.check(): + sys.stderr.write('node {} not usable\n'.format(rpc_provider)) + sys.exit(1) + +# build and send rpc call +o = jsonrpc_template() +o['method'] = 'eth_blockNumber' +r = rpc.do(o) + +# interpret result for humans +try: + block_number = int(r, 10) +except ValueError: + block_number = int(r, 16) + +print('block number {}'.format(block_number)) diff --git a/example/online_transaction.py b/example/online_transaction.py new file mode 100644 index 0000000..7fad016 --- /dev/null +++ b/example/online_transaction.py @@ -0,0 +1,57 @@ +# standard imports +import sys +import os + +# external imports +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.chain import ChainSpec +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import RPCGasOracle +from chainlib.eth.tx import ( + TxFactory, + TxFormat, + ) +from chainlib.error import JSONRPCException + +# eth transactions need an explicit chain parameter as part of their signature +chain_spec = ChainSpec.from_chain_str('evm:ethereum:1') + +# create keystore and signer +keystore = DictKeystore() +signer = EIP155Signer(keystore) +sender_address = keystore.new() +recipient_address = keystore.new() + +# set up node connection +rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545') +rpc = EthHTTPConnection(rpc_provider) + +# check the connection +if not rpc.check(): + sys.stderr.write('node {} not usable\n'.format(rpc_provider)) + sys.exit(1) + +# nonce will now be retrieved from network +nonce_oracle = RPCNonceOracle(sender_address, rpc) + +# gas price retrieved from network, and limit from callback +def calculate_gas(code=None): + return 21000 +gas_oracle = RPCGasOracle(rpc, code_callback=calculate_gas) + +# create a new transaction +tx_factory = TxFactory(chain_spec, signer=signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) +tx = tx_factory.template(sender_address, recipient_address, use_nonce=True) +tx['value'] = 1024 +(tx_hash, tx_rpc) = tx_factory.finalize(tx) + +print('transaction hash: ' + tx_hash) +print('jsonrpc payload: ' + str(tx_rpc)) diff --git a/example/transaction.py b/example/transaction.py new file mode 100644 index 0000000..6e6b244 --- /dev/null +++ b/example/transaction.py @@ -0,0 +1,65 @@ +# standard imports +import os + +# external imports +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.chain import ChainSpec +from chainlib.eth.nonce import OverrideNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.tx import ( + TxFactory, + TxFormat, + unpack, + pack, + raw, + ) + +# eth transactions need an explicit chain parameter as part of their signature +chain_spec = ChainSpec.from_chain_str('evm:ethereum:1') + +# create keystore and signer +keystore = DictKeystore() +signer = EIP155Signer(keystore) +sender_address = keystore.new() +recipient_address = keystore.new() + +# explicitly set nonce and gas parameters on this transaction +nonce_oracle = OverrideNonceOracle(sender_address, 0) +gas_oracle = OverrideGasOracle(price=1000000000, limit=21000) + +# create a new transaction +tx_factory = TxFactory(chain_spec, signer=signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle) +tx = tx_factory.template(sender_address, recipient_address, use_nonce=True) +tx['value'] = 1024 +(tx_hash, tx_rpc) = tx_factory.finalize(tx) + +print('transaction hash: ' + tx_hash) +print('jsonrpc payload: ' + str(tx_rpc)) + +# create a new transaction, but output in raw rlp format +tx = tx_factory.template(sender_address, recipient_address, use_nonce=True) # will now have increased nonce by 1 +tx['value'] = 1024 +(tx_hash, tx_signed_raw) = tx_factory.finalize(tx, tx_format=TxFormat.RLP_SIGNED) +print('transaction hash: ' + tx_hash) +print('raw rlp payload: ' + tx_signed_raw) + +# convert tx from raw RLP +tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw)) +tx_src = unpack(tx_signed_raw_bytes, chain_spec) +print('tx parsed from rlp payload: ' + str(tx_src)) + +# .. and back +tx_signed_raw_bytes_recovered = pack(tx_src, chain_spec) +tx_signed_raw_recovered = add_0x(tx_signed_raw_bytes_recovered.hex()) +print('raw rlp payload re-parsed: ' + tx_signed_raw_recovered) + +# create a raw send jsonrpc payload from the raw RLP +o = raw(tx_signed_raw_recovered) +print('jsonrpc payload: ' + str(o)) diff --git a/example/tx_object.py b/example/tx_object.py new file mode 100644 index 0000000..868e55e --- /dev/null +++ b/example/tx_object.py @@ -0,0 +1,51 @@ +# standard imports +import os + +# external imports +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer +from crypto_dev_signer.eth.transaction import EIP155Transaction +from hexathon import ( + add_0x, + strip_0x, + ) + +# local imports +from chainlib.chain import ChainSpec +from chainlib.eth.tx import ( + unpack, + Tx, + ) + +# eth transactions need an explicit chain parameter as part of their signature +chain_spec = ChainSpec.from_chain_str('evm:ethereum:1') +chain_id = chain_spec.chain_id() + +# create keystore and signer +keystore = DictKeystore() +signer = EIP155Signer(keystore) +sender_address = keystore.new() +recipient_address = keystore.new() + + +# set up a transaction dict source +tx_src = { + 'from': sender_address, + 'to': recipient_address, + 'gas': 21000, + 'gasPrice': 1000000000, + 'value': 1024, + 'data': '0xdeadbeef', + } +sender_nonce = 0 +tx = EIP155Transaction(tx_src, sender_nonce, chain_id) +signature = signer.sign_transaction(tx) +print('signature: {}'.format(signature.hex())) + +tx.apply_signature(chain_id, signature) +print('tx with signature: {}'.format(tx.serialize())) + +tx_signed_raw_bytes = tx.rlp_serialize() +tx_src = unpack(tx_signed_raw_bytes, chain_spec) +tx_parsed = Tx(tx_src) +print('parsed signed tx: {}'.format(tx_parsed.to_human())) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..16695e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +crypto-dev-signer~=0.4.14b6 +pysha3==1.0.2 +hexathon~=0.0.1a7 +websocket-client==0.57.0 +potaahto~=0.0.1a1 diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..9ac346f --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e +set -x +#export PYTHONPATH=${PYTHONPATH:.} +for f in `ls tests/*.py`; do + python $f +done +set +x +set +e diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0dc3e90 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,43 @@ +[metadata] +name = chainlib-eth +version = 0.0.5a1 +description = Ethereum implementation of the chainlib interface +author = Louis Holbrook +author_email = dev@holbrook.no +url = https://gitlab.com/chaintools/chainlib +keywords = + dlt + blockchain + cryptocurrency + ethereum +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + Development Status :: 3 - Alpha + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Topic :: Internet +# Topic :: Blockchain :: EVM +license = GPL3 +licence_files = + LICENSE.txt + +[options] +python_requires = >= 3.6 +packages = + chainlib.eth + chainlib.eth.runnable + chainlib.eth.pytest + chainlib.eth.unittest + +[options.entry_points] +console_scripts = + eth-balance = chainlib.eth.runnable.balance:main + eth-checksum = chainlib.eth.runnable.checksum:main + eth-gas = chainlib.eth.runnable.gas:main + eth-raw = chainlib.eth.runnable.raw:main + eth-get = chainlib.eth.runnable.get:main + eth-decode = chainlib.eth.runnable.decode:main + eth-info = chainlib.eth.runnable.info:main + eth = chainlib.eth.runnable.info:main diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c481db7 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +from setuptools import setup +import configparser +import os + + +requirements = [] +f = open('requirements.txt', 'r') +while True: + l = f.readline() + if l == '': + break + requirements.append(l.rstrip()) +f.close() + +test_requirements = [] +f = open('test_requirements.txt', 'r') +while True: + l = f.readline() + if l == '': + break + test_requirements.append(l.rstrip()) +f.close() + + +setup( + install_requires=requirements, + tests_require=test_requirements, + ) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..62ab455 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,4 @@ +eth_tester==0.5.0b3 +py-evm==0.3.0a20 +rlp==2.0.1 +pytest==6.0.1 diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..cc765aa --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,5 @@ +SOLC = /usr/bin/solc + +all: + $(SOLC) --bin TestContract.sol --evm-version byzantium | awk 'NR>3' > TestContract.bin + truncate -s -1 TestContract.bin diff --git a/tests/TestContract.bin b/tests/TestContract.bin new file mode 100644 index 0000000..b489c22 --- /dev/null +++ b/tests/TestContract.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b50610260806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c01000000000000000000000000000000000000000000000000000000009004806333aa24121461004d575b600080fd5b61006760048036038101906100629190610122565b61007d565b604051610074919061018b565b60405180910390f35b6000827fa8ed44a382304c8db2c9059e4de342080401fb8e9a71986396d595c869fa3792836040516100af91906101a6565b60405180910390a27f33c4ea6ccc21f1a14e9de7326edcc52c2b8a302ac875ae6b8fee2560eadd75ad836040516100e691906101c1565b60405180910390a16001905092915050565b600081359050610107816101fc565b92915050565b60008135905061011c81610213565b92915050565b6000806040838503121561013557600080fd5b60006101438582860161010d565b9250506020610154858286016100f8565b9150509250929050565b610167816101dc565b82525050565b610176816101e8565b82525050565b610185816101f2565b82525050565b60006020820190506101a0600083018461015e565b92915050565b60006020820190506101bb600083018461016d565b92915050565b60006020820190506101d6600083018461017c565b92915050565b60008115159050919050565b6000819050919050565b6000819050919050565b610205816101e8565b811461021057600080fd5b50565b61021c816101f2565b811461022757600080fd5b5056fea26469706673582212205c428504ca5a53a9250d76b287bec8ee4b13d5c7cb386a0f67cbf77ecb96a9ea64736f6c63430008040033 \ No newline at end of file diff --git a/tests/TestContract.sol b/tests/TestContract.sol new file mode 100644 index 0000000..c3de74f --- /dev/null +++ b/tests/TestContract.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.8.0; + +contract TestEventContract { + + event TestEventOne(uint256 indexed _foo, bytes32 _bar); + event TestEventTwo(uint256 _foo); + + function foo(uint256 _foo, bytes32 _bar) public returns (bool) { + emit TestEventOne(_foo, _bar); + emit TestEventTwo(_foo); + return true; + } +} diff --git a/tests/contract.py b/tests/contract.py new file mode 100644 index 0000000..24cf148 --- /dev/null +++ b/tests/contract.py @@ -0,0 +1,66 @@ +# standard imports +import os + +# external iports +from chainlib.eth.tx import ( + TxFactory, + TxFormat, + receipt, + ) +from chainlib.eth.contract import ( + ABIContractEncoder, + #ABIContractDecoder, + ABIContractType, + ) +from hexathon import add_0x + +script_dir = os.path.realpath(os.path.dirname(__file__)) +data_dir = script_dir + +class TestContract(TxFactory): + + __abi = None + __bytecode = None + + @staticmethod + def gas(code=None): + return 1000000 + + + @staticmethod + def abi(): + if TestContract.__abi == None: + f = open(os.path.join(data_dir, 'TestContract.json'), 'r') + TestContract.__abi = json.load(f) + f.close() + return TestContract.__abi + + + @staticmethod + def bytecode(): + if TestContract.__bytecode == None: + f = open(os.path.join(data_dir, 'TestContract.bin')) + TestContract.__bytecode = f.read() + f.close() + return TestContract.__bytecode + + + def constructor(self, sender_address, tx_format=TxFormat.JSONRPC, id_generator=None): + code = TestContract.bytecode() + tx = self.template(sender_address, None, use_nonce=True) + tx = self.set_code(tx, code) + return self.finalize(tx, tx_format, id_generator=id_generator) + + + def foo(self, contract_address, sender_address, x, y, tx_format=TxFormat.JSONRPC, id_generator=None): + enc = ABIContractEncoder() + enc.method('foo') + enc.typ(ABIContractType.UINT256) + enc.typ(ABIContractType.BYTES32) + enc.uint256(x) + enc.bytes32(y) + data = add_0x(enc.get()) + tx = self.template(sender_address, contract_address, use_nonce=True) + tx = self.set_code(tx, data) + tx = self.finalize(tx, tx_format, id_generator=id_generator) + return tx diff --git a/tests/test_abi.py b/tests/test_abi.py new file mode 100644 index 0000000..d1e00bc --- /dev/null +++ b/tests/test_abi.py @@ -0,0 +1,29 @@ +from chainlib.eth.contract import ( + ABIContractEncoder, + ABIContractType, + ) + + +def test_abi_param(): + + e = ABIContractEncoder() + e.uint256(42) + e.bytes32('0x666f6f') + e.address('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') + e.method('foo') + e.typ(ABIContractType.UINT256) + e.typ(ABIContractType.BYTES32) + e.typ(ABIContractType.ADDRESS) + + assert e.types[0] == ABIContractType.UINT256 + assert e.types[1] == ABIContractType.BYTES32 + assert e.types[2] == ABIContractType.ADDRESS + assert e.contents[0] == '000000000000000000000000000000000000000000000000000000000000002a' + assert e.contents[1] == '0000000000000000000000000000000000000000000000000000000000666f6f' + assert e.contents[2] == '000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' + + assert e.get() == 'a08f54bb000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000666f6f000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' + + +if __name__ == '__main__': + test_abi_param() diff --git a/tests/test_address.py b/tests/test_address.py new file mode 100644 index 0000000..693f603 --- /dev/null +++ b/tests/test_address.py @@ -0,0 +1,35 @@ +import unittest + +from chainlib.eth.address import ( + is_address, + is_checksum_address, + to_checksum, + ) + +from tests.base import TestBase + + +class TestChain(TestBase): + + def test_chain_spec(self): + checksum_address = '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C' + plain_address = checksum_address.lower() + + self.assertEqual(checksum_address, to_checksum(checksum_address)) + + self.assertTrue(is_address(plain_address)) + self.assertFalse(is_checksum_address(plain_address)) + self.assertTrue(is_checksum_address(checksum_address)) + + self.assertFalse(is_address(plain_address + "00")) + self.assertFalse(is_address(plain_address[:len(plain_address)-2])) + + with self.assertRaises(ValueError): + to_checksum(plain_address + "00") + + with self.assertRaises(ValueError): + to_checksum(plain_address[:len(plain_address)-2]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_bloom.py b/tests/test_bloom.py new file mode 100644 index 0000000..536869d --- /dev/null +++ b/tests/test_bloom.py @@ -0,0 +1,120 @@ +# standard imports +import os +import unittest +import logging + +# local imports +from chainlib.eth.unittest.ethtester import EthTesterCase +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import OverrideGasOracle +from chainlib.eth.tx import receipt +from chainlib.eth.block import block_by_number +from chainlib.eth.log import LogBloom +from hexathon import ( + strip_0x, + add_0x, + ) + +# test imports +from tests.contract import TestContract + +script_dir = os.path.realpath(os.path.dirname(__file__)) + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +#{'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'block_hash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'block_number': '0xc1ee5a', 'contractAddress': None, 'contract_address': None, 'cumulativeGasUsed': '0xbc659', 'cumulative_gas_used': '0xbc659', 'from': '0xf6025e63cee5e436a5f1486e040aeead7e97b745', 'gasUsed': '0x1dddb', 'gas_used': '0x1dddb', 'logs': [ + +#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x', 'logIndex': '0xd', 'removed': False, 'topics': ['0x92e98423f8adac6e64d0608e519fd1cefb861498385c6dee70d58fc926ddc68c', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6', '0x000000000000000000000000f6025e63cee5e436a5f1486e040aeead7e97b745'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'}, + +#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x0000000000000000000000000000000000000000000000000000000060d7119f', 'logIndex': '0xe', 'removed': False, 'topics': ['0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'}, + +#{'address': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'blockHash': '0xe657e31045be85cfff8c28af6b4fd6417cace7150c4ebbeb736e638313d8e66d', 'blockNumber': '0xc1ee5a', 'data': '0x', 'logIndex': '0xf', 'removed': False, 'topics': ['0xfe25c73e3b9089fac37d55c4c7efcba6f04af04cebd2fc4d6d7dbb07e1e5234f', '0x0000000000000000000000000000000000000000000000813b65aa80e5770000'], 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe'}] + +#, 'logsBloom': '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000', 'logs_bloom': '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000', 'status': '0x1', 'to': '0x4e58ab12d2051ea2068e78e4fcee7ddee6785848', 'transactionHash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transaction_hash': '0xd0f039591953d277d55f628694248cb442590fab95ac53fcfb69e9dbba7db97a', 'transactionIndex': '0xe', 'transaction_index': '0xe', 'type': '0x0'} + + +class BloomTestCase(EthTesterCase): + + def setUp(self): + super(BloomTestCase, self).setUp() + + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc) + c = TestContract(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + (tx_hash, o) = c.constructor(self.accounts[0]) + r = self.rpc.do(o) + o = receipt(tx_hash) + r = self.rpc.do(o) + self.assertEqual(r['status'], 1) + + self.address = r['contract_address'] + logg.info('deployed contract on {}'.format(self.address)) + + + def test_log_proof(self): + bloom = LogBloom() + + address = bytes.fromhex(strip_0x('0x4e58ab12d2051ea2068e78e4fcee7ddee6785848')) + logs = [ + ['0x92e98423f8adac6e64d0608e519fd1cefb861498385c6dee70d58fc926ddc68c', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6', '0x000000000000000000000000f6025e63cee5e436a5f1486e040aeead7e97b745'], + ['0x0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f', '0x0000000000000000000000000000000000000000000000000000000005f6aa5a', '0x00000000000000000000000000000000000000000000000000000000000000d6'], + ['0xfe25c73e3b9089fac37d55c4c7efcba6f04af04cebd2fc4d6d7dbb07e1e5234f', '0x0000000000000000000000000000000000000000000000813b65aa80e5770000'], + ] + + bloom.add(address) + for topics in logs: + topics_bytes = [] + for topic in topics: + topic_bytes = bytes.fromhex(strip_0x(topic)) + bloom.add(topic_bytes) + + log_proof_hex = '0x0000000000000000000000000000000000000080000000000000000000c000000000000000408000000000000000000000000000000200080000000000000000100000000000000000000000000000000000000000000200000020000000000000000000000000800000400000000000400000000400000000000400100000000000000000000000000000000000000000000480000000000000000000000000000000000000000000000000008000000000080000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000002008000000000000000' + log_proof = bytes.fromhex(strip_0x(log_proof_hex)) + + log_proof_bitcount = 0 + for b in log_proof: + for i in range(8): + if b & (1 << (7 - i)) > 0: + log_proof_bitcount += 1 + logg.debug('proof log has {} bits set'.format(log_proof_bitcount)) + + log_created_bitcount = 0 + for b in bloom.content: + for i in range(8): + if b & (1 << (7 - i)) > 0: + log_created_bitcount += 1 + logg.debug('created log has {} bits set'.format(log_created_bitcount)) + + logg.debug('log_proof:\n{}'.format(log_proof_hex)) + logg.debug('log_created:\n{}'.format(add_0x(bloom.content.hex()))) + for i in range(len(bloom.content)): + chk = bloom.content[i] & log_proof[i] + if chk != bloom.content[i]: + self.fail('mismatch at {}: {} != {}'.format(i, chk, bloom.content[i])) + + + + @unittest.skip('pyevm tester produces bogus log blooms') + def test_log(self): + nonce_oracle = RPCNonceOracle(self.accounts[0], conn=self.rpc) + gas_oracle = OverrideGasOracle(limit=50000, conn=self.rpc) + c = TestContract(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle) + b = b'\xee' * 32 + (tx_hash, o) = c.foo(self.address, self.accounts[0], 42, b.hex()) + r = self.rpc.do(o) + o = receipt(tx_hash) + rcpt = self.rpc.do(o) + self.assertEqual(rcpt['status'], 1) + + bloom = LogBloom() + topic = rcpt['logs'][0]['topics'][0] + topic = bytes.fromhex(strip_0x(topic)) + address = bytes.fromhex(strip_0x(self.address)) + bloom.add(topic, address) + + o = block_by_number(rcpt['block_number']) + r = self.rpc.do(o) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_event.py b/tests/test_event.py new file mode 100644 index 0000000..9c749fa --- /dev/null +++ b/tests/test_event.py @@ -0,0 +1,38 @@ +# standard imports +import unittest +import logging + +# local imports +from chainlib.eth.unittest.ethtester import EthTesterCase +from chainlib.eth.contract import ( + ABIContractLogDecoder, + ABIContractType, + ) + +logging.basicConfig(level=logging.DEBUG) + + +class TestContractLog(EthTesterCase): + + def test_log(self): + dec = ABIContractLogDecoder() + dec.topic('TestEventOne') + dec.typ(ABIContractType.UINT256) + dec.typ(ABIContractType.BYTES32) + s = dec.get_method_signature() + n = 42 + topics = [ + s, + n.to_bytes(32, byteorder='big'), + ] + data = [ + (b'\xee' * 32), + ] + dec.apply(topics, data) + o = dec.decode() + self.assertEqual(o[0], 42) + self.assertEqual(o[1], data[0].hex()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_nonce.py b/tests/test_nonce.py new file mode 100644 index 0000000..f9332c8 --- /dev/null +++ b/tests/test_nonce.py @@ -0,0 +1,26 @@ +# standard imports +import os +import unittest + +# local imports +from chainlib.eth.address import to_checksum_address +from chainlib.eth.nonce import OverrideNonceOracle +from hexathon import add_0x + +# test imports +from tests.base import TestBase + + +class TestNonce(TestBase): + + def test_nonce(self): + addr_bytes = os.urandom(20) + addr = add_0x(to_checksum_address(addr_bytes.hex())) + n = OverrideNonceOracle(addr, 42) + self.assertEqual(n.get_nonce(), 42) + self.assertEqual(n.next_nonce(), 42) + self.assertEqual(n.next_nonce(), 43) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_sign.py b/tests/test_sign.py new file mode 100644 index 0000000..8175600 --- /dev/null +++ b/tests/test_sign.py @@ -0,0 +1,119 @@ +# standard imports +import os +import socket +import unittest +import unittest.mock +import logging +import json + +# external imports +from crypto_dev_signer.eth.transaction import EIP155Transaction +from crypto_dev_signer.eth.signer.defaultsigner import ReferenceSigner +from crypto_dev_signer.keystore.dict import DictKeystore + +# local imports +import chainlib +from chainlib.eth.connection import EthUnixSignerConnection +from chainlib.eth.sign import sign_transaction +from chainlib.eth.tx import TxFactory +from chainlib.eth.address import to_checksum_address +from chainlib.jsonrpc import ( + jsonrpc_response, + jsonrpc_error, + ) +from hexathon import ( + add_0x, + ) +from chainlib.chain import ChainSpec + +from tests.base import TestBase + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + +keystore = DictKeystore() +alice = keystore.new() +bob = keystore.new() + + +class Mocket(socket.socket): + + req_id = None + error = False + tx = None + signer = None + + def connect(self, v): + return self + + + def send(self, v): + o = json.loads(v) + logg.debug('mocket received {}'.format(v)) + Mocket.req_id = o['id'] + params = o['params'][0] + if to_checksum_address(params.get('from')) != alice: + logg.error('from does not match alice {}'.format(params)) + Mocket.error = True + if to_checksum_address(params.get('to')) != bob: + logg.error('to does not match bob {}'.format(params)) + Mocket.error = True + if not Mocket.error: + Mocket.tx = EIP155Transaction(params, params['nonce'], params['chainId']) + logg.debug('mocket {}'.format(Mocket.tx)) + return len(v) + + + def recv(self, c): + if Mocket.req_id != None: + + o = None + if Mocket.error: + o = jsonrpc_error(Mocket.req_id) + else: + tx = Mocket.tx + r = Mocket.signer.sign_transaction_to_rlp(tx) + Mocket.tx = None + o = jsonrpc_response(Mocket.req_id, add_0x(r.hex())) + Mocket.req_id = None + return json.dumps(o).encode('utf-8') + + return b'' + + +class TestSign(TestBase): + + + def setUp(self): + super(TestSign, self).__init__() + self.chain_spec = ChainSpec('evm', 'foo', 42) + + + logg.debug('alice {}'.format(alice)) + logg.debug('bob {}'.format(bob)) + + self.signer = ReferenceSigner(keystore) + + Mocket.signer = self.signer + + + def test_sign_build(self): + with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m: + rpc = EthUnixSignerConnection('foo', chain_spec=self.chain_spec) + f = TxFactory(self.chain_spec, signer=rpc) + tx = f.template(alice, bob, use_nonce=True) + tx = f.build(tx) + logg.debug('tx result {}'.format(tx)) + + + def test_sign_rpc(self): + with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m: + rpc = EthUnixSignerConnection('foo') + f = TxFactory(self.chain_spec, signer=rpc) + tx = f.template(alice, bob, use_nonce=True) + tx_o = sign_transaction(tx) + rpc.do(tx_o) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_stat.py b/tests/test_stat.py new file mode 100644 index 0000000..a47317b --- /dev/null +++ b/tests/test_stat.py @@ -0,0 +1,49 @@ +# standard imports +import unittest +import datetime + +# external imports +from chainlib.stat import ChainStat +from chainlib.eth.block import Block + + +class TestStat(unittest.TestCase): + + def test_block(self): + + s = ChainStat() + + d = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) + block_a = Block({ + 'timestamp': d.timestamp(), + 'hash': None, + 'transactions': [], + 'number': 41, + }) + + d = datetime.datetime.utcnow() + block_b = Block({ + 'timestamp': d.timestamp(), + 'hash': None, + 'transactions': [], + 'number': 42, + }) + + s.block_apply(block_a) + s.block_apply(block_b) + self.assertEqual(s.block_average(), 30.0) + + d = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) + block_c = Block({ + 'timestamp': d.timestamp(), + 'hash': None, + 'transactions': [], + 'number': 43, + }) + + s.block_apply(block_c) + self.assertEqual(s.block_average(), 20.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tx.py b/tests/test_tx.py new file mode 100644 index 0000000..ca976b9 --- /dev/null +++ b/tests/test_tx.py @@ -0,0 +1,79 @@ +# standard imports +import os +import unittest +import logging + +# local imports +from chainlib.eth.unittest.ethtester import EthTesterCase +from chainlib.eth.nonce import RPCNonceOracle +from chainlib.eth.gas import ( + RPCGasOracle, + Gas, + ) +from chainlib.eth.tx import ( + unpack, + pack, + raw, + transaction, + TxFormat, + TxFactory, + Tx, + ) +from chainlib.eth.contract import ( + ABIContractEncoder, + ABIContractType, + ) +from chainlib.eth.address import to_checksum_address +from hexathon import ( + strip_0x, + add_0x, + ) + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + + +class TxTestCase(EthTesterCase): + + def test_tx_reciprocal(self): + nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc) + gas_oracle = RPCGasOracle(self.rpc) + c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec) + (tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED) + tx = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec) + self.assertEqual(tx['from'], self.accounts[0]) + self.assertEqual(tx['to'], self.accounts[1]) + + + def test_tx_pack(self): + nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc) + gas_oracle = RPCGasOracle(self.rpc) + + mock_contract = to_checksum_address(add_0x(os.urandom(20).hex())) + + f = TxFactory(self.chain_spec, signer=self.rpc) + enc = ABIContractEncoder() + enc.method('fooMethod') + enc.typ(ABIContractType.UINT256) + enc.uint256(13) + data = enc.get() + tx = f.template(self.accounts[0], mock_contract, use_nonce=True) + tx = f.set_code(tx, data) + (tx_hash, tx_signed_raw_hex) = f.finalize(tx, TxFormat.RLP_SIGNED) + logg.debug('tx result {}'.format(tx)) + o = raw(tx_signed_raw_hex) + r = self.rpc.do(o) + o = transaction(tx_hash) + tx_rpc_src = self.rpc.do(o) + logg.debug('rpc src {}'.format(tx_rpc_src)) + + tx_signed_raw_bytes = bytes.fromhex(strip_0x(tx_signed_raw_hex)) + tx_src = unpack(tx_signed_raw_bytes, self.chain_spec) + txo = Tx(tx_src) + tx_signed_raw_bytes_recovered = pack(txo, self.chain_spec) + logg.debug('o {}'.format(tx_signed_raw_bytes.hex())) + logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex())) + self.assertEqual(tx_signed_raw_bytes, tx_signed_raw_bytes_recovered) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c b/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c new file mode 100644 index 0000000..2b843ec --- /dev/null +++ b/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c @@ -0,0 +1 @@ +{"address":"eb3907ecad74a0013c259d5874ae7f22dcbcc95c","crypto":{"cipher":"aes-128-ctr","ciphertext":"b0f70a8af4071faff2267374e2423cbc7a71012096fd2215866d8de7445cc215","cipherparams":{"iv":"9ac89383a7793226446dcb7e1b45cdf3"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"299f7b5df1d08a0a7b7f9c9eb44fe4798683b78da3513fcf9603fd913ab3336f"},"mac":"6f4ed36c11345a9a48353cd2f93f1f92958c96df15f3112a192bc994250e8d03"},"id":"61a9dd88-24a9-495c-9a51-152bd1bfaa5b","version":3} \ No newline at end of file