├── .flake8 ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature-request.md │ └── security-issue.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTORS.md ├── LICENSE ├── README.rst ├── fasteth ├── __init__.py ├── exceptions.py ├── models.py ├── py.typed ├── rpc.py ├── types.py └── utils.py ├── poetry.lock ├── pyproject.toml └── tests └── test_fasteth.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, W503 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Catch all for unspecified items 2 | # This is listed first as CODEOWNERS is chained bottom to top 3 | * 0xAlcibiades 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [0xAlcibiades, sephynox] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create an issue regarding a bug 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | Problem Description 10 | ------------------- 11 | ``` 12 | 13 | ``` 14 | 15 | Steps to Duplicate 16 | ------------------ 17 | ``` 18 | 19 | ``` 20 | 21 | Expected Results 22 | ---------------- 23 | ``` 24 | 25 | ``` 26 | 27 | Actual Results 28 | -------------- 29 | ``` 30 | 31 | ``` 32 | 33 | Relevant logs 34 | ------------- 35 | ``` 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Request a new feature 4 | title: '' 5 | labels: 'feature' 6 | assignees: '' 7 | --- 8 | 9 | Is your feature request related to a problem? 10 | --------------------------------------------- 11 | ``` 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | ``` 14 | 15 | Describe the solution you'd like 16 | -------------------------------- 17 | ``` 18 | A clear and concise description of what you want to happen. 19 | ``` 20 | 21 | Describe alternatives you've considered 22 | --------------------------------------- 23 | ``` 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | ``` 26 | 27 | Additional context 28 | ------------------ 29 | ``` 30 | Add any other context or screenshots about the feature request here. 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security Issue 3 | about: Create an issue regarding security holes or bugs 4 | title: '' 5 | labels: 'security' 6 | assignees: '' 7 | --- 8 | 9 | What security issue have you noticed? 10 | ------------------------------------- 11 | ``` 12 | 13 | ``` 14 | 15 | Relevant logs 16 | ------------- 17 | ``` 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Pull Request 2 | ============ 3 | What type of Pull Request is this? 4 | - [ ] Bug Report 5 | - [ ] Feature Request 6 | - [ ] Security Issue 7 | 8 | Related Issue: #123456 9 | 10 | Short Description of original issue 11 | ----------------------------------- 12 | ``` 13 | 14 | ``` 15 | 16 | How this PR resolves it 17 | ----------------------- 18 | ``` 19 | 20 | ``` 21 | 22 | Related Testing 23 | --------------- 24 | ``` 25 | 26 | ``` 27 | 28 | List of changes 29 | --------------- 30 | ``` 31 | 32 | ``` 33 | 34 | Additional Notes 35 | ---------------- 36 | ``` 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests' 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | #---------------------------------------------- 10 | # check-out repo and set-up python 11 | #---------------------------------------------- 12 | - name: Check out repository 13 | uses: actions/checkout@v2 14 | - name: Set up python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: 3.9 18 | architecture: x64 19 | #---------------------------------------------- 20 | # Setup node & ganache-cli 21 | #---------------------------------------------- 22 | - uses: actions/setup-node@v1 23 | - name: Install Ganache CLI 24 | run: npm install -g ganache-cli 25 | - name: Run Ganache CLI in Background 26 | run: ganache-cli --networkId 1337 & 27 | #---------------------------------------------- 28 | # ----- install & configure poetry ----- 29 | #---------------------------------------------- 30 | - name: Install Poetry 31 | uses: snok/install-poetry@v1.1.2 32 | with: 33 | virtualenvs-create: true 34 | virtualenvs-in-project: true 35 | #---------------------------------------------- 36 | # load cached venv if cache exists 37 | #---------------------------------------------- 38 | - name: Load cached venv 39 | id: cached-poetry-dependencies 40 | uses: actions/cache@v2 41 | with: 42 | path: .venv 43 | key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} 44 | #---------------------------------------------- 45 | # install dependencies if cache does not exist 46 | #---------------------------------------------- 47 | - name: Install dependencies 48 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 49 | run: poetry install --no-interaction --no-root 50 | #---------------------------------------------- 51 | # install your root project, if required 52 | #---------------------------------------------- 53 | - name: Install library 54 | run: poetry install --no-interaction 55 | #---------------------------------------------- 56 | # run test suite 57 | #---------------------------------------------- 58 | - run: poetry run pre-commit install --hook-type pre-push 59 | - run: poetry run pre-commit run --all-files --hook-stage push 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | .vscode/ 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # Intellij 142 | .idea/ 143 | 144 | #################### 145 | ## Python detritus 146 | #################### 147 | 148 | # Virtual environments 149 | pyenv/ 150 | 151 | ################ 152 | ## Mac detritus 153 | ################ 154 | 155 | .DS_Store 156 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 19.10b0 6 | hooks: 7 | - id: black 8 | - repo: https://github.com/pre-commit/mirrors-isort 9 | rev: v5.8.0 10 | hooks: 11 | - id: isort 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v2.3.0 14 | hooks: 15 | - id: end-of-file-fixer 16 | exclude: ^tests/sample_outputs 17 | - id: trailing-whitespace 18 | name: Trim Trailing Whitespace 19 | description: This hook trims trailing whitespace. 20 | entry: trailing-whitespace-fixer 21 | language: python 22 | types: [text] 23 | - id: flake8 24 | language_version: python3 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: v0.812 27 | hooks: 28 | - id: mypy 29 | - repo: local 30 | hooks: 31 | - id: tests 32 | name: Run PyTest Test Cases 33 | entry: pytest -v tests/ 34 | language: python 35 | types: [python] 36 | stages: [push] 37 | pass_filenames: false 38 | language_version: python3 39 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | fasteth Contributors 2 | ==================== 3 | fasteth was build with love by the following contributors: 4 | 5 | 1. [0xAlcibiades](https://alcibiades.capital) 6 | 2. [Tanveer Wahid](https://github.com/sephynox) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. role:: bash(code) 2 | :language: bash 3 | 4 | ####### 5 | fasteth 6 | ####### 7 | 8 | .. image:: https://github.com/Alcibiades-Capital/fasteth/workflows/Tests/badge.svg 9 | :target: https://github.com/Alcibiades-Capital/fasteth/actions?workflow=Tests 10 | 11 | ******** 12 | Abstract 13 | ******** 14 | 15 | :bash:`fasteth` is an asynchronous python abstraction layer for the Ethereum 16 | JSON RPC. It biases towards using native python types and dataclasses to represent 17 | Ethereum data, whilst remaining lightweight and making minimal assumptions. 18 | 19 | ***** 20 | Goals 21 | ***** 22 | 23 | The motivation for creating this is to provide fast, asynchronous, native 24 | python, access to the Ethereum JSON RPC. Extant python libraries are synchronous and 25 | provide additional abstraction which may not always be needed. This library favors 26 | speed and asynchronicity. 27 | 28 | ****** 29 | Status 30 | ****** 31 | 32 | This project is still a work in progress. 33 | 34 | *************** 35 | Further Reading 36 | *************** 37 | 38 | TODO(These should be links) 39 | 40 | Quickstart 41 | ========== 42 | 43 | This project aims to make it easy to make async requests and get responses back from the 44 | Ethereum JSON RPC. Here is a simple example: 45 | 46 | .. code-block:: bash 47 | 48 | pip install fasteth 49 | 50 | .. code-block:: python 51 | 52 | import asyncio 53 | from fasteth import AsyncEthereumJSONRPC 54 | 55 | async def do_requests(): 56 | async with AsyncEthereumJSONRPC() as rpc: 57 | print(await rpc.network_version()) 58 | 59 | asyncio.run(do_requests()) 60 | 61 | See the :bash:`fastapi` docs for a 62 | `great explanation `_ of 63 | async/concurrency. As an aside, :bash:`fastapi` was the inspiration for the name 64 | :bash:`fasteth`. 65 | 66 | 67 | Getting Involved 68 | ================ 69 | 70 | PR your changes :). 71 | 72 | Developer Guide 73 | --------------- 74 | 75 | You'll need poetry. 76 | 77 | .. code-block:: bash 78 | 79 | poetry install 80 | poetry shell 81 | 82 | You'll also need a running instance of :bash:`ganache-cli` and in should be started 83 | with the argument :bash:`--networkId 1337` 84 | 85 | Running the pre-commit hooks on demand 86 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 87 | 88 | Initial setup: 89 | 90 | .. code-block:: bash 91 | 92 | pre-commit install 93 | pre-commit install --hook-type pre-push 94 | 95 | Then: 96 | 97 | .. code-block:: bash 98 | 99 | pre-commit run --all-files 100 | 101 | This will run: 102 | 103 | During commit 104 | 105 | * Check if there are merge conflicts. 106 | * Check if there are debug statements, we don't want those in checked in code 107 | * Lint with :bash:`flake8`. 108 | * Use :bash:`black` to format the code, modifies files in-place if the code in the 109 | changeset is not already black compliant and fails the hook. 110 | 111 | During push 112 | 113 | * All of the above runs also :bash:`pytest` with verbose flag 114 | (if any python files have changed). 115 | 116 | Building the PyPI package 117 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 118 | 119 | .. code-block:: bash 120 | 121 | poetry build 122 | 123 | It's that simple. 124 | -------------------------------------------------------------------------------- /fasteth/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | 3 | from .rpc import AsyncEthereumJSONRPC # noqa: F401 4 | -------------------------------------------------------------------------------- /fasteth/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for fasteth.""" 2 | 3 | 4 | class JSONRPCError(Exception): 5 | """An ethereum JSON RPC error.""" 6 | 7 | pass 8 | 9 | 10 | class ParseError(JSONRPCError): 11 | """Invalid JSON was received by the server. 12 | 13 | An error occurred on the server while parsing the JSON text. 14 | """ 15 | 16 | pass 17 | 18 | 19 | class InvalidRequest(JSONRPCError): 20 | """The JSON sent is not a valid Request object.""" 21 | 22 | pass 23 | 24 | 25 | class MethodNotFound(JSONRPCError): 26 | """The method does not exist/is not available.""" 27 | 28 | pass 29 | 30 | 31 | class InvalidParams(JSONRPCError): 32 | """Invalid method parameter(s).""" 33 | 34 | pass 35 | 36 | 37 | class InternalError(JSONRPCError): 38 | """Internal JSON-RPC error.""" 39 | 40 | pass 41 | 42 | 43 | class ServerError(JSONRPCError): 44 | """Reserved for implementation-defined server-errors.""" 45 | 46 | 47 | class EthereumRPCError(JSONRPCError): 48 | """A generic ethereum RPC error.""" 49 | 50 | pass 51 | 52 | 53 | class UnauthorizedError(EthereumRPCError): 54 | """Action is not authorized.""" 55 | 56 | pass 57 | 58 | 59 | class ActionNotAllowed(EthereumRPCError): 60 | """Should be used when some action is not allowed, e.g. preventing 61 | an action, while another depending action is processing on, like 62 | sending again when a confirmation popup is shown to the user 63 | """ 64 | 65 | pass 66 | 67 | 68 | class ExecutionError(EthereumRPCError): 69 | """Will contain a subset of custom errors in the data field. """ 70 | 71 | pass 72 | 73 | 74 | class NotFound(ExecutionError): 75 | """Something which should be in response is not found.""" 76 | 77 | pass 78 | 79 | 80 | class RequiresEther(ExecutionError): 81 | """Action requires a value of ether.""" 82 | 83 | pass 84 | 85 | 86 | class GasTooLow(ExecutionError): 87 | """The gas value provided is too low.""" 88 | 89 | pass 90 | 91 | 92 | class GasLimitExceeded(ExecutionError): 93 | """The gas limit has been exceeded.""" 94 | 95 | pass 96 | 97 | 98 | class Rejected(ExecutionError): 99 | """Action rejected because of contents.""" 100 | 101 | pass 102 | 103 | 104 | class EtherTooLow(ExecutionError): 105 | """The provided ether value was too low.""" 106 | 107 | pass 108 | -------------------------------------------------------------------------------- /fasteth/models.py: -------------------------------------------------------------------------------- 1 | """Dataclasses for fasteth data types.""" 2 | from abc import ABC, abstractmethod 3 | from datetime import datetime 4 | from enum import Enum 5 | from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar, Union 6 | 7 | from pydantic import BaseModel, Field 8 | 9 | from fasteth import exceptions as eth_exp 10 | from fasteth import types as eth_types 11 | from fasteth import utils 12 | 13 | 14 | class Network(int, Enum): 15 | """An enum representing the ethereum network id.""" 16 | 17 | Mainnet = 1 18 | Morden = 2 19 | Ropsten = 3 20 | Rinkeby = 4 21 | Kovan = 42 22 | Ganache = 1337 23 | 24 | 25 | class RPCSchema(tuple, Enum): 26 | """An enum representing method and id mappings.""" 27 | 28 | client_version = ("web3_clientVersion", 67) 29 | sha3 = ("web3_sha3", 64) 30 | network_version = ("net_version", 67) 31 | network_listening = ("net_listening", 67) 32 | network_peer_count = ("net_peerCount", 74) 33 | protocol_version = ("eth_protocolVersion", 67) 34 | syncing = ("eth_syncing", 1) 35 | coinbase = ("eth_coinbase", 64) 36 | mining = ("eth_mining", 71) 37 | hashrate = ("eth_hashrate", 71) 38 | gas_price = ("eth_gasPrice", 73) 39 | accounts = ("eth_accounts", 1) 40 | block_number = ("eth_blockNumber", 83) 41 | get_balance = ("eth_getBalance", 1) 42 | get_storage_at = ("eth_getStorageAt", 1) 43 | get_transaction_count = ("eth_getTransactionCount", 1) 44 | get_block_by_number = ("eth_getBlockByNumber", 1) 45 | get_block_by_hash = ("eth_getBlockByHash", 1) 46 | get_block_transaction_count_by_hash = ("eth_getBlockTransactionCountByHash", 1) 47 | get_block_transaction_count_by_number = ("eth_getBlockTransactionCountByNumber", 1) 48 | get_uncle_count_by_block_hash = ("eth_getUncleCountByBlockHash", 1) 49 | get_uncle_count_by_block_number = ("eth_getUncleCountByBlockNumber", 1) 50 | get_shh_messages = ("shh_getMessages", 73) 51 | get_shh_filter_changes = ("shh_getFilterChanges", 73) 52 | get_code = ("eth_getCode", 1) 53 | submit_hashrate = ("eth_submitHashrate", 73) 54 | sign = ("eth_sign", 1) 55 | sign_transaction = ("eth_signTransaction", 1) 56 | send_transaction = ("eth_sendTransaction", 1) 57 | send_raw_transaction = ("eth_sendRawTransaction", 1337) 58 | call = ("eth_call", 1) 59 | estimate_gas = ("eth_estimateGas", 1) 60 | shh_version = ("shh_version", 73) 61 | shh_post = ("shh_post", 73) 62 | shh_new_identity = ("shh_newIdentity", 73) 63 | shh_has_identity = ("shh_hasIdentity", 73) 64 | shh_new_group = ("shh_newGroup", 73) 65 | shh_add_to_group = ("shh_addToGroup", 73) 66 | shh_new_filter = ("shh_newFilter", 73) 67 | shh_uninstall_filter = ("shh_uninstallFilter", 73) 68 | 69 | 70 | class Ethable(ABC, BaseModel): 71 | """Abstract base class for classes presenting an explicit conversion for eth RPC 72 | """ 73 | 74 | @abstractmethod 75 | def dict(self) -> Dict: 76 | """Returns a dict for submission in an RPC request. 77 | 78 | :returns 79 | dict: The RPC request data. 80 | """ 81 | pass 82 | 83 | @staticmethod 84 | @abstractmethod 85 | def parse_obj(data: Dict) -> Any: 86 | """Returns the data decoded to this type.""" 87 | pass 88 | 89 | 90 | # Create a generic variable that can be 'AutoEthable', or any subclass. 91 | T = TypeVar("T", bound=Ethable) 92 | FROM_KEY = "from_address" 93 | FROM = "from" 94 | 95 | 96 | def iterate_list(model_type: Type[T], data: List): 97 | """Returns the elements of the data converted to model_type in a new list.""" 98 | return [model_type.parse_obj(v) for v in data] 99 | 100 | 101 | class AutoEthable(Ethable): 102 | """Provides encode and decode functionality without explicit conversion. 103 | 104 | Using AutoEthable vs declaring explicit dict and parse_obj methods incurs 105 | a 30% overhead overall, and a ~2x overhead in the inter-quartile range. 106 | 107 | Use with that in mind. 108 | """ 109 | 110 | def dict(self: Type[T]) -> Dict: 111 | """Returns a dict for submission in an RPC request. 112 | 113 | :returns 114 | dict: The RPC request data. 115 | """ 116 | # Dictionary for eth RPC Request JSON 117 | r: Dict = {} 118 | 119 | for k, t in self.__annotations__.items(): 120 | v = getattr(self, k) 121 | # Workaround python built-in 122 | if v is None: 123 | continue 124 | 125 | if k == FROM_KEY: 126 | r[FROM] = v 127 | k = FROM 128 | 129 | if t in utils.to_eth_converters: 130 | # Check if this is a simple type with a converter. 131 | r[k] = utils.to_eth_converters[t](v) 132 | elif issubclass(type(t), Ethable): 133 | r[k] = t.dict(v) 134 | elif hasattr(t, "__args__") and t == List[t.__args__[0]]: # type: ignore 135 | if t.__args__[0] in utils.to_eth_converters: 136 | # Map the converter to each member of the list 137 | r[k] = [utils.to_eth_converters[t.__args__[0]](x) for x in v] 138 | elif issubclass(t.__args__[0], Ethable): 139 | # This is an ethable type, recurse encoder. 140 | r[k] = [t.__args__[0].dict(x) for x in v] 141 | else: 142 | r[k] = v 143 | 144 | return r 145 | 146 | @classmethod 147 | def parse_obj(cls: Type[T], data: Dict) -> T: 148 | """Returns python typed object from ethereum typed object. 149 | 150 | This will mutate data, so used data.copy() to avoid as needed. 151 | TODO(consider making this input not mutate, it's bug prone) 152 | 153 | :type data: dict 154 | :param data: The dictionary of data to populate the dataclass with. 155 | """ 156 | if FROM in data: 157 | data[FROM_KEY] = data.pop(FROM) 158 | 159 | for k, t in cls.__annotations__.items(): 160 | # We only need to access the value once. 161 | v = data.get(k) 162 | 163 | if v is None: 164 | # Then this is either an optional or non-existent value in input. 165 | continue 166 | 167 | if t in utils.to_py_converters: 168 | # Check if the type has an eth encoder, encode if so. 169 | data[k] = utils.to_py_converters[t](v) 170 | elif issubclass(type(t), Ethable): 171 | data[k] = t.parse_obj(v) 172 | elif hasattr(t, "__args__") and t == List[t.__args__[0]]: # type: ignore 173 | # A list of non-Ethable types. 174 | if t.__args__[0] in utils.to_py_converters: 175 | # Map the converter to each member of the list 176 | data[k] = [utils.to_py_converters[t.__args__[0]](x) for x in v] 177 | elif issubclass(t.__args__[0], Ethable): 178 | # This is an ethable type, recurse decoder. 179 | data[k] = [t.__args__[0].parse_obj(x) for x in v] 180 | else: 181 | data[k] = v 182 | 183 | return cls(**data) 184 | 185 | 186 | # noinspection PyUnresolvedReferences 187 | class JSONRPCRequest(BaseModel): 188 | """Model for JSON RPC Request. 189 | 190 | Attributes: 191 | jsonrpc: A String specifying the version of the JSON-RPC protocol. 192 | MUST be exactly "2.0". 193 | method: A String containing the name of the method to be invoked. Method names 194 | that begin with the word rpc followed by a period character 195 | (U+002E or ASCII 46) are reserved for rpc-internal methods and 196 | extensions and MUST NOT be used for anything else. 197 | params: A Structured value that holds the parameter values to be used during 198 | the invocation of the method. This member MAY be omitted. 199 | id: An identifier established by the Client that MUST contain a String, Number, 200 | or None value if included. If it is not included it is assumed to be a 201 | notification. The value SHOULD normally not be None and Numbers 202 | SHOULD NOT contain fractional parts. 203 | 204 | The Server MUST reply with the same value in the Response object if included. 205 | This member is used to correlate the context between the two objects. 206 | 207 | The use of None as a value for the id member in a Request object is discouraged, 208 | because this specification uses a value of None for Responses with an unknown id. 209 | Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could 210 | cause confusion in handling. 211 | 212 | Fractional parts may be problematic, since many decimal fractions cannot be 213 | represented exactly as binary fractions. 214 | """ 215 | 216 | jsonrpc: str = "2.0" 217 | method: str 218 | params: List = Field(default_factory=list) 219 | id: int 220 | 221 | 222 | class EthereumErrorData(BaseModel): 223 | # TODO(Break out handling logic here.) 224 | code: int 225 | message: str 226 | 227 | 228 | # noinspection PyUnresolvedReferences 229 | class JSONRPCErrorData(BaseModel): 230 | """RPC Call Error Model. 231 | 232 | Attributes: 233 | code: A Number that indicates the error type that occurred. 234 | This MUST be an integer. 235 | message: A String providing a short description of the error. 236 | The message SHOULD be limited to a concise single sentence. 237 | data: A Primitive or Structured value that contains additional information 238 | about the error. This may be omitted. The value of this member is 239 | defined by the Server (e.g. detailed error information, nested 240 | errors etc.). 241 | 242 | The error codes from and including -32768 to -32000 are reserved for 243 | pre-defined errors. Any code within this range, but not defined explicitly 244 | below is reserved for future use. The error codes are nearly the same as those 245 | suggested for XML-RPC at the following url: 246 | http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php 247 | 248 | code message meaning 249 | -32700 Parse error Invalid JSON was received by the server. 250 | An error occurred on the server while parsing 251 | the JSON text. 252 | -32600 Invalid Request The JSON sent is not a valid Request object. 253 | -32601 Method not found The method does not exist / is not available. 254 | -32602 Invalid params Invalid method parameter(s). 255 | -32603 Internal error Internal JSON-RPC error. 256 | -32000 257 | to 258 | -32099 Server error Reserved for implementation-defined server-errors. 259 | 260 | The remainder of the space is available for application defined errors. 261 | """ 262 | 263 | code: int 264 | message: str 265 | data: Optional[Union[Dict, List, List[EthereumErrorData]]] 266 | _exp: ClassVar = { 267 | -32700: eth_exp.ParseError, 268 | -32600: eth_exp.InvalidRequest, 269 | -32601: eth_exp.MethodNotFound, 270 | -32602: eth_exp.InvalidParams, 271 | -32603: eth_exp.InternalError, 272 | 1: eth_exp.UnauthorizedError, 273 | 2: eth_exp.ActionNotAllowed, 274 | } 275 | _eth_error: ClassVar = { 276 | 100: eth_exp.NotFound, 277 | 101: eth_exp.RequiresEther, 278 | 102: eth_exp.GasTooLow, 279 | 103: eth_exp.GasLimitExceeded, 280 | 104: eth_exp.Rejected, 281 | 105: eth_exp.EtherTooLow, 282 | } 283 | 284 | def raise_for_error(self): 285 | if self.code in self._exp: 286 | if self.code == 3: 287 | # TODO(Consider raising multiple exceptions here for each error 288 | # in the list of errors) 289 | for elem in self.data: 290 | raise self._eth_error[elem.code](elem.message) 291 | else: 292 | raise self._exp[self.code](self.message) 293 | elif self.code in range(-32099, -32000): 294 | raise eth_exp.ServerError 295 | else: 296 | # Raise the generic error. 297 | raise eth_exp.JSONRPCError 298 | 299 | 300 | # noinspection PyUnresolvedReferences 301 | class JSONRPCResponse(BaseModel): 302 | """Model for JSON RPC response. 303 | 304 | Attributes: 305 | id: This member is REQUIRED. It MUST be the same as the value of the id 306 | member in the JSONRPCRequest Object. If there was an error in detecting 307 | the id in the Request object (e.g. Parse error/Invalid Request), it MUST 308 | be None. 309 | jsonrpc: A String specifying the version of the JSON-RPC protocol. 310 | MUST be exactly "2.0". 311 | result: This member is REQUIRED on success. This member MUST NOT exist if 312 | there was an error invoking the method. The value of this member is 313 | determined by the method invoked on the Server. 314 | error: This member is REQUIRED on error. This member MUST NOT exist if 315 | there was no error triggered during invocation. The value for this 316 | member MUST be an Object. 317 | # TODO(Add result and error types according to json rpc spec) 318 | # https://www.jsonrpc.org/specification 319 | """ 320 | 321 | id: Optional[int] = None 322 | jsonrpc: str = "2.0" 323 | error: Optional[JSONRPCErrorData] = None 324 | result: Optional[Union[Dict, List, eth_types.HexStr, bool]] = None 325 | 326 | 327 | # noinspection PyUnresolvedReferences 328 | class SyncStatus(AutoEthable): 329 | """Model representing node sync status. 330 | 331 | Attributes: 332 | startingBlock (int): The starting block for the sync in progress. 333 | currentBlock (int): The current block for the sync in progress. 334 | currentBlock (int): The current block for the sync in progress. 335 | """ 336 | 337 | startingBlock: Optional[int] = None 338 | currentBlock: Optional[int] = None 339 | highestBlock: Optional[int] = None 340 | syncing: bool = False 341 | 342 | 343 | # noinspection PyUnresolvedReferences 344 | class Transaction(AutoEthable): 345 | """The transaction object. 346 | 347 | Attributes: 348 | from_address (eth_types.Address): The address the transaction is sent from. This 349 | is sent to the RPC as 'from'. 350 | to (eth_types.Address): (optional when creating new contract) The address the 351 | transaction is directed to. 352 | gas (int): Integer of the gas provided for the transaction. It will return 353 | unused gas. 354 | gasPrice (int): Integer of the gasPrice used for each paid gas, in Wei. 355 | value (int): Integer of the value sent with this transaction, in Wei. 356 | data (eth_types.Data): The compiled code of a contract OR the hash of the 357 | invoked method signature and encoded parameters. See 358 | the Ethereum Contract ABI specification for more detail. 359 | nonce (int): This allows to overwrite your own pending transactions that use 360 | the same nonce. 361 | 362 | """ 363 | 364 | from_address: eth_types.HexAddress 365 | data: Optional[eth_types.Data] = None 366 | to: Optional[eth_types.HexAddress] = None 367 | gas: Optional[int] = None 368 | gasPrice: Optional[int] = None 369 | value: Optional[int] = None 370 | nonce: Optional[int] = None 371 | hash: Optional[eth_types.Hash32] = None 372 | input: Optional[bytes] = None 373 | transactionIndex: Optional[int] = None 374 | blockHash: Optional[eth_types.Hash32] = None 375 | blockNumber: Optional[int] = None 376 | type: Optional[int] = None # This is not in the spec but exists in the return 377 | v: Optional[int] = None 378 | r: Optional[eth_types.Signature] = None 379 | s: Optional[eth_types.Signature] = None 380 | 381 | 382 | class Block(AutoEthable): 383 | # noinspection PyUnresolvedReferences 384 | """The block object. 385 | 386 | Attributes: 387 | number (int): The block number. None when its pending block. 388 | hash (Union[eth_types.Hash32, None]): 32 Bytes - hash of the block. 389 | None when its pending block. 390 | parentHash (eth_types.Hash32): 32 Bytes - hash of the parent block. 391 | nonce (Union[eth_types.Data, None]): 8 Bytes - hash of the generated 392 | proof-of-work. None when its pending block. 393 | sha3Uncles (eth_types.Hash32): 32 Bytes - SHA3 of the uncles data in the 394 | block. 395 | logsBloom (eth_types.LogsBloom): 256 Bytes - the bloom filter for the logs 396 | of the block. null when its pending block. 397 | transactionsRoot (eth_types.Hash32): 32 Bytes - the root of the transaction 398 | trie of the block. 399 | stateRoot (eth_types.Hash32): 32 Bytes - the root of the final state trie 400 | of the block. 401 | receiptsRoot (eth_types.Hash32): 32 Bytes - the root of the receipts trie of 402 | the block. 403 | miner (eth_types.Address): 20 Bytes - the address of the beneficiary to 404 | whom the mining rewards were given. 405 | difficulty (int): The difficulty for this block. 406 | totalDifficulty (int): The total difficulty of the chain until this block. 407 | extraData (eth_types.Data): The “extra data” field of this block. 408 | size (int): The size of this block in bytes. 409 | gasLimit (int): The maximum gas allowed in this block. 410 | gasUsed (int): The total used gas by all transactions in this block. 411 | timestamp (Arrow): Time for when the block was collated. 412 | transactions (List[Union[Transaction, eth_types.Hash32]): List of transaction 413 | objects, or 32 Bytes transaction hashes depending on the last given 414 | parameter. 415 | uncles (List[eth_types.Hash32]): List of uncle hashes. 416 | """ 417 | 418 | logsBloom: Optional[eth_types.LogsBloom] = None 419 | number: Optional[int] = None 420 | hash: Optional[eth_types.Hash32] = None 421 | nonce: Optional[int] = None 422 | parentHash: eth_types.Hash32 423 | sha3Uncles: eth_types.Hash32 424 | mixHash: eth_types.Hash32 425 | transactionsRoot: eth_types.Hash32 426 | stateRoot: eth_types.Hash32 427 | receiptsRoot: eth_types.Hash32 428 | miner: eth_types.Address 429 | difficulty: int 430 | totalDifficulty: int 431 | extraData: str 432 | size: int 433 | gasLimit: int 434 | gasUsed: int 435 | timestamp: datetime 436 | uncles: List[eth_types.Hash32] 437 | transactions: List[Transaction] = Field(default_factory=list) 438 | 439 | 440 | class WhisperFilter(AutoEthable): 441 | # noinspection PyUnresolvedReferences 442 | """Creates filter to notify, when client receives whisper message 443 | matching the filter options. 444 | 445 | Attributes: 446 | to (eth_types.Address): Identity of the receiver. When 447 | present it will try to decrypt any incoming message if the 448 | client holds the private key to this identity. 449 | topics (list[eth_types.Data]): Array of DATA topics which the incoming 450 | message’s topics should match. You can use the following 451 | combinations: 452 | [A, B] = A && B 453 | [A, [B, C]] = A && (B || C) 454 | [null, A, B] = ANYTHING && A && B null works as a wildcard 455 | """ 456 | 457 | to: eth_types.HexAddress 458 | topics: list[eth_types.Data] 459 | 460 | 461 | class Message(AutoEthable): 462 | # noinspection PyUnresolvedReferences 463 | """Whisper Message. 464 | 465 | Attributes: 466 | hash (eth_types.Data): The hash of the message. 467 | from (eth_types.Address): The sender of the message, if specified. 468 | to (eth_types.Address): The receiver of the message, if specified. 469 | expiry (int): Time in seconds when this message should expire. 470 | ttl (int): Time the message should float in the system in seconds. 471 | sent (int): The unix timestamp when the message was sent. 472 | topics (list[eth_types.Data]): Topics the message contained. 473 | payload (eth_types.Data): The payload of the message. 474 | workProved (int): The work this message required before it was send. 475 | """ 476 | 477 | topics: list[eth_types.Data] 478 | payload: eth_types.Data 479 | ttl: int 480 | priority: int = 1 481 | from_address: Optional[eth_types.HexAddress] = None 482 | to: Optional[eth_types.HexAddress] = None 483 | workProved: Optional[int] = None 484 | sent: Optional[int] = None 485 | expiry: Optional[int] = None 486 | message_hash: Optional[eth_types.Hash32] = None 487 | -------------------------------------------------------------------------------- /fasteth/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valorem-labs-inc/fasteth/d773cc8ff5b90617fbac8da65d54a92bb913cf17/fasteth/py.typed -------------------------------------------------------------------------------- /fasteth/rpc.py: -------------------------------------------------------------------------------- 1 | # 3rd party imports 2 | from typing import Any, List, Optional, Union 3 | 4 | # noinspection PyPackageRequirements 5 | import httpx 6 | import orjson 7 | from eth_utils import conversions 8 | 9 | from fasteth import models as eth_models 10 | from fasteth import types as eth_types 11 | from fasteth import utils as eth_utils 12 | 13 | # TODO(Websocket support) 14 | # TODO(IPC Support) 15 | # TODO(Add doc about https://eth.wiki/json-rpc/API#the-default-block-parameter) 16 | # TODO(Consider this: https://github.com/ethereum/snake-charmers-tactical-manual) 17 | # Reminder, use decimal.Decimal for any math involving 2 integers becoming a float. 18 | # That is the python style guide for ethereum projects. 19 | # See https://docs.soliditylang.org/en/latest/abi-spec.html 20 | 21 | # pile of "magic" variables to deal with. 22 | # TODO(move these out of the module scope) 23 | default_block_id: eth_types.DefaultBlockIdentifier = "latest" 24 | position_zero: eth_types.Data = eth_utils.to_eth_converters[eth_types.Data]("0x0") 25 | result_key = "result" 26 | localhost = "http://localhost:8545/" 27 | json_headers = {"Content-Type": "application/json"} 28 | 29 | 30 | class AsyncJSONRPCCore(httpx.AsyncClient): 31 | """Asynchronous remote procedure call client.""" 32 | 33 | def __init__(self, rpc_uri: str = localhost, http2: bool = False): 34 | """Initialize JSON RPC. 35 | 36 | :param rpc_uri: RPC URI for ethereum client. 37 | :param http2: Boolean to use http2 when true. 38 | """ 39 | super().__init__(http2=http2) 40 | self.rpc_uri = rpc_uri 41 | 42 | async def rpc(self, rpc_request: eth_models.JSONRPCRequest) -> Any: 43 | """Return JSONRPCResponse for the JSONRPCRequest, executing a RPC. 44 | 45 | :raises eth_exceptions.JSONRPCError 46 | :raises httpx.HTTPStatusError 47 | :raises httpx.StreamError""" 48 | response = await self.post( 49 | url=self.rpc_uri, 50 | headers=json_headers, 51 | content=orjson.dumps(rpc_request.dict()), 52 | ) 53 | # We want to raise here http errors. 54 | response.raise_for_status() 55 | # Now we get back the JSON and do error handling. 56 | rpc_response = eth_models.JSONRPCResponse.parse_obj( 57 | orjson.loads(response.content) 58 | ) 59 | if rpc_response.error: 60 | rpc_response.error.raise_for_error() 61 | return rpc_response.result 62 | 63 | 64 | class AsyncEthereumJSONRPC(AsyncJSONRPCCore): 65 | """Presents an asynchronous interface to the ethereum JSON RPC. 66 | 67 | This class aggressively converts the strings returned in result 68 | bodies into efficient native python data eth_types in cases where a string 69 | is returned in place of an int, et cētera. 70 | 71 | The API info at https://eth.wiki/json-rpc/API was highly helpful in 72 | creating this. 73 | """ 74 | 75 | rpc_schema = eth_models.RPCSchema 76 | 77 | async def client_version(self) -> str: 78 | """Return the current ethereum client version as a string. 79 | 80 | Calls web3_clientVersion 81 | 82 | :returns 83 | string: The current client version. 84 | """ 85 | return await self.rpc( 86 | eth_models.JSONRPCRequest( 87 | method=self.rpc_schema.client_version[0], 88 | id=self.rpc_schema.client_version[1], 89 | ) 90 | ) 91 | 92 | async def sha3(self, data: eth_types.Data) -> eth_types.Data: 93 | """Returns the Keccak-256 of the given data. 94 | 95 | Consider using eth_utils.sha3 instead to save the round trip. 96 | 97 | :param data: Bytes of eth_types.Data to Keccak-256 hash. 98 | 99 | :returns 100 | eth_types.Data: Keccak-256 bytes of eth_types.Data 101 | """ 102 | return eth_utils.to_py_converters[eth_types.Hash32]( 103 | await self.rpc( 104 | eth_models.JSONRPCRequest( 105 | method=self.rpc_schema.sha3[0], 106 | id=self.rpc_schema.sha3[1], 107 | params=[eth_utils.to_eth_converters[eth_types.Data](data)], 108 | ) 109 | ) 110 | ) 111 | 112 | async def network_version(self) -> eth_models.Network: 113 | """Returns the current network ID. 114 | 115 | Calls net_version. 116 | 117 | :returns 118 | Network:Enum populated with network ID. 119 | """ 120 | # noinspection PyArgumentList 121 | # PyCharm is incorrect here. 122 | return eth_models.Network( 123 | int( 124 | await self.rpc( 125 | eth_models.JSONRPCRequest( 126 | method=self.rpc_schema.network_version[0], 127 | id=self.rpc_schema.network_version[1], 128 | ) 129 | ) 130 | ) 131 | ) 132 | 133 | async def network_listening(self) -> bool: 134 | """Returns true if client is actively listening for network connections. 135 | 136 | Calls net_listening. 137 | 138 | :returns 139 | bool: True when listening, otherwise False""" 140 | return await self.rpc( 141 | eth_models.JSONRPCRequest( 142 | method=self.rpc_schema.network_listening[0], 143 | id=self.rpc_schema.network_listening[1], 144 | ) 145 | ) 146 | 147 | async def network_peer_count(self) -> int: 148 | """Returns number of peers currently connected to the client 149 | 150 | Calls net_peerCount. 151 | 152 | :returns 153 | int: number of connected peers 154 | """ 155 | return eth_utils.to_py_converters[int]( 156 | await self.rpc( 157 | eth_models.JSONRPCRequest( 158 | method=self.rpc_schema.network_peer_count[0], 159 | id=self.rpc_schema.network_peer_count[1], 160 | ) 161 | ) 162 | ) 163 | 164 | async def protocol_version(self) -> int: 165 | """Returns the current ethereum protocol version. 166 | 167 | Calls eth_protocolVersion. 168 | 169 | :returns 170 | int: The current Ethereum protocol version as an Integer.""" 171 | return eth_utils.to_py_converters[int]( 172 | await self.rpc( 173 | eth_models.JSONRPCRequest( 174 | method=self.rpc_schema.protocol_version[0], 175 | id=self.rpc_schema.protocol_version[1], 176 | ) 177 | ) 178 | ) 179 | 180 | async def syncing(self) -> eth_models.SyncStatus: 181 | """Returns an object with sync status data. 182 | 183 | Calls eth_syncing. 184 | 185 | :returns 186 | eth_models.SyncStatus or False: with sync status data or False when 187 | not syncing. 188 | """ 189 | # It mystifies me why this can't return a proper JSON boolean. 190 | result = eth_utils.result_truthiness( 191 | await self.rpc( 192 | eth_models.JSONRPCRequest( 193 | method=self.rpc_schema.syncing[0], id=self.rpc_schema.syncing[1] 194 | ) 195 | ) 196 | ) 197 | 198 | if result: 199 | result["syncing"] = True 200 | return eth_models.SyncStatus.parse_obj(result) 201 | else: 202 | return eth_models.SyncStatus(syncing=False) 203 | 204 | async def coinbase(self) -> eth_types.HexAddress: 205 | """Returns the client coinbase address 206 | 207 | Calls eth_coinbase. 208 | 209 | :returns 210 | str:The current coinbase address. 211 | :raises 212 | :exception JSONRPCError: when this method is not supported. 213 | """ 214 | return eth_types.HexAddress( 215 | await self.rpc( 216 | eth_models.JSONRPCRequest( 217 | method=self.rpc_schema.coinbase[0], id=self.rpc_schema.coinbase[1] 218 | ) 219 | ) 220 | ) 221 | 222 | async def mining(self) -> bool: 223 | """Returns True if the client is actively mining new blocks. 224 | 225 | Calls eth_mining. 226 | 227 | :returns 228 | bool: True if the client is mining, otherwise False 229 | """ 230 | # Why can this RPC actually return a bool? 231 | return eth_utils.result_truthiness( 232 | await self.rpc( 233 | eth_models.JSONRPCRequest( 234 | method=self.rpc_schema.mining[0], id=self.rpc_schema.mining[1] 235 | ) 236 | ) 237 | ) 238 | 239 | async def hashrate(self) -> int: 240 | """Returns the number of hashes per second that the node is mining with. 241 | 242 | Calls eth_hashrate. 243 | 244 | :returns: 245 | int:Number of hashes per second. 246 | """ 247 | return eth_utils.to_py_converters[int]( 248 | await self.rpc( 249 | eth_models.JSONRPCRequest( 250 | method=self.rpc_schema.hashrate[0], id=self.rpc_schema.mining[1] 251 | ) 252 | ) 253 | ) 254 | 255 | async def gas_price(self) -> int: 256 | """Returns the current gas price as an integer in wei. 257 | 258 | Calls eth_gasPrice. 259 | 260 | :returns 261 | int:integer of the current gas price in wei. 262 | """ 263 | return eth_utils.to_py_converters[int]( 264 | await self.rpc( 265 | eth_models.JSONRPCRequest( 266 | method=self.rpc_schema.gas_price[0], id=self.rpc_schema.gas_price[1] 267 | ) 268 | ) 269 | ) 270 | 271 | async def accounts(self) -> list[eth_types.HexAddress]: 272 | """Returns a list of addresses owned by the client. 273 | 274 | Calls eth_accounts. 275 | 276 | :returns 277 | list: A list of eth_types.Address owned by the client. 278 | """ 279 | result = await self.rpc( 280 | eth_models.JSONRPCRequest( 281 | method=self.rpc_schema.accounts[0], id=self.rpc_schema.accounts[1] 282 | ) 283 | ) 284 | 285 | return result or [] 286 | 287 | async def block_number(self) -> int: 288 | """Returns the number of most recent block. 289 | 290 | Calls eth_blockNumber. 291 | 292 | :returns 293 | eth_types.BlockNumber: The current block number a client is on as an int. 294 | """ 295 | return eth_utils.to_py_converters[int]( 296 | await self.rpc( 297 | eth_models.JSONRPCRequest( 298 | method=self.rpc_schema.block_number[0], 299 | id=self.rpc_schema.block_number[1], 300 | ) 301 | ) 302 | ) 303 | 304 | async def get_balance( 305 | self, 306 | address: eth_types.HexAddress, 307 | block_identifier: eth_types.DefaultBlockIdentifier = default_block_id, 308 | ) -> int: 309 | """Returns the balance of the given address during the given block block_number. 310 | 311 | Calls eth_getBalance. 312 | 313 | :param address: an ethereum address to get the balance of. 314 | :param block_identifier: an eth_types.DefaultBlockIdentifier of the block 315 | number to check at. 316 | 317 | :returns 318 | int: The balance in wei during block_number. 319 | """ 320 | return eth_utils.to_py_converters[int]( 321 | await self.rpc( 322 | eth_models.JSONRPCRequest( 323 | method=self.rpc_schema.get_balance[0], 324 | id=self.rpc_schema.get_balance[1], 325 | params=[address, block_identifier], 326 | ) 327 | ) 328 | ) 329 | 330 | async def get_storage_at( 331 | self, 332 | address: eth_types.HexAddress, 333 | position: int = 0, 334 | block_identifier: eth_types.DefaultBlockIdentifier = default_block_id, 335 | ) -> eth_types.Data: 336 | """Returns storage from position at a given address during block_identifier. 337 | 338 | Calls eth_getStorageAt. 339 | 340 | See: https://eth.wiki/json-rpc/API#eth_getstorageat 341 | 342 | There are some usage examples at that link which are useful. 343 | 344 | eth_utils.keccak and eth-hash are useful here as well. 345 | 346 | :param address: eth_types.Address address of the storage. 347 | :param position: integer as eth_types.Data of the position in the storage. 348 | :param block_identifier: eth_types.DefaultBlockIdentifier for the block to 349 | retrieve from. 350 | 351 | :returns eth_types.Data: containing the data at the address, position, 352 | block_identifier. 353 | """ 354 | return eth_utils.to_py_converters[eth_types.Data]( 355 | await self.rpc( 356 | eth_models.JSONRPCRequest( 357 | method=self.rpc_schema.get_storage_at[0], 358 | id=self.rpc_schema.get_storage_at[1], 359 | params=[ 360 | address, 361 | eth_utils.to_eth_converters[int](position), 362 | block_identifier, 363 | ], 364 | ) 365 | ) 366 | ) 367 | 368 | async def get_transaction_count( 369 | self, 370 | address: eth_types.HexAddress, 371 | block_identifier: eth_types.DefaultBlockIdentifier = default_block_id, 372 | ) -> int: 373 | """Returns the number of transactions sent from an address. 374 | 375 | Calls eth_getTransactionCount 376 | 377 | :param address: address to get count for. 378 | :param block_identifier: eth_types.DefaultBlockIdentifier to get count at. 379 | 380 | :returns int: The number of transactions sent from address. 381 | """ 382 | return eth_utils.to_py_converters[int]( 383 | await self.rpc( 384 | eth_models.JSONRPCRequest( 385 | method=self.rpc_schema.get_transaction_count[0], 386 | id=self.rpc_schema.get_transaction_count[1], 387 | params=[address, block_identifier], 388 | ) 389 | ) 390 | ) 391 | 392 | # TODO(conversion from block number to hash) 393 | async def get_block_transaction_count_by_hash( 394 | self, block_hash: eth_types.Hash32 395 | ) -> int: 396 | """Returns the number of txns in a block matching the given block_hash. 397 | 398 | Calls eth_getBlockTransactionCountByHash. 399 | 400 | Can raise an exception converting integer if block_hash is is invalid. 401 | 402 | :param block_hash: eth_types.Hash32 of the block. 403 | 404 | :returns 405 | int: Transaction count for given block. 406 | """ 407 | return eth_utils.to_py_converters[int]( 408 | await self.rpc( 409 | eth_models.JSONRPCRequest( 410 | method=self.rpc_schema.get_block_transaction_count_by_hash[0], 411 | id=self.rpc_schema.get_block_transaction_count_by_hash[1], 412 | params=[eth_utils.to_eth_converters[eth_types.Hash32](block_hash)], 413 | ) 414 | ) 415 | ) 416 | 417 | async def get_block_transaction_count_by_number( 418 | self, block_identifier: eth_types.DefaultBlockIdentifier 419 | ) -> int: 420 | """Returns the number of txns in a block matching the given block_identifier. 421 | 422 | Calls eth_getBlockTransactionCountByNumber. 423 | 424 | Can raise an exception converting integer if block_identifier is is invalid. 425 | 426 | :param block_identifier: eth_types.DefaultBlockIdentifier of the block. 427 | 428 | :returns 429 | int: Transaction count for given block. 430 | """ 431 | return eth_utils.to_py_converters[int]( 432 | await self.rpc( 433 | eth_models.JSONRPCRequest( 434 | method=self.rpc_schema.get_block_transaction_count_by_number[0], 435 | id=self.rpc_schema.get_block_transaction_count_by_number[1], 436 | params=[block_identifier], 437 | ) 438 | ) 439 | ) 440 | 441 | async def get_uncle_count_by_block_hash(self, block_hash: eth_types.Hash32) -> int: 442 | """Returns the number of uncles from a block matching the given block_hash. 443 | 444 | Calls eth_getUncleCountByBlockHash. 445 | 446 | Can raise an exception converting integer if block_hash is is invalid. 447 | 448 | :param block_hash: eth_types.HexAddress hash of the block. 449 | 450 | :returns 451 | int: number of uncles in this block. 452 | """ 453 | return eth_utils.conversions.to_int( 454 | hexstr=await self.rpc( 455 | eth_models.JSONRPCRequest( 456 | method=self.rpc_schema.get_uncle_count_by_block_hash[0], 457 | id=self.rpc_schema.get_uncle_count_by_block_hash[1], 458 | params=[eth_utils.to_eth_converters[eth_types.Hash32](block_hash)], 459 | ) 460 | ) 461 | ) 462 | 463 | async def get_uncle_count_by_block_number( 464 | self, block_identifier: eth_types.DefaultBlockIdentifier 465 | ) -> int: 466 | """Returns the number of uncles block matching the given block_identifier. 467 | 468 | Calls eth_getUncleCountByBlockNumber. 469 | 470 | Can raise an exception converting integer if block_identifier is is invalid. 471 | 472 | :param block_identifier: eth_types.DefaultBlockIdentifier of the block. 473 | 474 | :returns 475 | int: number of uncles in this block. 476 | """ 477 | return eth_utils.conversions.to_int( 478 | hexstr=await self.rpc( 479 | eth_models.JSONRPCRequest( 480 | method=self.rpc_schema.get_uncle_count_by_block_number[0], 481 | id=self.rpc_schema.get_uncle_count_by_block_number[1], 482 | params=[block_identifier], 483 | ) 484 | ) 485 | ) 486 | 487 | async def get_code( 488 | self, 489 | address: eth_types.HexAddress, 490 | block_identifier: eth_types.DefaultBlockIdentifier = default_block_id, 491 | ) -> eth_types.Data: 492 | """Return code at a given address during specified block. 493 | 494 | :param address: The address to retrieve the code from. 495 | :param block_identifier: the block during which to get the code from. 496 | 497 | :returns 498 | eth_types.HexStr: string in hex format containing the code as data. 499 | """ 500 | return eth_utils.to_py_converters[eth_types.Data]( 501 | await self.rpc( 502 | eth_models.JSONRPCRequest( 503 | method=self.rpc_schema.get_code[0], 504 | id=self.rpc_schema.get_code[1], 505 | params=[address, block_identifier], 506 | ) 507 | ) 508 | ) 509 | 510 | async def sign( 511 | self, address: eth_types.HexAddress, message: eth_types.HexStr 512 | ) -> eth_types.Data: 513 | """Returns an signed message. 514 | 515 | sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))) 516 | 517 | By adding a prefix to the message makes the calculated signature recognizable 518 | as an Ethereum specific signature. This prevents misuse where a malicious 519 | DApp can sign arbitrary data (e.g. transaction) and use the signature to 520 | impersonate the victim. 521 | 522 | Note the address to sign with must be unlocked. 523 | 524 | Calls eth_sign. 525 | 526 | TODO(Add tests for this function) 527 | 528 | :param address: address to sign with. 529 | :param message: hex string of n bytes, message to sign. 530 | 531 | :returns 532 | eth_types.Data: signature 533 | """ 534 | return await self.rpc( 535 | eth_models.JSONRPCRequest( 536 | method=self.rpc_schema.sign[0], 537 | id=self.rpc_schema.sign[1], 538 | params=[address, message], 539 | ) 540 | ) 541 | 542 | async def sign_transaction( 543 | self, transaction: eth_models.Transaction 544 | ) -> eth_types.HexStr: 545 | """Returns a signed transaction object as eth_types.HexStr. 546 | 547 | Signs and returns a transaction that can be submitted to the network at a 548 | later time using with send_raw_transaction. 549 | 550 | Calls eth_signTransaction 551 | 552 | :param transaction: eth_models.Transaction object to sign. 553 | 554 | :returns 555 | eth_types.HexStr: The signed transaction object. 556 | """ 557 | return await self.rpc( 558 | eth_models.JSONRPCRequest( 559 | method=self.rpc_schema.sign_transaction[0], 560 | id=self.rpc_schema.sign_transaction[1], 561 | params=[transaction.dict()], 562 | ) 563 | ) 564 | 565 | async def send_transaction( 566 | self, transaction: eth_models.Transaction 567 | ) -> eth_types.HexStr: 568 | """Creates new message call transaction or a contract creation. 569 | 570 | :param transaction: eth_models.Transaction object to send. 571 | 572 | :returns 573 | eth_types.HexStr: the transaction hash, or the zero hash if the transaction 574 | is not yet available. 575 | """ 576 | 577 | return eth_types.HexStr( 578 | await self.rpc( 579 | eth_models.JSONRPCRequest( 580 | method=self.rpc_schema.send_transaction[0], 581 | id=self.rpc_schema.send_transaction[1], 582 | params=[transaction.dict()], 583 | ) 584 | ) 585 | ) 586 | 587 | async def send_raw_transaction(self, data: eth_types.HexStr) -> eth_types.HexStr: 588 | """Creates new transaction or a contract creation for signed transactions. 589 | 590 | # TODO(Handle reverted execution) 591 | 592 | :param data: The signed transaction data. 593 | 594 | :returns 595 | eth_types.HexStr: the transaction hash, or the zero hash if the transaction 596 | is not yet available. 597 | """ 598 | return eth_types.HexStr( 599 | await self.rpc( 600 | eth_models.JSONRPCRequest( 601 | method=self.rpc_schema.send_raw_transaction[0], 602 | id=self.rpc_schema.send_raw_transaction[1], 603 | params=[data], 604 | ) 605 | ) 606 | ) 607 | 608 | async def call( 609 | self, 610 | transaction: eth_models.Transaction, 611 | block_identifier: eth_types.DefaultBlockIdentifier, 612 | ) -> eth_types.HexStr: 613 | """Execute a new message call without creating a new block chain transaction. 614 | 615 | Calls eth_call. 616 | 617 | :param transaction: eth_models.Transaction call object. 618 | :param block_identifier: block to call the transaction against. 619 | 620 | :returns 621 | eth_types.data: The return value of executed contract. 622 | """ 623 | return await self.rpc( 624 | eth_models.JSONRPCRequest( 625 | method=self.rpc_schema.call[0], 626 | id=self.rpc_schema.call[1], 627 | params=[transaction.dict(), block_identifier], 628 | ) 629 | ) 630 | 631 | async def estimate_gas( 632 | self, 633 | transaction: eth_models.Transaction, 634 | block_identifier: eth_types.DefaultBlockIdentifier, 635 | ) -> int: 636 | """Returns an estimate of how much gas is necessary to complete the transaction. 637 | 638 | Generates and returns an estimate of how much gas is necessary to allow the 639 | transaction to complete. The transaction will not be added to the blockchain. 640 | Note that the estimate may be significantly more than the amount of gas 641 | actually used by the transaction, for a variety of reasons including EVM 642 | mechanics and node performance. 643 | 644 | Calls eth_estimateGas. 645 | 646 | :param transaction: eth_models.Transaction call object. 647 | :param block_identifier: block to call the transaction against. 648 | 649 | :returns 650 | int: The amount of gas used. 651 | """ 652 | ret: int = eth_utils.to_py_converters[int]( 653 | await self.rpc( 654 | eth_models.JSONRPCRequest( 655 | method=self.rpc_schema.estimate_gas[0], 656 | id=self.rpc_schema.estimate_gas[1], 657 | params=[transaction.dict(), block_identifier], 658 | ) 659 | ) 660 | ) 661 | return ret 662 | 663 | async def get_block_by_hash( 664 | self, block_id: eth_types.Hash32, full: bool = False, 665 | ) -> Optional[eth_models.Block]: 666 | """Returns information about a block by hash. 667 | 668 | Calls the eth_getBlockByHash. 669 | 670 | :param block_id: eth_types.Hash32 of a block. 671 | :param full: If True it returns the full transaction objects, if False 672 | only the hashes of the transactions. 673 | 674 | :returns 675 | Union[eth_models.Block, None]: A block object, or None when no block found. 676 | """ 677 | data = await self.rpc( 678 | eth_models.JSONRPCRequest( 679 | method=self.rpc_schema.get_block_by_hash[0], 680 | id=self.rpc_schema.get_block_by_hash[1], 681 | params=[eth_utils.to_eth_converters[eth_types.Hash32](block_id), full], 682 | ) 683 | ) 684 | return eth_models.Block.parse_obj(data) 685 | 686 | async def get_block_by_number( 687 | self, block_id: eth_types.DefaultBlockIdentifier, full: bool 688 | ) -> Optional[eth_models.Block]: 689 | """Returns information about a block by block number. 690 | 691 | Calls the eth_getBlockByNumber. 692 | 693 | :param block_id: Integer of a block number, or the string 694 | "earliest", "latest" or "pending", as in the 695 | default block parameter. 696 | :param full: If true it returns the full transaction objects, if false 697 | only the hashes of the transactions. 698 | 699 | :returns 700 | Union[eth_models.Block, None]: A block object, or None when no block was 701 | found. 702 | """ 703 | if block_id not in ["pending", "latest", "earliest"]: 704 | block_id = eth_utils.to_eth_converters[int](block_id) 705 | return eth_models.Block.parse_obj( 706 | await self.rpc( 707 | eth_models.JSONRPCRequest( 708 | method=self.rpc_schema.get_block_by_number[0], 709 | id=self.rpc_schema.get_block_by_number[1], 710 | params=[block_id, full], 711 | ) 712 | ) 713 | ) 714 | 715 | async def submit_hashrate( 716 | self, hashrate: eth_types.HexStr, identifier: eth_types.HexStr, 717 | ) -> bool: 718 | """Return code at a given address during specified block. 719 | 720 | Calls eth_submitHashrate. 721 | 722 | :param hashrate: A hexadecimal string representation of the hash rate. 723 | :param identifier: A random hexadecimal ID identifying the client. 724 | 725 | :returns 726 | bool: True if submitting went through and false otherwise. 727 | """ 728 | return eth_utils.result_truthiness( 729 | await self.rpc( 730 | eth_models.JSONRPCRequest( 731 | method=self.rpc_schema.submit_hashrate[0], 732 | id=self.rpc_schema.submit_hashrate[1], 733 | params=[hashrate, identifier], 734 | ) 735 | ) 736 | ) 737 | 738 | async def shh_version(self) -> str: 739 | """Returns the current whisper protocol version. 740 | 741 | Calls shh_version. 742 | 743 | :returns 744 | str: The current whisper protocol version. 745 | """ 746 | return await self.rpc( 747 | eth_models.JSONRPCRequest( 748 | method=self.rpc_schema.shh_version[0], id=self.rpc_schema.shh_version[1] 749 | ) 750 | ) 751 | 752 | async def shh_post(self, whisper: eth_models.Message) -> bool: 753 | """Sends a whisper message. 754 | 755 | Calls shh_post. 756 | 757 | :param whisper: The whisper post object. 758 | 759 | :returns 760 | bool: Returns true if the message was send, otherwise false. 761 | """ 762 | return await self.rpc( 763 | eth_models.JSONRPCRequest( 764 | method=self.rpc_schema.shh_post[0], 765 | id=self.rpc_schema.shh_post[1], 766 | params=[whisper.dict()], 767 | ) 768 | ) 769 | 770 | async def shh_new_identity(self) -> eth_types.Data: 771 | """Creates new whisper identity in the client. 772 | 773 | Calls shh_newIdentity. 774 | 775 | :returns 776 | eth_types.Data: The address of the new identity (60 Bytes). 777 | """ 778 | return await self.rpc( 779 | eth_models.JSONRPCRequest( 780 | method=self.rpc_schema.shh_new_identity[0], 781 | id=self.rpc_schema.shh_new_identity[1], 782 | ) 783 | ) 784 | 785 | async def shh_has_identity(self, identifier: eth_types.Data) -> bool: 786 | """Checks if the client hold the private keys for a given identity. 787 | 788 | Calls shh_hasIdentity. 789 | 790 | :params id: The identity address to check. 791 | 792 | :returns 793 | bool: Returns true if the message was send, otherwise false. 794 | """ 795 | return eth_utils.result_truthiness( 796 | await self.rpc( 797 | eth_models.JSONRPCRequest( 798 | method=self.rpc_schema.shh_has_identity[0], 799 | id=self.rpc_schema.shh_has_identity[1], 800 | params=[identifier], 801 | ) 802 | ) 803 | ) 804 | 805 | async def shh_new_group(self) -> eth_types.Data: 806 | """Create a new whisper group (?). 807 | 808 | Calls shh_newGroup. 809 | 810 | :returns 811 | eth_types.Data: The address of the new group (60 Bytes). 812 | """ 813 | return await self.rpc( 814 | eth_models.JSONRPCRequest( 815 | method=self.rpc_schema.shh_new_group[0], 816 | id=self.rpc_schema.shh_new_group[1], 817 | ) 818 | ) 819 | 820 | async def shh_add_to_group(self, identifier: eth_types.Data) -> bool: 821 | """Add an identity to a group (?). 822 | 823 | Calls shh_addToGroup. 824 | 825 | :params id: The identity address to add to a group. 826 | 827 | :returns 828 | bool: Returns true if the identity was successfully added to the 829 | group, otherwise false (?). 830 | """ 831 | return eth_utils.result_truthiness( 832 | await self.rpc( 833 | eth_models.JSONRPCRequest( 834 | method=self.rpc_schema.shh_add_to_group[0], 835 | id=self.rpc_schema.shh_add_to_group[1], 836 | params=[identifier], 837 | ) 838 | ) 839 | ) 840 | 841 | async def shh_new_filter(self, whisper_filter: eth_models.WhisperFilter) -> int: 842 | """Creates filter to notify, when client receives whisper message 843 | matching the filter options. 844 | 845 | Calls shh_newFilter. 846 | 847 | :params filter: The filter options. 848 | 849 | :returns 850 | int: The newly created filter. 851 | """ 852 | return eth_utils.conversions.to_int( 853 | await self.rpc( 854 | eth_models.JSONRPCRequest( 855 | method=self.rpc_schema.shh_new_filter[0], 856 | id=self.rpc_schema.shh_new_filter[1], 857 | params=[whisper_filter.dict()], 858 | ) 859 | ) 860 | ) 861 | 862 | async def shh_uninstall_filter(self, identifier: int) -> bool: 863 | """Uninstalls a filter with given id. Should always be called when 864 | watch is no longer needed. Additionally, filters timeout when they 865 | are not requested with shh_getFilterChanges for a period of time. 866 | 867 | Calls shh_uninstallFilter. 868 | 869 | :params id: The filter id. 870 | 871 | :returns 872 | bool: True if the filter was successfully uninstalled, 873 | otherwise false. 874 | """ 875 | return await self.rpc( 876 | eth_models.JSONRPCRequest( 877 | method=self.rpc_schema.shh_uninstall_filter[0], 878 | id=self.rpc_schema.shh_uninstall_filter[1], 879 | params=[conversions.to_hex(identifier)], 880 | ) 881 | ) 882 | 883 | async def get_shh_filter_changes(self, identifier: int) -> List[eth_models.Message]: 884 | """Polling method for whisper filters. Returns new messages since the 885 | last call of this method. 886 | 887 | Note: Calling the shh_getMessages method, will reset the buffer for 888 | this method, so that you won’t receive duplicate messages. 889 | 890 | Calls shh_getFilterChanges. 891 | 892 | :param identifier: The filter id. 893 | 894 | :returns 895 | List[eth_models.Messages]: Array of messages received since last poll. 896 | """ 897 | result = await self.rpc( 898 | eth_models.JSONRPCRequest( 899 | method=self.rpc_schema.get_shh_filter_changes[0], 900 | id=self.rpc_schema.get_shh_filter_changes[1], 901 | params=[conversions.to_hex(identifier)], 902 | ) 903 | ) 904 | 905 | return eth_models.iterate_list(eth_models.Message, result) 906 | 907 | async def get_shh_messages( 908 | self, identifier: int 909 | ) -> Union[List[eth_models.Message], bool]: 910 | """Get all messages matching a filter. Unlike shh_getFilterChanges 911 | this returns all messages. 912 | 913 | Calls shh_getMessages. 914 | 915 | :param identifier: The filter id. 916 | 917 | :returns 918 | List[eth_models.Messages]: Array of messages received. 919 | bool: False if no messages. 920 | """ 921 | result = await self.rpc( 922 | eth_models.JSONRPCRequest( 923 | method=self.rpc_schema.get_shh_messages[0], 924 | id=self.rpc_schema.get_shh_messages[1], 925 | params=[conversions.to_hex(identifier)], 926 | ) 927 | ) 928 | 929 | truthiness = eth_utils.result_truthiness(result) 930 | if isinstance(truthiness, bool): 931 | return truthiness 932 | 933 | return eth_models.iterate_list(eth_models.Message, result) 934 | -------------------------------------------------------------------------------- /fasteth/types.py: -------------------------------------------------------------------------------- 1 | """Types for ethereum data.""" 2 | from typing import NewType, Union 3 | 4 | # This wraps the ethereum foundation typing. 5 | from eth_typing.abi import Decodable, TypeStr # noqa: F401 6 | from eth_typing.bls import BLSPubkey, BLSSignature # noqa: F401 7 | from eth_typing.discovery import NodeID # noqa: F401 8 | from eth_typing.encoding import HexStr, Primitives # noqa: F401 9 | from eth_typing.enums import ForkName # noqa: F401 10 | from eth_typing.ethpm import URI, ContractName, Manifest # noqa: F401 11 | from eth_typing.evm import ( # noqa: F401 12 | Address, 13 | AnyAddress, 14 | BlockIdentifier, 15 | BlockNumber, 16 | ChecksumAddress, 17 | Hash32, 18 | HexAddress, 19 | ) 20 | 21 | # These are used profusely in the API docs as generic HexStr. 22 | # A quantity as a HexStr 23 | Quantity = NewType("Quantity", str) 24 | # A HexStr of data with an arbitrary length 25 | Data = NewType("Data", str) 26 | # A LogsBloom datatype 27 | LogsBloom = NewType("LogsBloom", Data) 28 | # A Signature datatype 29 | Signature = NewType("Signature", Data) 30 | # A block identifier with string support 31 | DefaultBlockIdentifier = Union[str, BlockIdentifier] 32 | -------------------------------------------------------------------------------- /fasteth/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Dict, Optional 3 | 4 | import eth_utils 5 | from eth_utils import add_0x_prefix, conversions # noqa: F401 6 | 7 | from fasteth import types as eth_types 8 | 9 | # TODO(consider replacing with enum) 10 | to_py_converters: Dict = { 11 | eth_types.Hash32: lambda x: eth_utils.to_bytes(None, x), 12 | eth_types.Address: lambda x: eth_utils.to_bytes(None, x), 13 | eth_types.HexAddress: eth_utils.to_normalized_address, 14 | eth_types.ChecksumAddress: eth_utils.to_checksum_address, 15 | eth_types.AnyAddress: lambda x: eth_utils.to_bytes(None, x), 16 | eth_types.HexStr: lambda x: eth_utils.to_text(None, x), 17 | eth_types.BlockNumber: lambda x: eth_utils.to_int(None, x), 18 | eth_types.BlockIdentifier: lambda x: eth_utils.to_int(None, x), 19 | eth_types.Data: lambda x: eth_utils.to_text(None, x), 20 | datetime: lambda x: datetime.fromtimestamp(eth_utils.to_int(None, x)), 21 | int: lambda x: eth_utils.to_int(None, x), 22 | str: lambda x: eth_utils.to_text(None, None, x), 23 | bytes: lambda x: eth_utils.to_bytes(None, x), 24 | Optional[eth_types.Hash32]: lambda x: eth_utils.to_bytes(None, x), 25 | Optional[eth_types.Address]: lambda x: eth_utils.to_bytes(None, x), 26 | Optional[eth_types.HexAddress]: eth_utils.to_normalized_address, 27 | Optional[eth_types.ChecksumAddress]: eth_utils.to_checksum_address, 28 | Optional[eth_types.AnyAddress]: lambda x: eth_utils.to_bytes(None, x), 29 | Optional[eth_types.HexStr]: lambda x: eth_utils.to_text(None, x), 30 | Optional[eth_types.BlockNumber]: lambda x: eth_utils.to_int(None, x), 31 | Optional[eth_types.BlockIdentifier]: lambda x: eth_utils.to_int(None, x), 32 | Optional[eth_types.Data]: lambda x: eth_utils.to_text(None, x), 33 | Optional[datetime]: lambda x: datetime.fromtimestamp(eth_utils.to_int(None, x)), 34 | Optional[int]: lambda x: eth_utils.to_int(None, x), 35 | Optional[str]: lambda x: eth_utils.to_text(None, None, x), 36 | Optional[bytes]: lambda x: eth_utils.to_bytes(None, x), 37 | } 38 | 39 | to_eth_converters: Dict = { 40 | eth_types.Hash32: eth_utils.to_hex, 41 | eth_types.Address: eth_utils.to_hex, 42 | eth_types.HexAddress: eth_utils.to_normalized_address, 43 | eth_types.ChecksumAddress: eth_utils.to_checksum_address, 44 | eth_types.AnyAddress: eth_utils.to_hex, 45 | eth_types.HexStr: lambda x: eth_utils.to_hex(None, None, x), 46 | eth_types.BlockNumber: eth_utils.to_hex, 47 | eth_types.BlockIdentifier: eth_utils.to_hex, 48 | eth_types.Data: lambda x: eth_utils.to_hex(None, None, x), 49 | datetime: lambda x: eth_utils.to_hex(x.timestamp()), 50 | int: eth_utils.to_hex, 51 | str: lambda x: eth_utils.to_hex(None, None, x), 52 | bytes: eth_utils.to_hex, 53 | Optional[eth_types.Hash32]: eth_utils.to_hex, 54 | Optional[eth_types.Address]: eth_utils.to_hex, 55 | Optional[eth_types.HexAddress]: eth_utils.to_normalized_address, 56 | Optional[eth_types.ChecksumAddress]: eth_utils.to_checksum_address, 57 | Optional[eth_types.AnyAddress]: eth_utils.to_hex, 58 | Optional[eth_types.HexStr]: lambda x: eth_utils.to_hex(None, None, x), 59 | Optional[eth_types.BlockNumber]: eth_utils.to_hex, 60 | Optional[eth_types.BlockIdentifier]: eth_utils.to_hex, 61 | Optional[eth_types.Data]: lambda x: eth_utils.to_hex(None, None, x), 62 | Optional[datetime]: lambda x: eth_utils.to_hex(x.timestamp()), 63 | Optional[int]: eth_utils.to_hex, 64 | Optional[str]: lambda x: eth_utils.to_hex(None, None, x), 65 | Optional[bytes]: eth_utils.to_hex, 66 | } 67 | 68 | 69 | def result_truthiness(result: str) -> Any: 70 | """Parse string to bool, or if there is no bool, return the original.""" 71 | if type(result) == str: 72 | # Results for True/False sometimes return as string. 73 | if result == "False": 74 | return False 75 | elif result == "True": 76 | return True 77 | else: 78 | return result 79 | return result 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fasteth" 3 | version = "0.1.0" 4 | description = "A lightweight, python native, async interface for the Ethereum JSON RPC." 5 | authors = ["0xAlcibiades "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | httpx = {extras = ["http2"], version = "^0.16.1"} 10 | eth-typing = "^2.2.2" 11 | eth-utils = "^1.10.0" 12 | pydantic = "^1.8.1" 13 | eth-hash = {extras = ["pycryptodome"], version = "^0.3.1"} 14 | orjson = "^3.5.1" 15 | eth-account = "^0.5.4" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "^5.2" 19 | black = "^20.8b1" 20 | pre-commit = "^2.10.1" 21 | flake8 = "^3.8.4" 22 | Pygments = "^2.8.0" 23 | docutils = "^0.16" 24 | pytest-asyncio = "^0.14.0" 25 | async_generator = "^1.10" 26 | coverage = "^5.4" 27 | Cython = "^0.29.22" 28 | Sphinx = "^3.5.1" 29 | pytest-benchmark = "^3.2.3" 30 | isort = "^5.7.0" 31 | scalene = "^1.3.0" 32 | mypy = "^0.812" 33 | bpython = "^0.21" 34 | 35 | [build-system] 36 | requires = ["poetry-core>=1.0.0"] 37 | build-backend = "poetry.core.masonry.api" 38 | 39 | [tool.isort] 40 | multi_line_output = 3 41 | include_trailing_comma = true 42 | force_grid_wrap = 0 43 | use_parentheses = true 44 | ensure_newline_before_comments = true 45 | line_length = 88 46 | -------------------------------------------------------------------------------- /tests/test_fasteth.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | from typing import AsyncGenerator, Dict, List, Optional, Type, Union 4 | 5 | import pytest 6 | from eth_utils import ( 7 | encode_hex, 8 | to_bytes, 9 | to_checksum_address, 10 | to_hex, 11 | to_int, 12 | to_normalized_address, 13 | to_text, 14 | ) 15 | from pydantic import BaseModel 16 | 17 | import fasteth 18 | from fasteth import exceptions 19 | from fasteth import models as eth_models 20 | from fasteth import types as eth_types 21 | from fasteth import utils as eth_utils 22 | 23 | # TODO(add more rigorous testing and parametrized testing) 24 | # These are all "golden path" tests, if you will. 25 | 26 | test_address = eth_types.HexAddress( 27 | eth_types.HexStr("0x36273803306a3C22bc848f8Db761e974697ece0d") 28 | ) 29 | test_any_address = "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB" 30 | storage_address = eth_types.HexAddress( 31 | eth_types.HexStr("0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB") 32 | ) 33 | test_data = eth_types.HexStr("0x00") 34 | zero_block = eth_types.HexAddress(eth_types.HexStr(test_data)) 35 | zero_block_hash: eth_types.Hash32 = eth_utils.to_py_converters[eth_types.Hash32]( 36 | zero_block 37 | ) 38 | whisper_client = "2" 39 | # Full: Geth\/v[0-9]+\.[0-9]+\.[0-9]+\-[A-z]+\-[0-9A-f]+\/[A-z-0-9]+\/[A-z-0-9.]+ 40 | infura_client = r"Geth\/v[0-9]+\.[0-9]+\.[0-9]+.+" 41 | ganache_client = r"EthereumJS\sTestRPC\/v[0-9]+\.[0-9]+\.[0-9]+\/ethereum\-js" 42 | test_block_hash = "0xba6c9192229ef4fc8615b510abd2c602f3805b1e51ff8892fb0964e1988ba1e2" 43 | test_hashrate_rate = eth_types.HexStr( 44 | "0x0000000000000000000000000000000000000000000000000000000000500000" 45 | ) 46 | test_hashrate_id = eth_types.HexStr( 47 | "0x59daa26581d0acd1fce254fb7e85952f4c09d0915afd33d3886cd914bc7d283c" 48 | ) 49 | test_hexstring = "0x74657374" # "test" 50 | test_block_num = "0x7c5b7a" 51 | test_block_id = "0xB6D1B0" 52 | test_data_list = ["0x74657374", "0x74657374"] 53 | test_topic_list = ["test", "test"] 54 | latest = "latest" 55 | test_whisper_filter = eth_models.WhisperFilter( 56 | to=eth_utils.to_eth_converters[eth_types.HexAddress](test_address), 57 | topics=test_topic_list, 58 | ) 59 | test_whisper_filter_id = 7 60 | test_whisper_id = 0 61 | test_whisper_address: eth_types.Data = eth_utils.to_py_converters[eth_types.Data]( 62 | test_data 63 | ) 64 | 65 | 66 | class PyableTestBench(eth_models.Ethable, BaseModel): 67 | """Benchmark dataclass for the pyable utility function.""" 68 | 69 | hash32: eth_types.Hash32 70 | address: eth_types.Address 71 | checksumaddress: eth_types.ChecksumAddress 72 | hexaddress: eth_types.HexAddress 73 | hexstring: eth_types.HexStr 74 | data: eth_types.Data 75 | blocknumber: eth_types.BlockNumber 76 | integer: int 77 | # Cannot be set through dacite 78 | data_list: Optional[list[eth_types.Data]] = None 79 | anyaddress: Optional[eth_types.Address] = None 80 | blockid: Optional[eth_types.BlockIdentifier] = None 81 | 82 | def dict(self: Type[eth_models.T]) -> Dict: 83 | """Benchmark without AutoEthable.""" 84 | return { 85 | "hash32": encode_hex(self.hash32), 86 | "address": encode_hex(self.address), 87 | "checksumaddress": to_checksum_address(self.checksumaddress), 88 | "hexaddress": to_normalized_address(self.hexaddress), 89 | "anyaddress": to_normalized_address(self.anyaddress), 90 | "hexstring": to_hex(None, None, self.hexstring), 91 | "data": to_hex(None, None, self.data), 92 | "data_list": list(map(lambda z: to_hex(None, None, z), self.data_list)), 93 | "blocknumber": to_hex(self.blocknumber), 94 | "integer": to_hex(self.integer), 95 | "blockid": to_hex(self.blockid), 96 | } 97 | 98 | @classmethod 99 | def parse_obj(cls: Type[eth_models.T], data: Dict): 100 | data["hash32"] = to_bytes(hexstr=data["hash32"]) 101 | data["address"] = to_bytes(None, data["address"]) 102 | data["hexaddress"] = to_normalized_address(data["hexaddress"]) 103 | data["checksumaddress"] = data["checksumaddress"] 104 | data["anyaddress"] = to_bytes(None, data["anyaddress"]) 105 | data["hexstring"] = to_text(None, data["hexstring"]) 106 | data["data"] = to_text(None, data["data"]) 107 | data["data_list"] = list(map(lambda z: to_text(z), data["data_list"])) 108 | data["blocknumber"] = to_int(None, data["blocknumber"]) 109 | data["integer"] = to_int(None, data["integer"]) 110 | data["blockid"] = to_int(None, data["blockid"]) 111 | return cls(**data) 112 | 113 | 114 | class PyableTest(eth_models.AutoEthable): 115 | """Test dataclass for the pyable utility function.""" 116 | 117 | hash32: eth_types.Hash32 118 | address: eth_types.Address 119 | checksumaddress: eth_types.ChecksumAddress 120 | hexaddress: eth_types.HexAddress 121 | anyaddress: eth_types.Address 122 | hexstring: eth_types.HexStr 123 | data: eth_types.Data 124 | data_list: List[eth_types.Data] 125 | blocknumber: eth_types.BlockNumber 126 | blockid: eth_types.BlockIdentifier 127 | integer: int 128 | 129 | 130 | def test_version(): 131 | assert fasteth.__version__ == "0.1.0" 132 | 133 | 134 | # TODO(Add a benchmark here for an explicitly defined conversion to and from) 135 | # both of these are using the type system to perform a conversion. 136 | class BenchConversionData: 137 | def __init__(self): 138 | self.result = { 139 | "hash32": test_block_hash.lower(), 140 | "address": test_address.lower(), 141 | "hexaddress": test_address.lower(), 142 | "checksumaddress": test_address, 143 | "hexstring": test_hexstring.lower(), 144 | "data": test_hexstring.lower(), 145 | "blocknumber": hex(56333), 146 | "integer": hex(9000), 147 | "data_list": test_data_list, 148 | "anyaddress": test_any_address.lower(), 149 | "blockid": test_block_num, 150 | } 151 | self.result_expect = { 152 | "hash32": to_bytes(None, self.result["hash32"]), 153 | "address": to_bytes(None, self.result["address"]), 154 | "hexaddress": to_normalized_address(self.result["hexaddress"]), 155 | "checksumaddress": to_checksum_address(self.result["checksumaddress"]), 156 | "anyaddress": to_bytes(None, self.result["anyaddress"]), 157 | "hexstring": to_text(None, self.result["hexstring"]), 158 | "data": to_text(None, self.result["data"]), 159 | "data_list": list(map(lambda z: to_text(z), self.result["data_list"])), 160 | "blocknumber": to_int(None, self.result["blocknumber"]), 161 | "integer": to_int(None, self.result["integer"]), 162 | "blockid": to_int(None, self.result["blockid"]), 163 | } 164 | 165 | 166 | @pytest.fixture(scope="module") 167 | def bench_data(): 168 | return BenchConversionData() 169 | 170 | 171 | def assert_keys_equal(obj: Union[PyableTest, PyableTestBench], exp: Dict): 172 | # Test Pyable coversions properly converted. 173 | assert obj.hash32 == exp["hash32"] 174 | assert obj.address == exp["address"] 175 | assert obj.hexstring == exp["hexstring"] 176 | assert obj.hexaddress == exp["hexaddress"] 177 | assert obj.anyaddress == exp["anyaddress"] 178 | assert obj.checksumaddress == exp["checksumaddress"] 179 | assert obj.data == exp["data"] 180 | assert obj.data_list == exp["data_list"] 181 | assert obj.blocknumber == exp["blocknumber"] 182 | assert obj.integer == exp["integer"] 183 | assert obj.blockid == exp["blockid"] 184 | 185 | 186 | def assert_dict_equal(test: Dict, exp: Dict): 187 | # Test Pyable coversions properly converted. 188 | assert test["hash32"] == exp["hash32"] 189 | assert test["address"] == exp["address"] 190 | assert test["hexstring"] == exp["hexstring"] 191 | assert test["hexaddress"] == exp["hexaddress"] 192 | assert test["anyaddress"] == exp["anyaddress"] 193 | assert test["checksumaddress"] == exp["checksumaddress"] 194 | assert test["data"] == exp["data"] 195 | assert test["data_list"] == exp["data_list"] 196 | assert test["blocknumber"] == exp["blocknumber"] 197 | assert test["integer"] == exp["integer"] 198 | assert test["blockid"] == exp["blockid"] 199 | 200 | 201 | def test_ethable_pyable(benchmark, bench_data): 202 | 203 | pyable_test = PyableTest.parse_obj(bench_data.result.copy()) 204 | 205 | # Test Pyable coversions properly converted. 206 | assert_keys_equal(pyable_test, bench_data.result_expect) 207 | 208 | # Leaving this here as reference on how to do a benchmark. 209 | # The whole function should be decorated with: @pytest.mark.benchmark 210 | # @benchmark 211 | # def convert_result() -> dict: 212 | # """Benchmark of the pyable utility function.""" 213 | # pyable_t = PyableTest.parse_obj(bench_data.result.copy()) 214 | # return pyable_t.dict() 215 | 216 | rebuilt = pyable_test.dict() 217 | # Assert roundtrip. 218 | assert_dict_equal(bench_data.result, rebuilt) 219 | 220 | 221 | def test_explicit(benchmark, bench_data): 222 | 223 | pyable_test = PyableTestBench.parse_obj(bench_data.result.copy()) 224 | 225 | # Test Pyable coversions properly converted. 226 | assert_keys_equal(pyable_test, bench_data.result_expect) 227 | 228 | rebuilt = pyable_test.dict() 229 | 230 | # Assert roundtrip. 231 | assert_dict_equal(bench_data.result, rebuilt) 232 | 233 | 234 | def test_transaction_dataclass(): 235 | transaction_data = { 236 | "from_address": storage_address.lower(), 237 | "to": test_address.lower(), 238 | "data": test_data, 239 | "gasPrice": hex(20000), 240 | } 241 | 242 | transaction = eth_models.Transaction.parse_obj(transaction_data.copy()) 243 | eth_payload: dict = transaction.dict() 244 | 245 | assert "from" in eth_payload 246 | assert "to" in eth_payload 247 | assert "gasPrice" in eth_payload 248 | assert eth_payload["from"] == transaction_data["from_address"] 249 | assert eth_payload["to"] == transaction_data["to"] 250 | assert eth_payload["gasPrice"] == transaction_data["gasPrice"] 251 | 252 | 253 | @pytest.fixture(scope="module") 254 | def event_loop(): 255 | """Create an instance of the default event loop for each test case.""" 256 | loop = asyncio.get_event_loop_policy().new_event_loop() 257 | yield loop 258 | loop.close() 259 | 260 | 261 | @pytest.fixture(scope="module") 262 | async def async_rpc() -> AsyncGenerator: 263 | """Returns an AsyncEthereumJSONRPC instance.""" 264 | # This fixture is reused for the entire module test run. 265 | # Temporary Infura ID 266 | # TODO(delete this infura project later) 267 | jsonrpc = fasteth.AsyncEthereumJSONRPC() 268 | yield jsonrpc 269 | 270 | 271 | @pytest.mark.asyncio 272 | async def test_client_version(async_rpc: fasteth.AsyncEthereumJSONRPC): 273 | """Test getting the client version.""" 274 | client_version = await async_rpc.client_version() 275 | assert isinstance(client_version, str) 276 | assert re.match(infura_client, client_version) or re.match( 277 | ganache_client, client_version 278 | ) 279 | 280 | 281 | @pytest.mark.asyncio 282 | async def test_sha3(async_rpc: fasteth.AsyncEthereumJSONRPC): 283 | """Test getting a sha3/Keccak-256 hash.""" 284 | data_to_hash = "0x68656c6c6f20776f726c64" 285 | hashed = "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad" 286 | hashed_ret = await async_rpc.sha3( 287 | eth_utils.to_py_converters[eth_types.Data](data_to_hash) 288 | ) 289 | assert hashed == eth_utils.to_eth_converters[eth_types.Hash32](hashed_ret) 290 | 291 | 292 | @pytest.mark.asyncio 293 | async def test_network_version(async_rpc: fasteth.AsyncEthereumJSONRPC): 294 | """Test getting the network version.""" 295 | network_version = await async_rpc.network_version() 296 | assert ( 297 | network_version == eth_models.Network.Rinkeby 298 | or network_version == eth_models.Network.Ganache 299 | ) 300 | 301 | 302 | @pytest.mark.asyncio 303 | async def test_network_listening(async_rpc: fasteth.AsyncEthereumJSONRPC): 304 | """Test getting the network version.""" 305 | network_listening = await async_rpc.network_listening() 306 | assert network_listening 307 | 308 | 309 | @pytest.mark.asyncio 310 | async def test_network_peer_count(async_rpc: fasteth.AsyncEthereumJSONRPC): 311 | """Test getting the network version.""" 312 | peer_count = await async_rpc.network_peer_count() 313 | assert isinstance(peer_count, int) 314 | 315 | 316 | @pytest.mark.asyncio 317 | async def test_protocol_version(async_rpc: fasteth.AsyncEthereumJSONRPC): 318 | """Test getting the network version.""" 319 | protocol_version = await async_rpc.protocol_version() 320 | assert protocol_version >= 65 321 | 322 | 323 | @pytest.mark.asyncio 324 | async def test_syncing(async_rpc: fasteth.AsyncEthereumJSONRPC): 325 | """Test getting the network sync status.""" 326 | # Our test client should not by in a syncing state. 327 | sync_status = await async_rpc.syncing() 328 | assert not sync_status.syncing 329 | 330 | 331 | @pytest.mark.asyncio 332 | async def test_coinbase(async_rpc: fasteth.AsyncEthereumJSONRPC): 333 | """Test getting the coinbase address for the eth client""" 334 | # We expect this to fail, as our test client does not have a coinbase address. 335 | try: 336 | await async_rpc.coinbase() 337 | except exceptions.JSONRPCError: 338 | pass 339 | 340 | 341 | @pytest.mark.asyncio 342 | async def test_mining(async_rpc: fasteth.AsyncEthereumJSONRPC): 343 | """Test checking if eth client is mining""" 344 | # We our test client to not be mining. 345 | result = await async_rpc.mining() 346 | # We really only care about the result. 347 | assert isinstance(result, bool) 348 | 349 | 350 | @pytest.mark.asyncio 351 | async def test_hashrate(async_rpc: fasteth.AsyncEthereumJSONRPC): 352 | """Test getting client hashrate.""" 353 | assert (await async_rpc.hashrate()) == 0 354 | 355 | 356 | @pytest.mark.asyncio 357 | async def test_gas_price(async_rpc: fasteth.AsyncEthereumJSONRPC): 358 | """Test getting the gas price in wei.""" 359 | gas_price = await async_rpc.gas_price() 360 | assert isinstance(gas_price, int) 361 | 362 | 363 | @pytest.mark.asyncio 364 | async def test_accounts(async_rpc: fasteth.AsyncEthereumJSONRPC): 365 | """Test getting the accounts owned by client.""" 366 | accounts = await async_rpc.accounts() 367 | assert isinstance(accounts, list) 368 | 369 | 370 | @pytest.mark.asyncio 371 | async def test_block_number(async_rpc: fasteth.AsyncEthereumJSONRPC): 372 | """Test getting the network version.""" 373 | block_number = await async_rpc.block_number() 374 | # TODO(figure out with fasteth.eth_typing.BlockNumber fails here) 375 | assert isinstance(block_number, int) 376 | 377 | 378 | @pytest.mark.asyncio 379 | async def test_get_balance(async_rpc: fasteth.AsyncEthereumJSONRPC): 380 | """Test getting balance of an account.""" 381 | balance = await async_rpc.get_balance(address=test_address) 382 | assert isinstance(balance, int) 383 | 384 | 385 | @pytest.mark.asyncio 386 | async def test_get_storage_at(async_rpc: fasteth.AsyncEthereumJSONRPC): 387 | """Test getting storage for an address at a given position.""" 388 | # data = await async_rpc.get_storage_at(storage_address, test_data, latest) 389 | # TODO(Find a better test address and position.) 390 | # assert data == test_data 391 | pass 392 | 393 | 394 | @pytest.mark.asyncio 395 | async def test_get_transaction_count(async_rpc: fasteth.AsyncEthereumJSONRPC): 396 | """Test getting transaction count for a given address.""" 397 | assert isinstance((await async_rpc.get_transaction_count(test_address)), int) 398 | 399 | 400 | @pytest.mark.asyncio 401 | async def test_get_block_transaction_count_by_hash( 402 | async_rpc: fasteth.AsyncEthereumJSONRPC, 403 | ): 404 | """Test getting the block transaction count by hash.""" 405 | assert isinstance( 406 | (await async_rpc.get_block_transaction_count_by_hash(zero_block_hash)), int, 407 | ) 408 | 409 | 410 | @pytest.mark.asyncio 411 | async def test_get_block_transaction_count_by_number( 412 | async_rpc: fasteth.AsyncEthereumJSONRPC, 413 | ): 414 | """Test getting the block transaction count by number.""" 415 | assert isinstance( 416 | ( 417 | await async_rpc.get_block_transaction_count_by_number( 418 | block_identifier=eth_types.BlockNumber(0) 419 | ) 420 | ), 421 | int, 422 | ) 423 | 424 | 425 | @pytest.mark.asyncio 426 | async def test_get_uncle_count_by_block_hash(async_rpc: fasteth.AsyncEthereumJSONRPC): 427 | """Test getting the uncle block count by hash.""" 428 | assert isinstance( 429 | (await async_rpc.get_uncle_count_by_block_hash(zero_block_hash)), int, 430 | ) 431 | 432 | 433 | @pytest.mark.asyncio 434 | async def test_get_uncle_count_by_block_number( 435 | async_rpc: fasteth.AsyncEthereumJSONRPC, 436 | ): 437 | """Test getting the block uncle count by number.""" 438 | assert isinstance( 439 | ( 440 | await async_rpc.get_uncle_count_by_block_number( 441 | block_identifier=test_block_num 442 | ) 443 | ), 444 | int, 445 | ) 446 | 447 | 448 | @pytest.mark.asyncio 449 | async def test_get_code(async_rpc: fasteth.AsyncEthereumJSONRPC): 450 | """Test getting code from a given address at a given block.""" 451 | storage_contents = await async_rpc.get_code(storage_address) 452 | assert type(storage_contents) == str 453 | 454 | 455 | @pytest.mark.asyncio 456 | async def test_sign(async_rpc: fasteth.AsyncEthereumJSONRPC): 457 | """Test signing and returning the signature.""" 458 | # We expect this to fail because it is unsupported on our test endpoint. 459 | try: 460 | await async_rpc.sign(address=test_address, message=test_data) 461 | except fasteth.exceptions.JSONRPCError: 462 | pass 463 | 464 | 465 | @pytest.mark.asyncio 466 | async def test_sign_transaction(async_rpc: fasteth.AsyncEthereumJSONRPC): 467 | """Test signing a transaction and returning the signed transaction.""" 468 | transaction = eth_models.Transaction(from_address=storage_address, data=test_data) 469 | # We expect this to fail because it is unsupported on our test endpoint. 470 | try: 471 | await async_rpc.sign_transaction(transaction=transaction) 472 | except fasteth.exceptions.JSONRPCError: 473 | pass 474 | 475 | 476 | @pytest.mark.asyncio 477 | async def test_send_transaction(async_rpc: fasteth.AsyncEthereumJSONRPC): 478 | """Test signing a transaction and returning the signed transaction.""" 479 | transaction = eth_models.Transaction(from_address=storage_address, data=test_data) 480 | # We expect this to fail because it is unsupported on our test endpoint. 481 | try: 482 | await async_rpc.send_transaction(transaction=transaction) 483 | except fasteth.exceptions.JSONRPCError: 484 | pass 485 | 486 | 487 | @pytest.mark.asyncio 488 | async def test_send_raw_transaction(async_rpc: fasteth.AsyncEthereumJSONRPC): 489 | # TODO(Fix this test to use a real tx data that works on Rinkeby) 490 | try: 491 | await async_rpc.send_raw_transaction( 492 | eth_types.HexStr( 493 | "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8e" 494 | "b970870f072445675" 495 | ) 496 | ) 497 | except fasteth.exceptions.JSONRPCError: 498 | pass 499 | 500 | 501 | @pytest.mark.asyncio 502 | async def test_call(async_rpc: fasteth.AsyncEthereumJSONRPC): 503 | """Test call to a contract function without posting a transaction.""" 504 | transaction = eth_models.Transaction(from_address=storage_address, data=test_data) 505 | # TODO(Get working test data in place) 506 | try: 507 | await async_rpc.call(transaction=transaction, block_identifier=latest) 508 | except fasteth.exceptions.JSONRPCError: 509 | pass 510 | 511 | 512 | @pytest.mark.asyncio 513 | async def test_estimate_gas(async_rpc: fasteth.AsyncEthereumJSONRPC): 514 | """Test call to a contract function without posting a transaction.""" 515 | transaction = eth_models.Transaction(from_address=storage_address, data=test_data) 516 | # TODO(Get working test data in place) 517 | try: 518 | await async_rpc.estimate_gas( 519 | transaction=transaction, block_identifier="pending" 520 | ) 521 | except fasteth.exceptions.JSONRPCError: 522 | pass 523 | 524 | 525 | @pytest.mark.asyncio 526 | async def test_get_block_by_hash(async_rpc: fasteth.AsyncEthereumJSONRPC): 527 | """Test getting a block by number.""" 528 | block = await async_rpc.get_block_by_hash(zero_block_hash, True) 529 | assert isinstance(block, eth_models.Block) 530 | 531 | 532 | @pytest.mark.asyncio 533 | async def test_get_block_by_number(async_rpc: fasteth.AsyncEthereumJSONRPC): 534 | """Test getting a block by number.""" 535 | block = await async_rpc.get_block_by_number(eth_types.BlockNumber(0), True) 536 | assert isinstance(block, eth_models.Block) 537 | 538 | 539 | @pytest.mark.asyncio 540 | async def test_submit_hashrate(async_rpc: fasteth.AsyncEthereumJSONRPC): 541 | """Test submitting a hashrate.""" 542 | submitted = await async_rpc.submit_hashrate(test_hashrate_rate, test_hashrate_id) 543 | assert isinstance(submitted, bool) 544 | print(submitted) 545 | 546 | 547 | @pytest.mark.asyncio 548 | async def test_shh_version(async_rpc: fasteth.AsyncEthereumJSONRPC): 549 | """Test getting the client version.""" 550 | try: 551 | client_version = await async_rpc.shh_version() 552 | assert isinstance(client_version, str) 553 | assert client_version == whisper_client 554 | except fasteth.exceptions.JSONRPCError: 555 | pass 556 | 557 | 558 | @pytest.mark.asyncio 559 | async def test_shh_post(async_rpc: fasteth.AsyncEthereumJSONRPC): 560 | """Test sending a whisper message.""" 561 | try: 562 | whisper = eth_models.Message( 563 | from_address=storage_address, 564 | to=test_address, 565 | topics=test_topic_list, 566 | payload=test_data, 567 | priority=100, 568 | ttl=100, 569 | ) 570 | success = await async_rpc.shh_post(whisper) 571 | assert isinstance(success, bool) 572 | assert success is True 573 | except fasteth.exceptions.JSONRPCError: 574 | pass 575 | 576 | 577 | @pytest.mark.asyncio 578 | async def test_shh_new_identity(async_rpc: fasteth.AsyncEthereumJSONRPC): 579 | """Test creating a new whisper identity in the client.""" 580 | try: 581 | identity = await async_rpc.shh_new_identity() 582 | assert isinstance(identity, str) 583 | except fasteth.exceptions.JSONRPCError: 584 | pass 585 | 586 | 587 | @pytest.mark.asyncio 588 | async def test_shh_has_identity(async_rpc: fasteth.AsyncEthereumJSONRPC): 589 | """Test if the client holds the private keys for a given identity.""" 590 | try: 591 | identity = await async_rpc.shh_has_identity(test_whisper_address) 592 | assert isinstance(identity, bool) 593 | except fasteth.exceptions.JSONRPCError: 594 | pass 595 | 596 | 597 | @pytest.mark.asyncio 598 | async def test_shh_new_group(async_rpc: fasteth.AsyncEthereumJSONRPC): 599 | """Test creating a new whisper group.""" 600 | try: 601 | identity = await async_rpc.shh_new_group() 602 | assert isinstance(identity, str) 603 | except fasteth.exceptions.JSONRPCError: 604 | pass 605 | 606 | 607 | @pytest.mark.asyncio 608 | async def test_shh_add_to_group(async_rpc: fasteth.AsyncEthereumJSONRPC): 609 | """Test if an identity was added to a group.""" 610 | try: 611 | identity = await async_rpc.shh_add_to_group(test_whisper_address) 612 | assert isinstance(identity, bool) 613 | except fasteth.exceptions.JSONRPCError: 614 | pass 615 | 616 | 617 | @pytest.mark.asyncio 618 | async def test_shh_new_filter(async_rpc: fasteth.AsyncEthereumJSONRPC): 619 | """Test creating a new filter.""" 620 | try: 621 | identity = await async_rpc.shh_new_filter(test_whisper_filter) 622 | assert isinstance(identity, int) 623 | except fasteth.exceptions.JSONRPCError: 624 | pass 625 | 626 | 627 | @pytest.mark.asyncio 628 | async def test_shh_uninstall_filter(async_rpc: fasteth.AsyncEthereumJSONRPC): 629 | """Test uninstalling a filter.""" 630 | try: 631 | # We need a filter installed 632 | identity = await async_rpc.shh_new_filter(test_whisper_filter) 633 | success = await async_rpc.shh_uninstall_filter(identity) 634 | assert isinstance(success, bool) 635 | assert success is True 636 | except fasteth.exceptions.JSONRPCError: 637 | pass 638 | 639 | 640 | @pytest.mark.asyncio 641 | async def test_get_shh_filter_changes(async_rpc: fasteth.AsyncEthereumJSONRPC): 642 | """Test polling method for whisper messages.""" 643 | try: 644 | await async_rpc.get_shh_filter_changes(test_whisper_filter_id) 645 | except fasteth.exceptions.JSONRPCError: 646 | pass 647 | 648 | 649 | @pytest.mark.asyncio 650 | async def test_get_shh_messages(async_rpc: fasteth.AsyncEthereumJSONRPC): 651 | """Test getting all messages matching a filter.""" 652 | try: 653 | await async_rpc.get_shh_messages(test_whisper_filter_id) 654 | except fasteth.exceptions.JSONRPCError: 655 | pass 656 | 657 | 658 | if __name__ == "__main__": 659 | """For running directly via CLI.""" 660 | import sys 661 | 662 | import pytest 663 | 664 | pytest.main(sys.argv) 665 | --------------------------------------------------------------------------------