commit f8afc172c6437eb578f248de0f7febde59f817cb
Author: nolash <dev@holbrook.no>
Date: Mon, 28 Jun 2021 07:48:36 +0200
Initial commit; receive all eth code from chainlib package
Diffstat:
59 files changed, 4537 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+__pycache__
+gmon.out
+*.pyc
+dist/
+build/
+*.egg-info
diff --git a/CHANGELOG 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
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <https://www.gnu.org/licenses/>.
+
+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:
+
+ <program> Copyright (C) <year> <name of author>
+ 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
+<https://www.gnu.org/licenses/>.
+
+ 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
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/MANIFEST.in b/MANIFEST.in
@@ -0,0 +1 @@
+include *requirements.txt LICENSE
diff --git a/README.md 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py
@@ -0,0 +1,102 @@
+#!python3
+
+"""Token balance query script
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -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
@@ -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
@@ -0,0 +1,65 @@
+#!python3
+
+"""Decode raw transaction
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -0,0 +1,180 @@
+#!python3
+
+"""Gas transfer script
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -0,0 +1,158 @@
+#!python3
+
+"""Data retrieval script
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -0,0 +1,170 @@
+#!python3
+
+"""Token balance query script
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -0,0 +1,189 @@
+#!python3
+
+"""Gas transfer script
+
+.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
+.. 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
+608060405234801561001057600080fd5b50610260806100206000396000f3fe608060405234801561001057600080fd5b5060043610610048576000357c01000000000000000000000000000000000000000000000000000000009004806333aa24121461004d575b600080fd5b61006760048036038101906100629190610122565b61007d565b604051610074919061018b565b60405180910390f35b6000827fa8ed44a382304c8db2c9059e4de342080401fb8e9a71986396d595c869fa3792836040516100af91906101a6565b60405180910390a27f33c4ea6ccc21f1a14e9de7326edcc52c2b8a302ac875ae6b8fee2560eadd75ad836040516100e691906101c1565b60405180910390a16001905092915050565b600081359050610107816101fc565b92915050565b60008135905061011c81610213565b92915050565b6000806040838503121561013557600080fd5b60006101438582860161010d565b9250506020610154858286016100f8565b9150509250929050565b610167816101dc565b82525050565b610176816101e8565b82525050565b610185816101f2565b82525050565b60006020820190506101a0600083018461015e565b92915050565b60006020820190506101bb600083018461016d565b92915050565b60006020820190506101d6600083018461017c565b92915050565b60008115159050919050565b6000819050919050565b6000819050919050565b610205816101e8565b811461021057600080fd5b50565b61021c816101f2565b811461022757600080fd5b5056fea26469706673582212205c428504ca5a53a9250d76b287bec8ee4b13d5c7cb386a0f67cbf77ecb96a9ea64736f6c63430008040033
+\ No newline at end of file
diff --git a/tests/TestContract.sol 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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