├── .coveragerc ├── .gitignore ├── .gitlint.yaml ├── .travis.yml ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── MANIFEST ├── README.rst ├── __init__.py ├── docs ├── .gitignore ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── pyetherscan ├── __init__.py ├── client.py ├── error.py ├── ethereum.py ├── response.py └── settings.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_client.py ├── test_ethereum.py └── test_response.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *test.py 4 | */tests/* 5 | */docs/* 6 | */python?.?/* 7 | */lib-python/?.?/*.py 8 | */lib_pypy/_*.py 9 | 10 | [report] 11 | # Regexes for lines to exclude from consideration 12 | exclude_lines = 13 | # Have to re-enable the standard pragma 14 | pragma: no cover 15 | 16 | # Don't complain about missing debug-only code: 17 | def __repr__ 18 | if self\.debug 19 | 20 | # Don't complain if tests don't hit defensive assertion code: 21 | raise AssertionError 22 | raise NotImplementedError 23 | 24 | # Don't complain if non-runnable code isn't run: 25 | if 0: 26 | if __name__ == .__main__.: 27 | 28 | ignore_errors = True 29 | -------------------------------------------------------------------------------- /.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 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask stuff: 56 | instance/ 57 | .webassets-cache 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # Jupyter Notebook 68 | .ipynb_checkpoints 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # celery beat schedule file 74 | celerybeat-schedule 75 | 76 | # SageMath parsed files 77 | *.sage.py 78 | 79 | # Environments 80 | .env 81 | .venv 82 | env/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | .spyproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | # mkdocs documentation 94 | /site 95 | 96 | # mypy 97 | .mypy_cache/ 98 | .DS_Store 99 | 100 | # vim 101 | *.swp 102 | -------------------------------------------------------------------------------- /.gitlint.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013-2014 Sebastian Kreft 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Regular expression matchers like \d, \w, must be escaped as in \\d, \\w. 16 | # If you need to include a string like '{}' or '{foo}', you need to double the 17 | # braces, as in '{{}}' or '{{foo}}'. See the pylint configuration for an 18 | # example. 19 | 20 | # CSS 21 | # Sample output: 22 | # /home/skreft/opensource/git-lint/test/e2etest/data/csslint/error.css: line 3, col 2, Warning - Duplicate property 'width' found. 23 | csslint: 24 | extensions: 25 | - .css 26 | command: csslint 27 | arguments: 28 | - "--ignore=ids,box-model,adjoining-classes,qualified-headings,unique-headings,zero-units" 29 | - "--format=compact" 30 | filter: "^{filename}: line (?P{lines}), col (?P\\d+)?, (?P\\S+) - (?P.+)" 31 | installation: "Go to https://github.com/stubbornella/csslint/wiki/Command-line-interface for installation instructions." 32 | 33 | # SCSS 34 | # Sample output: 35 | # /home/skreft/opensource/git-lint/test/e2etest/data/scss/error.scss:2 [W] `0px` should be written without units as `0` 36 | scss: 37 | extensions: 38 | - .scss 39 | command: scss-lint 40 | filter: "^{filename}:(?P{lines}) \\[(?P.+)\\] (?P.+)" 41 | installation: "gem install scss-lint or go to https://github.com/causes/scss-lint" 42 | 43 | # Javascript 44 | # Sample output: 45 | # Line 1, E:0002: Missing space before "=" 46 | gjslint: 47 | extensions: 48 | - .js 49 | command: gjslint 50 | filter: "^Line\\s+(?P{lines}), (?P[^: ]+):((?P\\d+):)? (?P.+)" 51 | installation: "Run pip install http://closure-linter.googlecode.com/files/closure_linter-latest.tar.gz, or visit https://developers.google.com/closure/utilities/docs/linter_howto for installation instructions." 52 | 53 | # Sample output: 54 | # /home/skreft/opensource/git-lint/test/e2etest/data/jshint/error.js: line 1, col 3, Use '===' to compare with ''. 55 | jshint: 56 | extensions: 57 | - .js 58 | command: jshint 59 | arguments: 60 | - "--config" 61 | - "{DEFAULT_CONFIGS}/jshint.json" 62 | filter: "^{filename}: line (?P{lines}), col (?P\\d+), (?P.+)" 63 | installation: "Visit http://www.jshint.com/install/ for installation instructions." 64 | 65 | # PHP 66 | # Sample output: 67 | # PHP Parse error: syntax error, unexpected 'bar' (T_STRING) in /home/skreft/opensource/git-lint/test/e2etest/data/php/error.php on line 3 68 | php: 69 | extensions: 70 | - .php 71 | command: php 72 | arguments: 73 | - "-l" 74 | filter: "^(?P.*) in {filename} on line (?P\\d+)" 75 | installation: "You first need to install PHP." 76 | 77 | # Sample output: 78 | # 2 | ERROR | Expected "if (...) {\n"; found "if (...) {" 79 | phpcs: 80 | extensions: 81 | - .php 82 | command: phpcs 83 | arguments: 84 | - "--report-width=1000" 85 | - "--standard=PSR2" 86 | filter: "^\\s*(?P{lines})\\s+[|]\\s+(?P\\S+)\\s+[|]\\s+(?P.+)" 87 | installation: "Visit https://github.com/squizlabs/PHP_CodeSniffer for installation instructions" 88 | 89 | # Python 90 | pylint: 91 | extensions: 92 | - .py 93 | command: pylint 94 | arguments: 95 | - "--rcfile={DEFAULT_CONFIGS}/pylintrc" 96 | - "--output-format=text" 97 | - "--msg-template='{{abspath}}:{{line}}:{{column}}: [{{category}}:{{symbol}}] {{obj}}: {{msg}}'" 98 | - "--reports=n" 99 | filter: "^{filename}:(?P{lines}):((?P\\d+):)? \\[(?P.+):(?P\\S+)\\]\\s+(: )?(?P.+)$" 100 | installation: "Run pip install pylint." 101 | 102 | # Sample output: gitlint/__init__.py:68:80: E501 line too long (80 > 79 characters) 103 | pep8: 104 | extensions: 105 | - .py 106 | command: pep8 107 | arguments: 108 | - "--max-line-length=80" 109 | filter: "^{filename}:(?P{lines}):((?P\\d+):)? (?P\\S+) (?P.+)$" 110 | installation: "Run pip install pep8." 111 | 112 | # JSON 113 | # Sample output: 114 | # Expecting property name: line 3 column 5 (char 15) 115 | json: 116 | extensions: 117 | - .json 118 | command: python 119 | arguments: 120 | - "-m" 121 | - "json.tool" 122 | # enforce that here comes a colon 123 | filter: "^(?P[^:]+(?=: line \\d+ column \\d+)|No JSON object could be decoded)(: line (?P\\d+) column (?P\\d+).*)?$" 124 | installation: "Nothing else should be required." 125 | 126 | # RST 127 | # Sample output: 128 | # /home/skreft/opensource/git-lint/test/e2etest/data/rst/error.rst:3: (WARNING/2) Inline interpreted text or phrase reference start-string without end-string. 129 | rst: 130 | extensions: 131 | - .rst 132 | command: rst2html.py 133 | filter: "^{filename}:(?P{lines}): [(](?P.+)[)] (?P.+)" 134 | installation: "Run pip install docutils." 135 | 136 | # PNG 137 | pngcrush: 138 | extensions: 139 | - .png 140 | command: pngcrush-linter.sh 141 | requirements: 142 | - pngcrush 143 | filter: "(?P.+)$" 144 | installation: "Run apt-get install pngcrush." 145 | 146 | optipng: 147 | extensions: 148 | - .png 149 | command: optipng-linter.sh 150 | requirements: 151 | - optipng 152 | filter: "(?P.+)$" 153 | installation: "Run apt-get install optipng." 154 | 155 | # JPEG 156 | jpegtran: 157 | extensions: 158 | - .jpg 159 | - .jpeg 160 | command: jpegtran-linter.sh 161 | requirements: 162 | - jpegtran 163 | filter: "(?P.+)" 164 | installation: "Run apt-get install jpegtran." 165 | 166 | # SHELL scripts 167 | # Sample output 168 | # /home/skreft/opensource/git-lint/test/e2etest/data/bash/error.sh: line 3: syntax error: unexpected end of file 169 | bash: 170 | extensions: 171 | - .sh 172 | command: bash 173 | arguments: 174 | - "-n" 175 | filter: "{filename}: line (?P{lines}): (?P.+)" 176 | installation: "Please install bash in your system." 177 | 178 | # YAML 179 | yaml: 180 | extensions: 181 | - .yaml 182 | - .yml 183 | command: yamllint 184 | arguments: 185 | - "--format" 186 | - "parsable" 187 | - "--config-data" 188 | - "relaxed" 189 | # Matches either: 190 | # - syntax error, on any line 191 | # - other error, on a modified line only 192 | filter: "^{filename}:(?P{lines}|\\d+\ 193 | (?=:\\d+: \\[error\\] syntax error:)):(?P\\d+): \ 194 | \\[(?P\\S+)\\] (?P.+)$" 195 | installation: "Run pip install yamllint." 196 | 197 | # INI 198 | ini: 199 | extensions: 200 | - .ini 201 | command: ini_linter.py 202 | filter: "(?P.+)$" 203 | installation: "" 204 | 205 | # HTML 206 | # Sample output: 207 | # line 2 column 1 - Warning: missing before 208 | tidy: 209 | extensions: 210 | - .html 211 | command: tidy-wrapper.sh 212 | requirements: 213 | - tidy 214 | - remove_template.py 215 | - grep 216 | arguments: 217 | - "-qe" 218 | - "--drop-empty-elements" 219 | - "false" 220 | installation: "Visit https://w3c.github.io/tidy-html5/" 221 | filter: "^line (?P{lines}) column (?P\\d+) - (?P[^:]+): (?P.+)" 222 | 223 | # Sample output: 224 | # 1:10: Error: Javascript ... 225 | html_lint: 226 | extensions: 227 | - .html 228 | command: html_lint.py 229 | arguments: 230 | - "--disable" 231 | - "optional_tag" 232 | installation: "pip install html-linter." 233 | filter: "^(?P{lines}):(?P\\d+): (?P\\S+): (?P.+)" 234 | 235 | # Ruby 236 | # Sample output: 237 | # error.rb: warning: line 1, column 1: unused constant FOO 238 | rubylint: 239 | command: ruby-lint 240 | arguments: 241 | - "--analysis" 242 | - "argument_amount,loop_keywords,pedantics,shadowing_variables,unused_variables,useless_equality_checks" 243 | extensions: 244 | - '.rb' 245 | # The first component is the basename, but it's not supported yet. 246 | filter: ".*: (?P.+): line (?P{lines}), column (?P\\d+): (?P.+)" 247 | installation: "sudo gem install ruby-lint (requires ruby 1.9) or visit https://github.com/yorickpeterse/ruby-lint" 248 | 249 | 250 | # Sample output with the --format emacs option: 251 | # /home/skreft/opensource/git-lint/test/e2etest/data/rubocop/error.rb:1:4: C: Surrounding space missing for operator '='. 252 | rubocop: 253 | command: rubocop 254 | arguments: 255 | - '--format' 256 | - 'emacs' 257 | - '--rails' 258 | extensions: 259 | - '.rb' 260 | # The first component is the relpath, but it's not supported yet. 261 | filter: "{filename}:(?P{lines}):(?P\\d+): (?P.+): (?P.+)" 262 | installation: "sudo gem install rubocop or visit https://github.com/bbatsov/rubocop" 263 | 264 | # Java 265 | # Sample output: 266 | # /home/skreft/opensource/git-lint/test/e2etest/data/checkstyle/error.java:0: Missing package-info.java file. 267 | # /home/skreft/opensource/git-lint/test/e2etest/data/checkstyle/error.java:1:7: warning: Name 'foo' must match pattern '^[A-Z][a-zA-Z0-9]*$'. 268 | checkstyle: 269 | command: checkstyle 270 | extensions: 271 | - .java 272 | requirements: 273 | - java 274 | arguments: 275 | - "-c" 276 | - "{DEFAULT_CONFIGS}/checkstyle.xml" 277 | filter: "{filename}:(?P{lines}):((?P\\d+):)? (?P.+)" 278 | installation: "sudo apt-get install checkstyle or go to http://checkstyle.sourceforge.net/cmdline.html" 279 | 280 | # Sample output: 281 | # /home/skreft/opensource/git-lint/test/e2etest/data/pmd/error.java:1: All methods are static. 282 | # Disabled rulesets because of false positives 283 | # rulesets/java/coupling.xml: Demeter 284 | # rulesets/java/design.xml: Static class 285 | # rulesets/java/optimizations.xml: Parameter could be final 286 | # rulesets/java/junit.xml: maximum asserts, asserts should have message 287 | pmd: 288 | command: run.sh 289 | extensions: 290 | - .java 291 | requirements: 292 | - java 293 | arguments: 294 | - "pmd" 295 | - "-format" 296 | - "text" 297 | - "-rulesets" 298 | - "rulesets/java/android.xml,rulesets/java/basic.xml,rulesets/java/braces.xml,rulesets/java/clone.xml,rulesets/java/codesize.xml,rulesets/java/empty.xml,rulesets/java/finalizers.xml,rulesets/java/imports.xml,rulesets/java/j2ee.xml,rulesets/java/logging-jakarta-commons.xml,rulesets/java/strictexception.xml,rulesets/java/strings.xml,rulesets/java/sunsecure.xml,rulesets/java/typeresolution.xml,rulesets/java/unnecessary.xml,rulesets/java/unusedcode.xml" 299 | - "-d" 300 | filter: "{filename}:(?P{lines}):\\s+(?P.+)" 301 | installation: "Go to http://pmd.sourceforge.net/pmd-5.1.1/installing.html" 302 | 303 | # Coffeescript 304 | # Sample output: 305 | # /home/skreft/opensource/git-lint/test/e2etest/data/coffeelint/error.coffee,4,,error,Operators must be spaced properly =. 306 | # /home/skreft/opensource/git-lint/test/e2etest/data/coffeelint/error.coffee,5,,error,[stdin]:5:1: error: reserved word 'yes' can't be assigned 307 | coffeelint: 308 | command: coffeelint 309 | extensions: 310 | - .coffee 311 | arguments: 312 | - "--reporter=csv" 313 | - "--file={DEFAULT_CONFIGS}/coffeelint.json" 314 | filter: "{filename},(?P{lines}),.*,(?P.+),(?:\\[stdin\\]:\\d+:(?P\\d+): .*: )?(?P.+)" 315 | installation: "npm install -g coffeelint" 316 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.5-dev" # 3.5 development branch 8 | - "3.6" 9 | - "3.6-dev" # 3.6 development branch 10 | - "3.7-dev" # 3.7 development branch 11 | - "nightly" # currently points to 3.7-dev 12 | 13 | # command to install dependencies 14 | install: 15 | - pip install -r requirements.txt 16 | - pip install coveralls 17 | 18 | # command to run tests 19 | script: 20 | coverage run --source=pyetherscan -m unittest discover -s tests/ 21 | after_success: 22 | coveralls 23 | 24 | notifications: 25 | on_success: never 26 | on_failure: always 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.2] - 2017-07-20 10 | ### Changed 11 | - Fixed the ConfigParser error for python 2+. 12 | 13 | ## [0.1.1] - 2017-07-18 14 | ### Changed 15 | - Fixed python version errors in the `settings.py` file. 16 | 17 | ## [0.1.0] - 2017-07-18 18 | ### Added 19 | - The ability to set `ETHERSCAN_API_KEY` using a configuration file (thanks 20 | to @veox). 21 | - Explicit tests for `pyetherscan.response` objects (vs. the previous implicit 22 | tests via teh `client` and `ethereum` objects). 23 | - Contribution instructions and a virtualenv tutorial link to the README file 24 | 25 | ### Changed 26 | - Added import statements to the packages `__init__.py` file. 27 | - The `TransactionContainer` object enforces typing (a list must be passed). 28 | - The `Block` and `Transaction` objects now return empty lists instead 29 | of `NoneType`'s 30 | - The base `EtherscanResponse` object more explicitly validates API responses 31 | by checking status messages instead of the binary 1/0 status code. This 32 | prevents exceptions from being raised when no data is present (e.g. if 33 | a user has never sent a transaction). 34 | 35 | ## [0.0.2] - 2017-07-16 36 | ### Changed 37 | - Fixed package versioning typos in the `setup.py` file. 38 | 39 | ## [0.0.1] - 2017-07-16 40 | ### Added 41 | - PyPi badge in the `README.rst` file for python versions. 42 | - Clarified environment variable setup in the README file. 43 | - Added path identification support for both python 2 and 3 in the `setup.py` 44 | file to identify the `long_descripton` variable (previously failed for py2). 45 | 46 | [Unreleased]: https://github.com/Marto32/pyetherscan/compare/0.1.2...HEAD 47 | [0.1.2]: https://github.com/Marto32/pyetherscan/compare/0.1.1...0.1.2 48 | [0.1.1]: https://github.com/Marto32/pyetherscan/compare/0.1.0...0.1.1 49 | [0.1.0]: https://github.com/Marto32/pyetherscan/compare/0.0.2...0.1.0 50 | [0.0.2]: https://github.com/Marto32/pyetherscan/compare/0.0.1...0.0.2 51 | [0.0.1]: https://github.com/Marto32/pyetherscan/compare/0.0.0...0.0.1 52 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | * @Marto32 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Martorella 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | pyetherscan/__init__.py 4 | pyetherscan/client.py 5 | pyetherscan/error.py 6 | pyetherscan/ethereum.py 7 | pyetherscan/response.py 8 | pyetherscan/settings.py 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/Marto32/pyetherscan.svg?branch=master 2 | :target: https://travis-ci.org/Marto32/pyetherscan 3 | 4 | .. image:: https://coveralls.io/repos/github/Marto32/pyetherscan/badge.svg?branch=master 5 | :target: https://coveralls.io/github/Marto32/pyetherscan?branch=master 6 | 7 | .. image:: https://img.shields.io/pypi/pyversions/pyetherscan.svg 8 | :target: https://pypi.python.org/pypi/pyetherscan 9 | 10 | .. image:: https://img.shields.io/pypi/v/pyetherscan.svg 11 | :target: https://pypi.python.org/pypi/pyetherscan 12 | 13 | 14 | pyetherscan 15 | =========== 16 | An unofficial wrapper for the `Etherscan `_ API. 17 | 18 | Installation 19 | ============ 20 | We recommend you install this library in a new `virtual environment `_. 21 | 22 | To install, create a new `etherscan account `_ and 23 | make note of your API key. Then install the library by running: 24 | 25 | .. code-block:: python 26 | 27 | pip install pyetherscan 28 | 29 | After installation, there are two main ways to set your API key. The first 30 | is by creating a configuration file named ``.pyetherscan.ini`` and 31 | saving it in your home directory. The format for this file is as follows: 32 | 33 | .. code-block:: none 34 | 35 | [Credentials] 36 | ETHERSCAN_API_KEY: YourApiKeyToken 37 | 38 | The second is by setting the environment variable ``ETHERSCAN_API_KEY``. 39 | 40 | If you do not use either option, the package will connect to the ropsten test 41 | chain via the Etherscan API by default. 42 | 43 | Usage 44 | ===== 45 | There are two main ways to use the library. The first is via the `Client` 46 | object to interact directly with the `Etherscan API `_. 47 | 48 | .. code-block:: python 49 | 50 | In [1]: from pyetherscan import Client 51 | 52 | In [2]: client = Client() 53 | 54 | In [3]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 55 | 56 | In [4]: address_balance = client.get_single_balance(address) 57 | 58 | In [5]: address_balance.response_status_code 59 | Out[5]: 200 60 | 61 | In [6]: address_balance.message 62 | Out[6]: 'OK' 63 | 64 | In [7]: address_balance.balance 65 | Out[7]: 748997604382925139479303 66 | 67 | The second is to use ``pyetherscan`` objects which fully abstract the API. These 68 | objects can be found in the ``pyetherscan.ethereum`` module and include: 69 | 70 | - ``Transaction`` 71 | - ``Address`` 72 | - ``Block`` 73 | - ``Token`` 74 | 75 | For example: 76 | 77 | .. code-block:: python 78 | 79 | In [1]: from pyetherscan import Address 80 | 81 | In [2]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 82 | 83 | In [3]: ethereum_address = Address(address) 84 | 85 | In [4]: ethereum_address.balance 86 | Out[4]: 748997604382925139479303.0 87 | 88 | In [4]: for txn in ethereum_address.transactions: 89 | ...: print(txn.value) 90 | 91 | Contributing 92 | ============ 93 | Fork this repository, create a branch and issue a PR. 94 | 95 | 96 | .. image:: https://badges.gitter.im/pyetherscan/Lobby.svg 97 | :alt: Join the chat at https://gitter.im/pyetherscan/Lobby 98 | :target: https://gitter.im/pyetherscan/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 99 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marto32/pyetherscan/fb9669f731bf58c196d128ebc893dfbb0ab18aa9/__init__.py -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _static 2 | _build 3 | _templates 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = pyetherscan 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyetherscan documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Jul 5 22:15:33 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | import os 16 | import sys 17 | 18 | if os.environ.get('READTHEDOCS', None) == 'True': 19 | # Run sphinx-apidoc automatically in readthedocs 20 | # Taken from this: https://lists.torproject.org/pipermail/tor-commits/2012-September/046695.html 21 | os.system('sphinx-apidoc -o api -T ../pyetherscan --separate') 22 | 23 | # If extensions (or modules to document with autodoc) are in another directory, 24 | # add these directories to sys.path here. If the directory is relative to the 25 | # documentation root, use os.path.abspath to make it absolute, like shown here. 26 | sys.path.insert(0, os.path.abspath(os.path.pardir)) 27 | 28 | # append the __init__ to class definitions 29 | autoclass_content = 'both' 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.todo', 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.autosummary', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'pyetherscan' 61 | copyright = '2017, Michael Martorella' 62 | author = 'Michael Martorella' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = '0.1.0' 70 | # The full version, including alpha/beta/rc tags. 71 | release = '0.1.0' 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | # This patterns also effect to html_static_path and html_extra_path 83 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # If true, `todo` and `todoList` produce output, else they produce nothing. 89 | todo_include_todos = True 90 | 91 | 92 | # -- Options for HTML output ---------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | # 97 | # html_theme = 'alabaster' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | # 103 | # html_theme_options = {} 104 | 105 | # Add any paths that contain custom static files (such as style sheets) here, 106 | # relative to this directory. They are copied after the builtin static files, 107 | # so a file named "default.css" will overwrite the builtin "default.css". 108 | # html_static_path = ['_static'] 109 | 110 | # Custom sidebar templates, must be a dictionary that maps document names 111 | # to template names. 112 | # 113 | # This is required for the alabaster theme 114 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 115 | # html_sidebars = { 116 | # '**': [ 117 | # 'about.html', 118 | # 'navigation.html', 119 | # 'relations.html', # needs 'show_related': True theme option to display 120 | # 'searchbox.html', 121 | # 'donate.html', 122 | # ] 123 | # } 124 | 125 | 126 | # -- Options for HTMLHelp output ------------------------------------------ 127 | 128 | # Output file base name for HTML help builder. 129 | htmlhelp_basename = 'pyetherscandoc' 130 | 131 | 132 | # -- Options for LaTeX output --------------------------------------------- 133 | 134 | latex_elements = { 135 | # The paper size ('letterpaper' or 'a4paper'). 136 | # 137 | # 'papersize': 'letterpaper', 138 | 139 | # The font size ('10pt', '11pt' or '12pt'). 140 | # 141 | # 'pointsize': '10pt', 142 | 143 | # Additional stuff for the LaTeX preamble. 144 | # 145 | # 'preamble': '', 146 | 147 | # Latex figure (float) alignment 148 | # 149 | # 'figure_align': 'htbp', 150 | } 151 | 152 | # Grouping the document tree into LaTeX files. List of tuples 153 | # (source start file, target name, title, 154 | # author, documentclass [howto, manual, or own class]). 155 | latex_documents = [ 156 | (master_doc, 'pyetherscan.tex', 'pyetherscan Documentation', 157 | 'Michael Martorella', 'manual'), 158 | ] 159 | 160 | 161 | # -- Options for manual page output --------------------------------------- 162 | 163 | # One entry per manual page. List of tuples 164 | # (source start file, name, description, authors, manual section). 165 | man_pages = [ 166 | (master_doc, 'pyetherscan', 'pyetherscan Documentation', 167 | [author], 1) 168 | ] 169 | 170 | 171 | # -- Options for Texinfo output ------------------------------------------- 172 | 173 | # Grouping the document tree into Texinfo files. List of tuples 174 | # (source start file, target name, title, author, 175 | # dir menu entry, description, category) 176 | texinfo_documents = [ 177 | (master_doc, 'pyetherscan', 'pyetherscan Documentation', 178 | author, 'pyetherscan', 'One line description of project.', 179 | 'Miscellaneous'), 180 | ] 181 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pyetherscan documentation master file, created by 2 | sphinx-quickstart on Wed Jul 5 22:15:33 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyetherscan's documentation! 7 | ======================================= 8 | 9 | .. include:: ../README.rst 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Contents: 14 | 15 | API Reference 16 | ------------- 17 | 18 | .. autosummary:: 19 | :toctree: api 20 | 21 | pyetherscan 22 | pyetherscan.client 23 | pyetherscan.error 24 | pyetherscan.ethereum 25 | pyetherscan.response 26 | pyetherscan.settings 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=pyetherscan 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /pyetherscan/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package containing core pyetherscan functionality. 3 | """ 4 | 5 | from pyetherscan import client 6 | from pyetherscan.client import Client 7 | 8 | from pyetherscan import error 9 | from pyetherscan.error import ( 10 | EtherscanDataError, 11 | EtherscanInitializationError, 12 | EtherscanConnectionError, 13 | EtherscanRequestError, 14 | EtherscanAddressError, 15 | EtherscanContractError, 16 | EtherscanTransactionError, 17 | EtherscanBlockError, 18 | EtherscanEventLogError, 19 | EtherscanGethProxyError, 20 | EtherscanWebsocketError, 21 | EtherscanTokenError, 22 | EtherscanStatsError, 23 | ) 24 | 25 | from pyetherscan import ethereum 26 | from pyetherscan.ethereum import ( 27 | Transaction, 28 | Address, 29 | Block, 30 | Token, 31 | ) 32 | 33 | from pyetherscan import response 34 | from pyetherscan.response import ( 35 | SingleAddressBalanceResponse, 36 | MultiAddressBalanceResponse, 37 | TransactionsByAddressResponse, 38 | TransactionsByHashResponse, 39 | BlocksMinedByAddressResponse, 40 | ContractABIByAddressResponse, 41 | ContractStatusResponse, 42 | TokenSupplyResponse, 43 | TokenAccountBalanceResponse, 44 | BlockRewardsResponse, 45 | ) 46 | 47 | 48 | __all__ = [ 49 | 'client', 50 | 'Client', 51 | 'error', 52 | 'EtherscanDataError', 53 | 'EtherscanInitializationError', 54 | 'EtherscanConnectionError', 55 | 'EtherscanRequestError', 56 | 'EtherscanAddressError', 57 | 'EtherscanContractError', 58 | 'EtherscanTransactionError', 59 | 'EtherscanBlockError', 60 | 'EtherscanEventLogError', 61 | 'EtherscanGethProxyError', 62 | 'EtherscanWebsocketError', 63 | 'EtherscanTokenError', 64 | 'EtherscanStatsError', 65 | 'ethereum', 66 | 'Transaction', 67 | 'Address', 68 | 'Block', 69 | 'Token', 70 | 'response', 71 | 'SingleAddressBalanceResponse', 72 | 'MultiAddressBalanceResponse', 73 | 'TransactionsByAddressResponse', 74 | 'TransactionsByHashResponse', 75 | 'BlocksMinedByAddressResponse', 76 | 'ContractABIByAddressResponse', 77 | 'ContractStatusResponse', 78 | 'TokenSupplyResponse', 79 | 'TokenAccountBalanceResponse', 80 | 'BlockRewardsResponse', 81 | ] 82 | -------------------------------------------------------------------------------- /pyetherscan/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | Library for connecting to the Etherscan API using a self contained client. 3 | """ 4 | import requests 5 | import sys 6 | 7 | from retrying import retry 8 | from . import error, response, settings 9 | 10 | 11 | def check_exception_for_retry(exception): 12 | """ 13 | Prevent retrying if an etherscan response status is not 1. 14 | """ 15 | data_error = isinstance(exception, error.EtherscanDataError) 16 | request_error = isinstance(exception, error.EtherscanRequestError) 17 | return not data_error and not request_error 18 | 19 | 20 | RETRY_KWARGS = { 21 | 'wait_exponential_multiplier': 1000, 22 | 'wait_exponential_max': 10000, 23 | 'stop_max_attempt_number': 5, 24 | 'retry_on_exception': check_exception_for_retry, 25 | } 26 | 27 | 28 | class Client(object): 29 | """ 30 | Represents an Etherscan API client. 31 | 32 | Initialized using the ETHERSCAN_API_KEY environment variable (or you may 33 | pass the API key as an argument). 34 | 35 | You can use this object to query the Etherscan database for raw data for 36 | each endpoint (see Public Methods below). An example is shown in the 37 | Example Usage section below. 38 | 39 | Public Attributes: 40 | - ``apikey`` 41 | - ``timeout`` 42 | 43 | Public Methods: 44 | - :py:meth:`get_single_balance` 45 | - :py:meth:`get_multi_balance` 46 | - :py:meth:`get_transactions_by_address` 47 | - :py:meth:`get_transaction_by_hash` 48 | - :py:meth:`get_blocks_mined_by_address` 49 | - :py:meth:`get_contract_abi` 50 | 51 | Example Usage: 52 | 53 | .. code-block:: python 54 | 55 | In [1]: client = Client() 56 | 57 | In [2]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 58 | 59 | In [3]: address_balance = client.get_single_balance(address) 60 | 61 | In [4]: address_balance.response_status_code 62 | Out[4]: 200 63 | 64 | In [5]: address_balance.message 65 | Out[5]: 'OK' 66 | 67 | In [6]: address_balance.balance 68 | Out[6]: 748997604382925139479303 69 | 70 | """ 71 | 72 | # Define etherscan API url parameters 73 | _base_url = 'https://api.etherscan.io/' 74 | _test_url = 'https://ropsten.etherscan.io/' 75 | _module = 'api?module={module}' 76 | _action = '&action={action}' 77 | _tag = '&tag={tag}' 78 | _offset = '&offset={offset}' 79 | _page = '&page={page}' 80 | _sort = '&sort={sort}' 81 | _blocktype = '&blocktype={blocktype}' 82 | _key = '&apikey={key}' 83 | _address = '&address={address}' 84 | _startblock = '&startblock={startblock}' 85 | _endblock = '&endblock={endblock}' 86 | _hash = '&txhash={hash}' 87 | _contract_address = '&contractaddress={contract_address}' 88 | _blockno = '&blockno={blockno}' 89 | 90 | # Define etherscan API module names 91 | _account_module = 'account' 92 | _contract_module = 'contract' 93 | _transaction_module = 'transaction' 94 | _block_module = 'block' 95 | _event_log_module = 'logs' 96 | _geth_proxy_module = 'proxy' 97 | _token_module = 'stats' 98 | _stats_module = 'stats' 99 | 100 | def __init__(self, apikey=settings.ETHERSCAN_API_KEY, timeout=5): 101 | self.timeout = timeout 102 | self.apikey = apikey 103 | 104 | if sys.version_info[0] < 3: 105 | accepted_types = (str, unicode) 106 | else: 107 | accepted_types = str 108 | if not isinstance(self.apikey, accepted_types): 109 | raise error.EtherscanInitializationError( 110 | 'You must supply an API key.' 111 | ) 112 | 113 | # If no key is supplied, use the test network 114 | if self.apikey == settings.TESTING_API_KEY: 115 | self._base_url = self._test_url 116 | 117 | if not isinstance(self.timeout, (float, int)): 118 | raise error.EtherscanInitializationError( 119 | 'Timeout seconds must be an integer or decimal.' 120 | ) 121 | 122 | self.key_uri = self._key.format(key=self.apikey) 123 | 124 | def _prep_request(self, url): 125 | payload = { 126 | 'url': url, 127 | 'timeout': self.timeout, 128 | } 129 | return payload 130 | 131 | def __repr__(self): 132 | return '{_class}(apikey=, timeout={_timeout})'.format( 133 | _class=self.__class__.__name__, 134 | _timeout=self.timeout 135 | ) 136 | 137 | @retry(**RETRY_KWARGS) 138 | def _get_request(self, url, response_object): 139 | """ 140 | Makes a standardized GET request. 141 | """ 142 | payload = self._prep_request(url) 143 | resp = requests.get(**payload) 144 | return response_object(resp) 145 | 146 | @retry(**RETRY_KWARGS) 147 | def _post_request(self, url, response_object): 148 | """ 149 | Makes a standardized POST request. 150 | """ 151 | payload = self._prep_request(url) 152 | resp = requests.post(**payload) 153 | return response_object(resp) 154 | 155 | ####################### 156 | # Address API methods # 157 | ####################### 158 | def get_single_balance(self, address): 159 | """ 160 | Obtains the balance for a single address. 161 | 162 | :param address: The ethereum address 163 | :type address: str 164 | :returns: A :py:obj:`response.SingleAddressBalanceResponse` instance 165 | 166 | Example Usage: 167 | 168 | .. code-block:: python 169 | 170 | In [1]: client = Client() 171 | 172 | In [2]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 173 | 174 | In [3]: address_balance = client.get_single_balance(address) 175 | 176 | In [4]: address_balance.balance 177 | Out[4]: 748997604382925139479303 178 | 179 | """ 180 | module_uri = self._module.format(module=self._account_module) 181 | action_uri = self._action.format(action='balance') 182 | address_uri = self._address.format(address=address) 183 | tag_uri = self._tag.format(tag='latest') 184 | 185 | request_url = self._base_url + \ 186 | module_uri + \ 187 | action_uri + \ 188 | address_uri + \ 189 | tag_uri + \ 190 | self.key_uri 191 | 192 | return self._get_request( 193 | url=request_url, 194 | response_object=response.SingleAddressBalanceResponse 195 | ) 196 | 197 | def get_multi_balance(self, addresses): 198 | """ 199 | Obtains the balance for multiple addresses. 200 | 201 | :param addresses: A list of ethereum addresses, each address should 202 | be a string 203 | :type addresses: list 204 | :returns: A :py:obj:`response.MultiAddressBalanceResponse` instance 205 | 206 | Example Usage: 207 | 208 | .. code-block:: python 209 | 210 | In [1]: client = Client() 211 | 212 | In [2]: addresses = addresses = [ 213 | '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', 214 | '0x63a9975ba31b0b9626b34300f7f627147df1f526', 215 | '0x198ef1ec325a96cc354c7266a038be8b5c558f67' 216 | ] 217 | 218 | In [3]: address_balances = client.get_multi_balance(addresses) 219 | 220 | In [4]: address_balances.balances 221 | Out[4]: { 222 | u'0x198ef1ec325a96cc354c7266a038be8b5c558f67': 1.2005264493462224e+22, 223 | u'0x63a9975ba31b0b9626b34300f7f627147df1f526': 3.3256713622282705e+20, 224 | u'0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a': 4.080716856407e+22 225 | } 226 | 227 | """ 228 | if not isinstance(addresses, list): 229 | raise error.EtherscanAddressError( 230 | 'A list must be passed to this method.' 231 | ) 232 | 233 | if len(addresses) > 20: 234 | raise error.EtherscanAddressError( 235 | 'Etherscan takes a maximum of 20 addresses in a single call.' 236 | ) 237 | 238 | _addresses = ','.join(addresses) 239 | module_uri = self._module.format(module=self._account_module) 240 | action_uri = self._action.format(action='balancemulti') 241 | address_uri = self._address.format(address=_addresses) 242 | tag_uri = self._tag.format(tag='latest') 243 | 244 | request_url = self._base_url + \ 245 | module_uri + \ 246 | action_uri + \ 247 | address_uri + \ 248 | tag_uri + \ 249 | self.key_uri 250 | 251 | return self._get_request( 252 | url=request_url, 253 | response_object=response.MultiAddressBalanceResponse 254 | ) 255 | 256 | def get_transactions_by_address(self, address, startblock=None, 257 | endblock=None, sort='asc', offset=None, page=None, internal=False): 258 | """ 259 | Obtains a list of transactions for an ethereum address. 260 | 261 | :param address: The ethereum address 262 | :type address: str 263 | :param startblock: An optional start block to limit transactions 264 | (defaults to None) 265 | :type startblock: int 266 | :param endblock: An optional end block to limit transactions 267 | (defaults to None) 268 | :type endblock: int 269 | :param sort: Sort result set (defaults to asc) 270 | :type sort: str 271 | :param offset: The max number of results (must be used 272 | with ``page``) 273 | :type offset: int 274 | :param page: The page number of the result set to pull (must be used 275 | with ``max_results``) 276 | :type page: int 277 | :param internal: Whether or not to limit transactions to internal 278 | transactions (between contracts) - defaults to False 279 | :type internal: bool 280 | :returns: A :py:obj:`response.TransactionsByAddressResponse` instance 281 | 282 | Example Usage: 283 | 284 | .. code-block:: python 285 | 286 | In [1]: client = Client() 287 | 288 | In [2]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 289 | 290 | In [3]: address_transactions = client.get_transactions_by_address(address) 291 | 292 | In [4]: address_transactions.transactions 293 | Out[4]: [ 294 | { 295 | u'nonce': u'0', 296 | u'contractAddress': u'0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', 297 | u'cumulativeGasUsed': u'1436963', 298 | u'hash': u'0x9c81f44c29ff0226f835cd0a8a2f2a7eca6db52a711f8211b566fd15d3e0e8d4', 299 | u'blockHash': u'0xd3cabad6adab0b52eb632c386ea194036805713682c62cb589b5abcd76de2159', 300 | u'timeStamp': u'1439048640', 301 | u'gas': u'2000000', 302 | u'value': u'11901464239480000000000000', 303 | u'blockNumber': u'54092', 304 | u'to': u'', 305 | u'confirmations': u'3921579', 306 | u'input': u'0x606060405260....' 307 | }, { 308 | ... 309 | }, { 310 | ... 311 | } 312 | ] 313 | 314 | """ 315 | module_uri = self._module.format(module=self._account_module) 316 | action = 'txlistinternal' if internal else 'txlist' 317 | action_uri = self._action.format(action=action) 318 | address_uri = self._address.format(address=address) 319 | 320 | if startblock is None: 321 | startblock_uri = '' 322 | else: 323 | startblock_uri = self._startblock.format(startblock=startblock) 324 | 325 | if endblock is None: 326 | endblock_uri = '' 327 | else: 328 | endblock_uri = self._endblock.format(endblock=endblock) 329 | 330 | sort_uri = self._sort.format(sort=sort) 331 | 332 | # If page or offset are set, _both_ must be set 333 | if page is not None or offset is not None: 334 | _both_set = page is not None and offset is not None 335 | if not _both_set: 336 | raise error.EtherscanTransactionError( 337 | 'If using page or offset, both must be set.' 338 | ) 339 | else: 340 | page_uri = self._page.format(page=page) 341 | offset_uri = self._offset.format(offset=offset) 342 | else: 343 | page_uri = self._page.format(page='') 344 | offset_uri = self._offset.format(offset='') 345 | 346 | request_url = self._base_url + \ 347 | module_uri + \ 348 | action_uri + \ 349 | address_uri + \ 350 | startblock_uri + \ 351 | endblock_uri + \ 352 | page_uri + \ 353 | offset_uri + \ 354 | sort_uri + \ 355 | self.key_uri 356 | 357 | return self._get_request( 358 | url=request_url, 359 | response_object=response.TransactionsByAddressResponse 360 | ) 361 | 362 | def get_transaction_by_hash(self, transaction_hash, startblock=None, 363 | endblock=None, sort='asc', offset=None, page=None): 364 | """ 365 | Obtains transaction details for a single transaction. 366 | 367 | :param hash: The ethereum transaction hash 368 | :type hash: hash 369 | :returns: A :py:obj:`response.TransactionsByHashResponse` instance 370 | 371 | Example Usage: 372 | 373 | .. code-block:: python 374 | 375 | In [1]: client = Client() 376 | 377 | In [2]: hash = '0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170' 378 | 379 | In [3]: transaction_details = client.get_transactions_by_hash(hash) 380 | 381 | In [4]: transaction_details.transaction 382 | Out[4]: { 383 | u'contractAddress': u'', 384 | u'from': u'0x2cac6e4b11d6b58f6d3c1c9d5fe8faa89f60e5a2', 385 | u'timeStamp': u'1466489498', 386 | u'gas': u'2300', 387 | u'errCode': u'', 388 | u'value': u'7106740000000000', 389 | u'blockNumber': u'1743059', 390 | u'to': u'0x66a1c3eaf0f1ffc28d209c0763ed0ca614f3b002', 391 | u'input': u'', 392 | u'type': u'call', 393 | u'isError': u'0', 394 | u'gasUsed': u'0' 395 | } 396 | 397 | """ 398 | module_uri = self._module.format(module=self._account_module) 399 | action_uri = self._action.format(action='txlistinternal') 400 | transaction_hash_uri = self._hash.format(hash=transaction_hash) 401 | 402 | request_url = self._base_url + \ 403 | module_uri + \ 404 | action_uri + \ 405 | transaction_hash_uri + \ 406 | self.key_uri 407 | 408 | return self._get_request( 409 | url=request_url, 410 | response_object=response.TransactionsByHashResponse 411 | ) 412 | 413 | def get_blocks_mined_by_address(self, address, startblock=None, 414 | endblock=None, sort='asc', offset=None, page=None): 415 | """ 416 | Obtains blocks mined by a single ethereum address. 417 | 418 | :param address: The ethereum address 419 | :type address: str 420 | :returns: A :py:obj:`response.BlocksMinedByAddressResponse` instance 421 | 422 | Example Usage: 423 | 424 | .. code-block:: python 425 | 426 | In [1]: client = Client() 427 | 428 | In [2]: address = '0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b' 429 | 430 | In [3]: blocks = client.get_blocks_mined_by_address(address) 431 | 432 | In [4]: blocks.blocks 433 | Out[4]: [ 434 | { 435 | u'timeStamp': u'1491118514', 436 | u'blockReward': u'5194770940000000000', 437 | u'blockNumber': u'3462296' 438 | }, { 439 | u'timeStamp': u'1480072029', 440 | u'blockReward': u'5086562212310617100', 441 | u'blockNumber': u'2691400' 442 | }, ... 443 | ] 444 | 445 | """ 446 | module_uri = self._module.format(module=self._account_module) 447 | action_uri = self._action.format(action='getminedblocks') 448 | address_uri = self._address.format(address=address) 449 | blocktype_uri = self._blocktype.format(blocktype='blocks') 450 | 451 | # If page or offset are set, _both_ must be set 452 | if page is not None or offset is not None: 453 | _both_set = page is not None and offset is not None 454 | if not _both_set: 455 | raise error.EtherscanTransactionError( 456 | 'If using page or offset, both must be set.' 457 | ) 458 | else: 459 | page_uri = self._page.format(page=page) 460 | offset_uri = self._offset.format(offset=offset) 461 | else: 462 | page_uri = self._page.format(page='') 463 | offset_uri = self._offset.format(offset='') 464 | 465 | request_url = self._base_url + \ 466 | module_uri + \ 467 | action_uri + \ 468 | address_uri + \ 469 | blocktype_uri + \ 470 | page_uri + \ 471 | offset_uri + \ 472 | self.key_uri 473 | 474 | return self._get_request( 475 | url=request_url, 476 | response_object=response.BlocksMinedByAddressResponse 477 | ) 478 | 479 | ######################## 480 | # Contract API methods # 481 | ######################## 482 | def get_contract_abi(self, address): 483 | """ 484 | Obtains contract details by address. 485 | 486 | :param address: The ethereum address of the contract 487 | :type address: str 488 | :returns: A :py:obj:`response.ContractABIByAddressResponse` instance 489 | 490 | Example Usage: 491 | 492 | .. code-block:: python 493 | 494 | In [1]: client = Client() 495 | 496 | In [2]: address = '0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413' 497 | 498 | In [3]: contract = client.get_contract_abi(address) 499 | 500 | In [4]: contract.contract_abi 501 | Out[4]: [ 502 | { 503 | "constant":true, 504 | "inputs": [ 505 | { 506 | "name":"", 507 | "type":"uint256" 508 | } 509 | ], 510 | "name":"proposals", 511 | "outputs": [ 512 | { 513 | "name":"recipient", 514 | "type":"address" 515 | }, { 516 | "name":"amount", 517 | "type":"uint256" 518 | }, { 519 | "name":"description", 520 | "type":"string" 521 | }, { 522 | ... 523 | } 524 | ] 525 | } 526 | 527 | """ 528 | module_uri = self._module.format(module=self._contract_module) 529 | action_uri = self._action.format(action='getabi') 530 | address_uri = self._address.format(address=address) 531 | 532 | request_url = self._base_url + \ 533 | module_uri + \ 534 | action_uri + \ 535 | address_uri + \ 536 | self.key_uri 537 | 538 | return self._get_request( 539 | url=request_url, 540 | response_object=response.ContractABIByAddressResponse 541 | ) 542 | 543 | ############################ 544 | # Transactions API methods # 545 | ############################ 546 | def get_contract_execution_status(self, transaction_hash): 547 | """ 548 | Retrieves contract status data by tx hash. Obtains whether or not 549 | there was an error during contract execution. 550 | 551 | :param transaction_hash: The hash of the contract 552 | :type transaction_hash: str 553 | :returns: A :py:obj:`response.ContractStatusResponse` instance 554 | 555 | Example Usage: 556 | 557 | .. code-block:: python 558 | 559 | In [1]: client = Client() 560 | 561 | In [2]: hash = '0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a' 562 | 563 | In [3]: contract = client.get_contract_execution_status(hash) 564 | 565 | In [4]: contract.contract_status 566 | Out[4]: { 567 | u'status': u'1', 568 | u'message': u'OK', 569 | u'result': { 570 | u'isError': u'1', 571 | u'errDescription': u'Bad jump destination' 572 | } 573 | } 574 | 575 | """ 576 | module_uri = self._module.format(module=self._transaction_module) 577 | action_uri = self._action.format(action='getstatus') 578 | transaction_hash_uri = self._hash.format(hash=transaction_hash) 579 | 580 | request_url = self._base_url + \ 581 | module_uri + \ 582 | action_uri + \ 583 | transaction_hash_uri + \ 584 | self.key_uri 585 | 586 | return self._get_request( 587 | url=request_url, 588 | response_object=response.ContractStatusResponse 589 | ) 590 | 591 | ##################### 592 | # Token API methods # 593 | ##################### 594 | def get_token_supply_by_address(self, address): 595 | """ 596 | Retrieves total token supply for an ERC-20 compliant token given a 597 | contract address. 598 | 599 | :param address: The address of the token contract 600 | :type address: str 601 | :returns: A :py:obj:`response.TokenSupplyResponse` instance 602 | 603 | Example Usage: 604 | 605 | .. code-block:: python 606 | 607 | In [1]: client = Client() 608 | 609 | In [2]: contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 610 | 611 | In [3]: contract = client.get_token_supply_by_address( 612 | contract_address 613 | ) 614 | 615 | In [4]: contract.total_supply 616 | Out[4]: 21265524714464.0 617 | 618 | """ 619 | module_uri = self._module.format(module=self._token_module) 620 | action_uri = self._action.format(action='tokensupply') 621 | contract_address_uri = self._contract_address.format(contract_address=address) 622 | 623 | request_url = self._base_url + \ 624 | module_uri + \ 625 | action_uri + \ 626 | contract_address_uri + \ 627 | self.key_uri 628 | 629 | return self._get_request( 630 | url=request_url, 631 | response_object=response.TokenSupplyResponse 632 | ) 633 | 634 | def get_token_balance_by_address(self, contract_address, account_address): 635 | """ 636 | Retrieves ERC-20 compliant token balance for an account given a 637 | contract account address. 638 | 639 | :param contract_address: The address of the token contract 640 | :type contract_address: str 641 | :param account_address: The address of the user account for which the 642 | token balance is being queried 643 | :type account_address: str 644 | :returns: A :py:obj:`response.TokenAccountBalanceResponse` instance 645 | 646 | Example Usage: 647 | 648 | .. code-block:: python 649 | 650 | In [1]: client = Client() 651 | 652 | In [2]: contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 653 | 654 | In [3]: account_address = '0xe04f27eb70e025b78871a2ad7eabe85e61212761' 655 | 656 | In [4]: token_balance = client.get_token_balance_by_address( 657 | contract_address, 658 | account_address 659 | ) 660 | 661 | In [4]: token_balance.balance 662 | Out[4]: 135499.0 663 | 664 | """ 665 | module_uri = self._module.format(module=self._account_module) 666 | action_uri = self._action.format(action='tokenbalance') 667 | contract_address_uri = self._contract_address.format(contract_address=contract_address) # noqa 668 | address_uri = self._address.format(address=account_address) 669 | 670 | request_url = self._base_url + \ 671 | module_uri + \ 672 | action_uri + \ 673 | contract_address_uri + \ 674 | address_uri + \ 675 | self.key_uri 676 | 677 | return self._get_request( 678 | url=request_url, 679 | response_object=response.TokenAccountBalanceResponse 680 | ) 681 | 682 | ##################### 683 | # Block API methods # 684 | ##################### 685 | def get_block_and_uncle_rewards_by_block_number(self, block_number): 686 | """ 687 | Retrieves block and uncle rewards by block number. 688 | 689 | :param block_number: The address of the token contract 690 | :type block_number: str or int 691 | :returns: A :py:obj:`response.TokenAccountBalanceResponse` instance 692 | 693 | Example Usage: 694 | 695 | .. code-block:: python 696 | 697 | In [1]: client = Client() 698 | 699 | In [2]: block_number = 2165403 700 | 701 | In [3]: block_data = client.get_block_and_uncle_rewards_by_block_number( 702 | block_number 703 | ) 704 | 705 | In [4]: block_data.rewards_data 706 | Out[4]: { 707 | "blockNumber": "2165403", 708 | "timeStamp": "1472533979", 709 | "blockMiner": "0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3", 710 | "blockReward": "5314181600000000000", 711 | "uncles": [ 712 | { 713 | "miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1", 714 | "unclePosition": "0", 715 | "blockreward": "3750000000000000000" 716 | }, { 717 | "miner": "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce", 718 | "unclePosition": "1", 719 | "blockreward": "3750000000000000000" 720 | } 721 | ], 722 | "uncleInclusionReward": "312500000000000000" 723 | } 724 | 725 | """ 726 | module_uri = self._module.format(module=self._block_module) 727 | action_uri = self._action.format(action='getblockreward') 728 | blockno_uri = self._blockno.format(blockno=block_number) 729 | 730 | request_url = self._base_url + \ 731 | module_uri + \ 732 | action_uri + \ 733 | blockno_uri + \ 734 | self.key_uri 735 | 736 | return self._get_request( 737 | url=request_url, 738 | response_object=response.BlockRewardsResponse 739 | ) 740 | -------------------------------------------------------------------------------- /pyetherscan/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom error definitions for Etherscan API objects. 3 | """ 4 | 5 | class EtherscanBaseError(Exception): 6 | """ 7 | A base error class from which other Etherscan errors 8 | should inherit. 9 | """ 10 | pass 11 | 12 | 13 | class EtherscanDataError(EtherscanBaseError): 14 | """ 15 | An abstract error class for data related errors (e.g. if result is []). 16 | """ 17 | pass 18 | 19 | 20 | class EtherscanInitializationError(EtherscanBaseError): 21 | """ 22 | An abstract error class for Initialization related errors. 23 | """ 24 | pass 25 | 26 | 27 | class EtherscanConnectionError(EtherscanBaseError): 28 | """ 29 | An abstract error class for Connection related errors. 30 | """ 31 | pass 32 | 33 | 34 | class EtherscanRequestError(EtherscanBaseError): 35 | """ 36 | An abstract error class for Request related errors. 37 | """ 38 | pass 39 | 40 | 41 | class EtherscanAddressError(EtherscanBaseError): 42 | """ 43 | An abstract error class for Address related errors. 44 | """ 45 | pass 46 | 47 | 48 | class EtherscanContractError(EtherscanBaseError): 49 | """ 50 | An abstract error class for Contract related errors. 51 | """ 52 | pass 53 | 54 | 55 | class EtherscanTransactionError(EtherscanBaseError): 56 | """ 57 | An abstract error class for Transaction related errors. 58 | """ 59 | pass 60 | 61 | 62 | class EtherscanBlockError(EtherscanBaseError): 63 | """ 64 | An abstract error class for Block related errors. 65 | """ 66 | pass 67 | 68 | 69 | class EtherscanEventLogError(EtherscanBaseError): 70 | """ 71 | An abstract error class for EventLog related errors. 72 | """ 73 | pass 74 | 75 | 76 | class EtherscanGethProxyError(EtherscanBaseError): 77 | """ 78 | An abstract error class for GethProxy related errors. 79 | """ 80 | pass 81 | 82 | 83 | class EtherscanWebsocketError(EtherscanBaseError): 84 | """ 85 | An abstract error class for Websocket related errors. 86 | """ 87 | pass 88 | 89 | 90 | class EtherscanTokenError(EtherscanBaseError): 91 | """ 92 | An abstract error class for Token related errors. 93 | """ 94 | pass 95 | 96 | 97 | class EtherscanStatsError(EtherscanBaseError): 98 | """ 99 | An abstract error class for Stats related errors. 100 | """ 101 | pass 102 | -------------------------------------------------------------------------------- /pyetherscan/ethereum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Library for building ethereum objects using the ETherscan API. 3 | """ 4 | import datetime 5 | 6 | from . import client, error 7 | 8 | 9 | class Transaction(object): 10 | """ 11 | Represents a generic ethereum transaction. The object is built to lazily 12 | parse transactions. Attributes are only evaluated when called for the 13 | first time. 14 | 15 | Public Attributes: 16 | 17 | - ``nonce`` 18 | - ``contract_address`` 19 | - ``cumulative_gas_used`` 20 | - ``hash`` 21 | - ``block_hash`` 22 | - ``time_stamp`` 23 | - ``gas`` 24 | - ``gas_price`` 25 | - ``value`` 26 | - ``block_number`` 27 | - ``block`` 28 | - ``to`` 29 | - ``from_`` 30 | - ``confirmations`` 31 | - ``input`` 32 | - ``transaction_index`` 33 | - ``type`` 34 | - ``datetime_executed`` 35 | - ``gas_used`` 36 | """ 37 | 38 | def __init__(self, data): 39 | """ 40 | Initializes the Transaction object. 41 | 42 | :param data: The dictionary of data that makes up the transaction. 43 | :type data: dict 44 | """ 45 | if not isinstance(data, dict): 46 | raise error.EtherscanInitializationError( 47 | 'data must be of type dict.' 48 | ) 49 | 50 | self._data = data 51 | self._nonce = None 52 | self._contract_address = None 53 | self._cumulative_gas_used = None 54 | self._hash = None 55 | self._block_hash = None 56 | self._time_stamp = None 57 | self._gas = None 58 | self._value = None 59 | self._block_number = None 60 | self._to = None 61 | self._confirmations = None 62 | self._input = None 63 | self._transaction_index = None 64 | self._from = None 65 | self._gas_price = None 66 | self._datetime_executed = None 67 | self._gas_used = None 68 | self._type = None 69 | 70 | def _retrieve_gas_price(self): 71 | self._gas_price = float(self._data.get('gasPrice')) 72 | return self._gas_price 73 | 74 | @property 75 | def gas_price(self): 76 | return self._gas_price or self._retrieve_gas_price() 77 | 78 | def _retrieve_from(self): 79 | self._from = self._data.get('from') 80 | return self._from 81 | 82 | @property 83 | def from_(self): 84 | return self._from or self._retrieve_from() 85 | 86 | def _retrieve_nonce(self): 87 | self._nonce = self._data.get('nonce') 88 | return self._nonce 89 | 90 | @property 91 | def nonce(self): 92 | return self._nonce or self._retrieve_nonce() 93 | 94 | def _retrieve_contract_address(self): 95 | self._contract_address = self._data.get('contractAddress') 96 | return self._contract_address 97 | 98 | @property 99 | def contract_address(self): 100 | return self._contract_address or self._retrieve_contract_address() 101 | 102 | def _retrieve_cumulative_gas_used(self): 103 | self._cumulative_gas_used = float(self._data.get('cumulativeGasUsed')) 104 | return self._cumulative_gas_used 105 | 106 | @property 107 | def cumulative_gas_used(self): 108 | return self._cumulative_gas_used or self._retrieve_cumulative_gas_used() 109 | 110 | def _retrieve_hash(self): 111 | self._hash = self._data.get('hash') 112 | return self._hash 113 | 114 | @property 115 | def hash(self): 116 | return self._hash or self._retrieve_hash() 117 | 118 | def _retrieve_block_hash(self): 119 | self._block_hash = self._data.get('blockHash') 120 | return self._block_hash 121 | 122 | @property 123 | def block_hash(self): 124 | return self._block_hash or self._retrieve_block_hash() 125 | 126 | def _retrieve_time_stamp(self): 127 | self._time_stamp = int(self._data.get('timeStamp')) 128 | return self._time_stamp 129 | 130 | @property 131 | def time_stamp(self): 132 | return self._time_stamp or self._retrieve_time_stamp() 133 | 134 | def _retrieve_gas(self): 135 | self._gas = float(self._data.get('gas')) 136 | return self._gas 137 | 138 | @property 139 | def gas(self): 140 | return self._gas or self._retrieve_gas() 141 | 142 | def _retrieve_value(self): 143 | self._value = float(self._data.get('value')) 144 | return self._value 145 | 146 | @property 147 | def value(self): 148 | return self._value or self._retrieve_value() 149 | 150 | def _retrieve_block_number(self): 151 | self._block_number = int(self._data.get('blockNumber')) 152 | return self._block_number 153 | 154 | @property 155 | def block_number(self): 156 | return self._block_number or self._retrieve_block_number() 157 | 158 | @property 159 | def block(self): 160 | return Block(self.block_number) 161 | 162 | def _retrieve_to(self): 163 | self._to = self._data.get('to') 164 | return self._to 165 | 166 | @property 167 | def to(self): 168 | return self._to or self._retrieve_to() 169 | 170 | def _retrieve_confirmations(self): 171 | self._confirmations = self._data.get('confirmations') 172 | return self._confirmations 173 | 174 | @property 175 | def confirmations(self): 176 | return self._confirmations or self._retrieve_confirmations() 177 | 178 | def _retrieve_input(self): 179 | self._input = self._data.get('input') 180 | return self._input 181 | 182 | @property 183 | def input(self): 184 | return self._input or self._retrieve_input() 185 | 186 | def _retrieve_transaction_index(self): 187 | self._transaction_index = int(self._data.get('transactionIndex')) 188 | return self._transaction_index 189 | 190 | @property 191 | def transaction_index(self): 192 | return self._transaction_index or self._retrieve_transaction_index() 193 | 194 | def _retrieve_gas_used(self): 195 | self._gas_used = float(self._data.get('gasUsed')) 196 | return self._gas_used 197 | 198 | @property 199 | def gas_used(self): 200 | return self._gas_used or self._retrieve_gas_used() 201 | 202 | def _retrieve_type(self): 203 | self._type = self._data.get('type') 204 | return self._type 205 | 206 | @property 207 | def type(self): 208 | return self._type or self._retrieve_type() 209 | 210 | def _convert_time_stamp(self): 211 | self._datetime_executed = datetime.datetime.utcfromtimestamp( 212 | self.time_stamp 213 | ) 214 | return self._datetime_executed 215 | 216 | @property 217 | def datetime_executed(self): 218 | return self._datetime_executed or self._convert_time_stamp() 219 | 220 | def __repr__(self): 221 | return 'Transaction(hash={hash}, value={value}, ' \ 222 | 'datetime_executed={datetime_executed})'.format( 223 | hash=self.hash, 224 | value=self.value, 225 | datetime_executed=self.datetime_executed 226 | ) 227 | 228 | 229 | class TransactionContainer(object): 230 | """ 231 | Represents a sequence of transactions (normal and internal). 232 | """ 233 | 234 | def __init__(self, transaction_list): 235 | if not isinstance(transaction_list, list): 236 | raise TypeError('transaction_list must be of type list, ' 237 | 'not {type}'.format(type=type(transaction_list))) 238 | self.transaction_list = transaction_list 239 | 240 | def __iter__(self): 241 | for transaction in self.transaction_list: 242 | yield Transaction(transaction) 243 | 244 | def __getitem__(self, index): 245 | transaction_to_return = self.transaction_list[index] 246 | return Transaction(transaction_to_return) 247 | 248 | def __repr__(self): 249 | return 'TransactionContainer(transaction_list=<{n} transactions>)'.format( 250 | n=len(self.transaction_list) 251 | ) 252 | 253 | 254 | class Address(object): 255 | """ 256 | Represents a base address. 257 | 258 | This uses the :py:class:`Client` object to retrieve information about, and 259 | construct, the ``Address``. 260 | 261 | Public Attributes: 262 | - ``address`` 263 | - ``balance`` 264 | - ``blocks_mined`` 265 | 266 | Public Methods: 267 | - :py:meth:`token_balance` 268 | 269 | Example Usage: 270 | 271 | .. code-block:: python 272 | 273 | In [1]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 274 | 275 | In [2]: ethereum_address = Address(address) 276 | 277 | In [3]: ethereum_address.balance 278 | Out[3]: 748997604382925139479303.0 279 | 280 | In [4]: for txn in ethereum_address.transactions: 281 | ...: print(txn.value) 282 | 283 | """ 284 | 285 | def __init__(self, address): 286 | """ 287 | Initializes an ethereum address object. 288 | """ 289 | if not isinstance(address, str): 290 | raise error.EtherscanInitializationError( 291 | "address must be a string." 292 | ) 293 | 294 | self.address = address 295 | self.client = client.Client() 296 | self._transactions = None 297 | self._balance = None 298 | self._block_list = None 299 | 300 | def token_balance(self, contract_address): 301 | """ 302 | Obtains an address's ERC-20 compliant token balance given a token 303 | contract address. 304 | 305 | :param contract_address: The address of the token contract. 306 | :type contract_address: str 307 | :returns: The balance of the token as a float. 308 | """ 309 | token = self.client.get_token_balance_by_address( 310 | contract_address=contract_address, 311 | account_address=self.address 312 | ) 313 | return token.balance 314 | 315 | def _retrieve_balance(self): 316 | balance_object = self.client.get_single_balance( 317 | self.address 318 | ) 319 | self._balance = balance_object.balance 320 | return self._balance 321 | 322 | def _retrieve_transactions_for_address(self): 323 | normal = self.client.get_transactions_by_address(self.address) 324 | internal = self.client.get_transactions_by_address( 325 | address=self.address, 326 | internal=True 327 | ) 328 | txns = normal.transactions + internal.transactions 329 | self._transactions = txns or [] 330 | return self._transactions 331 | 332 | @property 333 | def _raw_transactions(self): 334 | return self._transactions or self._retrieve_transactions_for_address() 335 | 336 | @property 337 | def balance(self): 338 | """ 339 | The balance in ether for this address. 340 | """ 341 | return self._balance or self._retrieve_balance() 342 | 343 | @property 344 | def transactions(self): 345 | """ 346 | returns a :py:obj:`ethereum.TransactionContainer` object. This object 347 | can be treated as a sequence object containing transactions this address 348 | was involved in. 349 | """ 350 | return TransactionContainer(self._raw_transactions) 351 | 352 | def _retrieve_block_list(self): 353 | response = self.client.get_blocks_mined_by_address(self.address) 354 | self._block_list = response.blocks or [] 355 | return self._block_list 356 | 357 | @property 358 | def blocks_mined(self): 359 | blocks = self._block_list or self._retrieve_block_list() 360 | return BlockContainer(blocks) 361 | 362 | def __repr__(self): 363 | return 'Address(address={address})'.format( 364 | address=self.address 365 | ) 366 | 367 | 368 | class BlockContainer(object): 369 | """ 370 | Represents a sequence of blocks. 371 | """ 372 | 373 | def __init__(self, block_list): 374 | self.block_list = block_list 375 | 376 | def __iter__(self): 377 | for block in self.block_list: 378 | block_number = int(block.get('blockNumber')) 379 | yield Block(block_number) 380 | 381 | def __getitem__(self, index): 382 | block_to_return = self.block_list[index] 383 | block_number = int(block_to_return.get('blockNumber')) 384 | return Block(block_number) 385 | 386 | def __repr__(self): 387 | return 'BlockContainer(block_list=<{n} blocks>)'.format( 388 | n=len(self.block_list) 389 | ) 390 | 391 | 392 | class Block(object): 393 | """ 394 | Represents an ethereum block. 395 | 396 | This uses the :py:class:`Client` object to retrieve information about, and 397 | construct, the ``Block``. 398 | 399 | Public Attributes: 400 | - ``time_stamp`` 401 | - ``block_miner`` 402 | - ``block_reward`` 403 | - ``uncles`` 404 | - ``datetime_mined`` 405 | - ``uncle_inclusion_reward`` 406 | 407 | Example Usage: 408 | 409 | .. code-block:: python 410 | 411 | In [1]: address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 412 | 413 | In [2]: ethereum_address = Address(address) 414 | 415 | In [3]: ethereum_address.balance 416 | Out[3]: 748997604382925139479303.0 417 | 418 | In [4]: for txn in ethereum_address.transactions: 419 | ...: print(txn.value) 420 | 421 | """ 422 | 423 | def __init__(self, block_number): 424 | if not isinstance(block_number, (int, str)): 425 | raise error.EtherscanInitializationError( 426 | "block_number must be a string or integer." 427 | ) 428 | 429 | self.block_number = block_number 430 | self.client = client.Client() 431 | 432 | self._block_reward_data = None 433 | 434 | self._time_stamp = None 435 | self._block_miner = None 436 | self._datetime_mined = None 437 | self._block_reward = None 438 | self._uncles = None 439 | self._uncle_inclusion_reward = None 440 | 441 | def _retrieve_block_reward_data(self): 442 | data = self.client.get_block_and_uncle_rewards_by_block_number( 443 | self.block_number 444 | ) 445 | self._block_reward_data = data.rewards_data 446 | return self._block_reward_data 447 | 448 | @property 449 | def _raw_block_data(self): 450 | return self._block_reward_data or self._retrieve_block_reward_data() 451 | 452 | def _retrieve_time_stamp(self): 453 | self._time_stamp = int(self._raw_block_data.get('timeStamp')) 454 | return self._time_stamp 455 | 456 | @property 457 | def time_stamp(self): 458 | return self._time_stamp or self._retrieve_time_stamp() 459 | 460 | def _retrieve_block_miner(self): 461 | self._block_miner = str(self._raw_block_data.get('blockMiner')) 462 | return self._block_miner 463 | 464 | @property 465 | def block_miner(self): 466 | return self._block_miner or self._retrieve_block_miner() 467 | 468 | def _convert_time_stamp(self): 469 | self._datetime_mined = datetime.datetime.utcfromtimestamp( 470 | self.time_stamp 471 | ) 472 | return self._datetime_mined 473 | 474 | @property 475 | def datetime_mined(self): 476 | return self._datetime_mined or self._convert_time_stamp() 477 | 478 | def _retrieve_block_reward(self): 479 | self._block_reward = float(self._raw_block_data.get('blockReward')) 480 | return self._block_reward 481 | 482 | @property 483 | def block_reward(self): 484 | return self._block_reward or self._retrieve_block_reward() 485 | 486 | def _retrieve_uncles(self): 487 | uncles = self._raw_block_data.get('uncles') 488 | parsed_uncles = [ 489 | { 490 | 'miner': str(u['miner']), 491 | 'block_reward': float(u['blockreward']) 492 | } for u in uncles 493 | ] 494 | self._uncles = parsed_uncles 495 | return self._uncles 496 | 497 | @property 498 | def uncles(self): 499 | return self._uncles or self._retrieve_uncles() 500 | 501 | def _retrieve_uncle_inclusion_reward(self): 502 | self._uncle_inclusion_reward = float( 503 | self._raw_block_data.get('uncleInclusionReward') 504 | ) 505 | return self._uncle_inclusion_reward 506 | 507 | @property 508 | def uncle_inclusion_reward(self): 509 | return self._uncle_inclusion_reward or \ 510 | self._retrieve_uncle_inclusion_reward() 511 | 512 | def __repr__(self): 513 | return 'Block(block_number={block_number})'.format( 514 | block_number=self.block_number 515 | ) 516 | 517 | 518 | class Token(object): 519 | """ 520 | Represents an ERC-20 compliant token. 521 | 522 | :param contract_address: The address of the Token contract 523 | :type contract_address: str 524 | """ 525 | 526 | def __init__(self, contract_address): 527 | if not isinstance(contract_address, str): 528 | raise error.EtherscanInitializationError( 529 | "contract_address must be a string." 530 | ) 531 | 532 | self.contract_address = contract_address 533 | self.client = client.Client() 534 | 535 | self._supply = None 536 | 537 | def _retrieve_supply(self): 538 | token = self.client.get_token_supply_by_address(self.contract_address) 539 | self._supply = token.total_supply 540 | return self._supply 541 | 542 | @property 543 | def supply(self): 544 | return self._supply or self._retrieve_supply() 545 | 546 | def token_balance(self, address): 547 | """ 548 | The user balance of this token for a given address. 549 | 550 | :param address: An ethereum address. 551 | :type address: str 552 | :returns: The balance as a float. 553 | """ 554 | token = self.client.get_token_balance_by_address( 555 | contract_address=self.contract_address, 556 | account_address=address 557 | ) 558 | return token.balance 559 | 560 | def __repr__(self): 561 | return 'Token(contract_address={contract_address})'.format( 562 | contract_address=self.contract_address 563 | ) 564 | -------------------------------------------------------------------------------- /pyetherscan/response.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module used to define API-specific response objects. All Etherscan API requests return an instance of :py:class:`EtherscanResponse`, extended to meet the endpoint's specific needs. 3 | 4 | See :doc:`/response` for an overview. 5 | """ 6 | import json 7 | 8 | from . import error 9 | 10 | try: 11 | from json.decoder import JSONDecodeError 12 | except (AttributeError, ImportError): 13 | # In python 2.x there is no JSONDecodeError, the json module 14 | # simply throws a ValueError 15 | JSONDecodeError = ValueError 16 | 17 | 18 | class EtherscanResponse(object): 19 | """ 20 | This is the parent class for all Etherscan API responses. 21 | 22 | All child classes must define :py:meth:`parse_response` 23 | 24 | Upon initialization, the class sets the following attributes: 25 | - `etherscan_response`: The response json from Etherscan. 26 | - `response_object`: The `requests.Response `_ returned by the API call. 27 | - `status`: The Etherscan response status (independent of the `requests.Response` status). 28 | - `message`: The Etherscan response message. 29 | - Class-specific attributes are then set via the call to :py:meth:`parse_response`. 30 | 31 | If a `403` error is received it will raise an :py:class:`EtherscanRequestError`. This typically means the rate limit has been reached. By default, :py:meth:`get_request` and :py:meth:`post_request` will handle this and automatically retry the request. 32 | """ 33 | 34 | def __init__(self, resp): 35 | 36 | # Check for rate limit errors 37 | if resp.status_code == 403: 38 | raise error.EtherscanRequestError( 39 | 'Rate limit reached.' 40 | ) 41 | 42 | # Ensure a valid response code was received 43 | if resp.status_code not in [200, 201]: 44 | raise error.EtherscanRequestError( 45 | 'reason: {reason}'.format(reason=resp.reason) 46 | ) 47 | 48 | # Attempt to parse response body 49 | try: 50 | self.etherscan_response = json.loads(resp.text) 51 | except (AttributeError, JSONDecodeError): 52 | raise error.EtherscanRequestError( 53 | 'Invalid request: \n{request}'.format( 54 | request=resp 55 | ) 56 | ) 57 | else: 58 | self.message = self.etherscan_response.get('message') 59 | self.status = self.etherscan_response.get('status') 60 | 61 | # Parse API message to check for API errors 62 | result = self.etherscan_response.get('result') 63 | bad_data = self.message == 'NOTOK' or result == 'Error!' 64 | if bad_data: 65 | raise error.EtherscanDataError( 66 | '{message}. result={result}'.format( 67 | message=self.message, 68 | result=result 69 | ) 70 | ) 71 | 72 | # Finish parsing response object 73 | self.response_object = resp 74 | self.response_status_code = resp.status_code 75 | self.parse_response() 76 | 77 | def parse_response(self): 78 | """ 79 | The method that will parse the response object and store 80 | all attributes within the specific API response object. 81 | """ 82 | raise NotImplementedError 83 | 84 | def __repr__(self): 85 | """ 86 | Build a response representation like: `EtherscanResponse(resp=)` 87 | """ 88 | return '{_class}(resp={resp})'.format( 89 | _class=self.__class__.__name__, 90 | resp=self.response_object 91 | ) 92 | 93 | 94 | class SingleAddressBalanceResponse(EtherscanResponse): 95 | """ 96 | Represents a response object for a single address account balance call within the Etherscan `Accounts` endpoint. 97 | 98 | Available attributes: 99 | - `balance`: The balance of the address returned as a float. 100 | 101 | Example: 102 | 103 | .. code-block:: python 104 | In [1]: response = SingleAddressBalanceResponse(resp) 105 | 106 | In [2]: response.etherscan_response 107 | Out[2]: { 108 | "status":"1", 109 | "message":"OK", 110 | "result":"40807168564070000000000" 111 | } 112 | 113 | In [3]: response.balance 114 | Out[3]: 40807168564070000000000.0 115 | """ 116 | 117 | def parse_response(self): 118 | """ 119 | Parses a single balance request response. Example API 120 | response output: 121 | 122 | .. code-block:: python 123 | 124 | { 125 | "status":"1", 126 | "message":"OK", 127 | "result":"40807168564070000000000" 128 | } 129 | 130 | """ 131 | self.balance = float(self.etherscan_response.get('result')) 132 | 133 | 134 | class MultiAddressBalanceResponse(EtherscanResponse): 135 | """ 136 | Represents a response object for a multi address account balance call 137 | within the Etherscan `Accounts` endpoint. 138 | 139 | Available attributes: 140 | - `balances`: The balances of the addresses returned as a dict. 141 | 142 | Example: 143 | 144 | .. code-block:: python 145 | In [1]: response = MultiAddressBalanceResponse(resp) 146 | 147 | In [2]: response.etherscan_response 148 | Out[2]: { 149 | "status":"1", 150 | "message":"OK", 151 | "result":[ 152 | { 153 | "account":"0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a", 154 | "balance":"40807168564070000000000" 155 | }, { 156 | "account":"0x63a9975ba31b0b9626b34300f7f627147df1f526", 157 | "balance":"332567136222827062478" 158 | }, { 159 | "account":"0x198ef1ec325a96cc354c7266a038be8b5c558f67", 160 | "balance":"12005264493462223951724" 161 | } 162 | ] 163 | } 164 | 165 | In [3]: response.balances 166 | Out[3]: { 167 | '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a': 40807168564070000000000.0, 168 | '0x63a9975ba31b0b9626b34300f7f627147df1f526': 332567136222827062478.0, 169 | '0x198ef1ec325a96cc354c7266a038be8b5c558f67': 12005264493462223951724.0, 170 | } 171 | """ 172 | 173 | def parse_response(self): 174 | """ 175 | Parses a multi balance request response. Example API 176 | response output: 177 | 178 | .. code-block:: python 179 | 180 | { 181 | "status":"1", 182 | "message":"OK", 183 | "result":[ 184 | { 185 | "account":"0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a", 186 | "balance":"40807168564070000000000" 187 | }, { 188 | "account":"0x63a9975ba31b0b9626b34300f7f627147df1f526", 189 | "balance":"332567136222827062478" 190 | }, { 191 | "account":"0x198ef1ec325a96cc354c7266a038be8b5c558f67", 192 | "balance":"12005264493462223951724" 193 | } 194 | ] 195 | } 196 | 197 | """ 198 | 199 | address_balance_mapping_list = self.etherscan_response.get('result') 200 | self.balances = { 201 | mapping.get('account'): float(mapping.get('balance')) 202 | for mapping in address_balance_mapping_list 203 | } 204 | 205 | 206 | class TransactionsByAddressResponse(EtherscanResponse): 207 | 208 | def parse_response(self): 209 | """ 210 | Parses a transactions by address request response. Example API 211 | response output: 212 | 213 | .. code-block:: python 214 | 215 | { 216 | "status":"1", 217 | "message":"OK", 218 | "result":[ 219 | { 220 | "blockNumber":"54092", 221 | "timeStamp":"1439048640", 222 | "hash":"0x9c81f44c29ff0226f83...", 223 | "nonce":"0", 224 | "blockHash":"0xd3cabad6adab0b5...", 225 | "transactionIndex":"0", 226 | "from":"0x5abfec25f74cd88437631a7731906932776356f9", 227 | "to":"", 228 | "value":"11901464239480000000000000", 229 | "gas":"2000000", 230 | "gasPrice":"10000000000000", 231 | "isError":"0", 232 | "input":"0x6060b91f525b5ae7a03d...", 233 | "contractAddress":"0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", 234 | "cumulativeGasUsed":"1436963", 235 | "gasUsed":"1436963", 236 | "confirmations":"3921024" 237 | }, { 238 | ... 239 | } 240 | ] 241 | } 242 | 243 | """ 244 | self.transactions = self.etherscan_response.get('result') 245 | 246 | 247 | class TransactionsByHashResponse(EtherscanResponse): 248 | 249 | def parse_response(self): 250 | """ 251 | Parses a transactions by hash request response. Example API 252 | response output: 253 | 254 | .. code-block:: python 255 | 256 | { 257 | "status":"1", 258 | "message":"OK", 259 | "result":[ 260 | { 261 | "blockNumber":"1743059", 262 | "timeStamp":"1466489498", 263 | "from":"0x2cac6e4b11d6b58f6d3c1c9d5fe8faa89f60e5a2", 264 | "to":"0x66a1c3eaf0f1ffc28d209c0763ed0ca614f3b002", 265 | "value":"7106740000000000", 266 | "contractAddress":"", 267 | "input":"", 268 | "type":"call", 269 | "gas":"2300", 270 | "gasUsed":"0", 271 | "isError":"0", 272 | "errCode":"" 273 | } 274 | ] 275 | } 276 | 277 | """ 278 | self.transaction = self.etherscan_response.get('result')[0] 279 | 280 | 281 | class BlocksMinedByAddressResponse(EtherscanResponse): 282 | 283 | def parse_response(self): 284 | """ 285 | Parses a blocks mined by address request response. Example API 286 | response output: 287 | 288 | .. code-block:: python 289 | 290 | { 291 | "status":"1", 292 | "message":"OK", 293 | "result":[ 294 | { 295 | "blockNumber":"3462296", 296 | "timeStamp":"1491118514", 297 | "blockReward":"5194770940000000000" 298 | }, { 299 | ... 300 | } 301 | ] 302 | } 303 | 304 | """ 305 | self.blocks = self.etherscan_response.get('result') 306 | 307 | 308 | class ContractABIByAddressResponse(EtherscanResponse): 309 | 310 | def parse_response(self): 311 | """ 312 | Parses a contract abi by address request response. Example API 313 | response output: 314 | 315 | .. code-block:: python 316 | 317 | { 318 | "status":"1", 319 | "message":"OK", 320 | "result":[ 321 | { 322 | 'constant': True, 323 | 'inputs': [ 324 | { 325 | 'name': '', 326 | 'type': 'uint256' 327 | } 328 | ], 329 | 'name': 'proposals', 330 | 'outputs': [ 331 | {'name': 'recipient', 'type': 'address'}, 332 | {'name': 'amount', 'type': 'uint256'}, 333 | {'name': 'description', 'type': 'string'}, 334 | {'name': 'votingDeadline', 'type': 'uint256'}, 335 | {'name': 'open', 'type': 'bool'}, 336 | {'name': 'proposalPassed', 'type': 'bool'}, 337 | {'name': 'proposalHash', 'type': 'bytes32'}, 338 | {'name': 'proposalDeposit', 'type': 'uint256'}, 339 | {'name': 'newCurator', 'type': 'bool'}, 340 | {'name': 'yea', 'type': 'uint256'}, 341 | {'name': 'nay', 'type': 'uint256'}, 342 | {'name': 'creator', 'type': 'address'} 343 | ], 344 | 'type': 'function' 345 | }, { 346 | ... 347 | } 348 | ] 349 | } 350 | 351 | """ 352 | self.contract_abi = self.etherscan_response.get('result') 353 | 354 | 355 | class ContractStatusResponse(EtherscanResponse): 356 | """ 357 | Represents a response object for a contract status call within the 358 | Etherscan `Contracts` endpoint. 359 | 360 | Available attributes: 361 | - `contract_status`: The status of the contract returned as a json object. 362 | 363 | Example: 364 | 365 | .. code-block:: python 366 | In [1]: response = ContractStatusResponse(resp) 367 | 368 | In [2]: response.contract_status 369 | Out[2]: { 370 | "status":"1", 371 | "message":"OK", 372 | "result":{ 373 | "isError":"1", 374 | "errDescription":"Bad jump destination" 375 | } 376 | } 377 | """ 378 | 379 | def parse_response(self): 380 | """ 381 | Parses a transaction status by hash request response. Example API 382 | response output: 383 | 384 | .. code-block:: python 385 | 386 | { 387 | "status":"1", 388 | "message":"OK", 389 | "result":{ 390 | "isError":"1", 391 | "errDescription":"Bad jump destination" 392 | } 393 | } 394 | 395 | """ 396 | self.contract_status = self.etherscan_response.get('result') 397 | 398 | 399 | class TokenSupplyResponse(EtherscanResponse): 400 | """ 401 | Represents a response object for a token supply call within the Etherscan `Tokens` endpoint. 402 | 403 | Available attributes: 404 | - `total_supply`: The total supply of the token returned as a float. 405 | 406 | Example: 407 | 408 | .. code-block:: python 409 | In [1]: response = TokenSupplyResponse(resp) 410 | 411 | In [2]: response.etherscan_response 412 | Out[2]: { 413 | "status":"1", 414 | "message":"OK", 415 | "result":"21265524714464" 416 | } 417 | 418 | In [3]: response.total_supply 419 | Out[3]: 21265524714464.0 420 | """ 421 | 422 | def parse_response(self): 423 | """ 424 | Parses a token supply by address request response. Example API 425 | response output: 426 | 427 | .. code-block:: python 428 | 429 | { 430 | "status":"1", 431 | "message":"OK", 432 | "result":"21265524714464" 433 | } 434 | 435 | """ 436 | self.total_supply = float(self.etherscan_response.get('result')) 437 | 438 | 439 | class TokenAccountBalanceResponse(EtherscanResponse): 440 | """ 441 | Represents a response object for a token account balance call within the 442 | Etherscan `Tokens` endpoint. 443 | 444 | Available attributes: 445 | - `balance`: The account balance of a token (by contract address) 446 | returned as a float. 447 | 448 | Example: 449 | 450 | .. code-block:: python 451 | In [1]: response = TokenSupplyResponse(resp) 452 | 453 | In [2]: response.etherscan_response 454 | Out[2]: { 455 | "status":"1", 456 | "message":"OK", 457 | "result":"135499" 458 | } 459 | 460 | In [3]: response.balance 461 | Out[3]: 135499.0 462 | """ 463 | 464 | def parse_response(self): 465 | """ 466 | Parses a token account balance request response. Example API 467 | response output: 468 | 469 | .. code-block:: python 470 | 471 | { 472 | "status":"1", 473 | "message":"OK", 474 | "result":"135499" 475 | } 476 | 477 | """ 478 | self.balance = float(self.etherscan_response.get('result')) 479 | 480 | 481 | class BlockRewardsResponse(EtherscanResponse): 482 | """ 483 | Represents a response object for a block / uncle rewards API call to the 484 | Etherscan `Blocks` endpoint. 485 | 486 | Available attributes: 487 | - `rewards_data`: A dict of the rewards for the block and any uncles mined. 488 | 489 | Example: 490 | 491 | .. code-block:: python 492 | In [1]: response = BlockRewardsResponse(resp) 493 | 494 | In [2]: response.rewards_data 495 | Out[2]: { 496 | "blockNumber": "2165403", 497 | "timeStamp": "1472533979", 498 | "blockMiner": "0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3", 499 | "blockReward": "5314181600000000000", 500 | "uncles": [ 501 | { 502 | "miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1", 503 | "unclePosition": "0", 504 | "blockreward": "3750000000000000000" 505 | }, { 506 | "miner": "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce", 507 | "unclePosition": "1", 508 | "blockreward": "3750000000000000000" 509 | } 510 | ], 511 | "uncleInclusionReward": "312500000000000000" 512 | } 513 | """ 514 | 515 | def parse_response(self): 516 | """ 517 | Parses a token account balance request response. Example API 518 | response output: 519 | 520 | .. code-block:: python 521 | 522 | { 523 | "status": "1", 524 | "message": "OK", 525 | "result": { 526 | "blockNumber": "2165403", 527 | "timeStamp": "1472533979", 528 | "blockMiner": "0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3", 529 | "blockReward": "5314181600000000000", 530 | "uncles": [ 531 | { 532 | "miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1", 533 | "unclePosition": "0", 534 | "blockreward": "3750000000000000000" 535 | }, { 536 | "miner": "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce", 537 | "unclePosition": "1", 538 | "blockreward": "3750000000000000000" 539 | } 540 | ], 541 | "uncleInclusionReward": "312500000000000000" 542 | } 543 | } 544 | 545 | """ 546 | self.rewards_data = self.etherscan_response.get('result') 547 | -------------------------------------------------------------------------------- /pyetherscan/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | HOME_DIR = os.path.expanduser('~') 5 | CONFIG_FILE = '.pyetherscan.ini' 6 | PATH = os.path.join(HOME_DIR, CONFIG_FILE) 7 | TESTING_API_KEY = 'YourApiKeyToken' 8 | 9 | 10 | def parse_configs(python_version, config_object): 11 | """ 12 | A helper function to parse configuration files in 13 | python 2 and 3. 14 | """ 15 | if python_version < 3: 16 | return config_object.get('Credentials', 'ETHERSCAN_API_KEY') 17 | else: 18 | return config_object['Credentials']['ETHERSCAN_API_KEY'] 19 | 20 | 21 | if os.path.isfile(PATH): 22 | try: 23 | from configparser import ConfigParser 24 | except ImportError: 25 | # Handle python 2.7 code 26 | from ConfigParser import ConfigParser 27 | config = ConfigParser() 28 | config.read(PATH) 29 | ETHERSCAN_API_KEY = parse_configs(sys.version_info[0], config) 30 | 31 | else: 32 | ETHERSCAN_API_KEY = os.environ.get('ETHERSCAN_API_KEY', TESTING_API_KEY) 33 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.4.17 2 | chardet==3.0.4 3 | idna==2.5 4 | requests==2.18.1 5 | retrying==1.3.3 6 | six==1.10.0 7 | urllib3==1.21.1 8 | coverage==4.4.1 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from setuptools import find_packages 3 | from os import path 4 | import sys 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | # Get the long description from the README file 9 | if sys.version_info[0] < 3: 10 | with open(path.join(here, 'README.rst'), 'rb') as f: 11 | long_description = f.read() 12 | else: 13 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 14 | long_description = f.read() 15 | 16 | setup( 17 | name='pyetherscan', 18 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 19 | version='0.1.2', 20 | description='An unofficial wrapper for the Etherscan.io API', 21 | long_description=long_description, 22 | author='Michael Martorella', 23 | author_email='michaelmartorella@gmail.com', 24 | url='https://github.com/Marto32/pyetherscan', 25 | download_url='https://github.com/Marto32/pyetherscan/archive/0.1.2.tar.gz', 26 | keywords=['ethereum', 'blockchain', 'etherscan'], 27 | license='MIT License', 28 | install_requires=[ 29 | 'requests', 30 | 'retrying', 31 | ], 32 | classifiers=[ 33 | 'Development Status :: 3 - Alpha', 34 | 'Intended Audience :: Developers', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Natural Language :: English', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Programming Language :: Python :: 3.5', 41 | 'Programming Language :: Python :: 3.6', 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Marto32/pyetherscan/fb9669f731bf58c196d128ebc893dfbb0ab18aa9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | from pyetherscan import client, response, error 5 | 6 | 7 | class BaseClientTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.client = client.Client() 11 | 12 | def base_etherscan_response_status(self, result): 13 | self.assertEqual(200, result.response_status_code) 14 | self.assertEqual('1', result.status) 15 | self.assertEqual('OK', result.message) 16 | 17 | 18 | class TestInitialization(BaseClientTestCase): 19 | 20 | def test_initialization_objects(self): 21 | 22 | # Test api format error 23 | with self.assertRaises(error.EtherscanInitializationError): 24 | client.Client(apikey=5) 25 | 26 | # Test timeout error 27 | with self.assertRaises(error.EtherscanInitializationError): 28 | client.Client(timeout='5') 29 | 30 | 31 | class TestAccountEndpoint(BaseClientTestCase): 32 | 33 | def test_get_single_balance(self): 34 | expected_bal = 744997704382925139479303.0 35 | 36 | expected_response = { 37 | u'status': u'1', 38 | u'message': u'OK', 39 | u'result': u'744997704382925139479303' 40 | } 41 | address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 42 | result = self.client.get_single_balance(address) 43 | 44 | self.assertEqual(response.SingleAddressBalanceResponse, type(result)) 45 | self.assertEqual(expected_response, result.etherscan_response) 46 | self.assertEqual(expected_bal, result.balance) 47 | self.base_etherscan_response_status(result) 48 | 49 | def test_get_multi_balance(self): 50 | expected_response = { 51 | u'status': u'1', 52 | u'message': u'OK', 53 | u'result': [ 54 | { 55 | u'account': u'0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', 56 | u'balance': u'40807168564070000000000' 57 | }, { 58 | u'account': u'0x63a9975ba31b0b9626b34300f7f627147df1f526', 59 | u'balance': u'332567136222827062478' 60 | }, { 61 | u'account': u'0x198ef1ec325a96cc354c7266a038be8b5c558f67', 62 | u'balance': u'12005264493462223951724' 63 | } 64 | ] 65 | } 66 | addresses = [ 67 | '0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a', 68 | '0x63a9975ba31b0b9626b34300f7f627147df1f526', 69 | '0x198ef1ec325a96cc354c7266a038be8b5c558f67' 70 | ] 71 | result = self.client.get_multi_balance(addresses) 72 | 73 | self.assertEqual(response.MultiAddressBalanceResponse, type(result)) 74 | self.assertEqual(expected_response, result.etherscan_response) 75 | 76 | balances = { 77 | u'0x198ef1ec325a96cc354c7266a038be8b5c558f67': 1.2005264493462224e+22, 78 | u'0x63a9975ba31b0b9626b34300f7f627147df1f526': 3.3256713622282705e+20, 79 | u'0xddbd2b932c763ba5b1b7ae3b362eac3e8d40121a': 4.080716856407e+22 80 | } 81 | self.assertEqual(balances, result.balances) 82 | self.base_etherscan_response_status(result) 83 | 84 | with self.assertRaises(error.EtherscanAddressError): 85 | self.client.get_multi_balance(addresses='') 86 | 87 | with self.assertRaises(error.EtherscanAddressError): 88 | self.client.get_multi_balance(addresses=['' for x in range(30)]) 89 | 90 | def test_get_transactions_by_address(self): 91 | address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 92 | result = self.client.get_transactions_by_address(address) 93 | 94 | self.assertEqual(response.TransactionsByAddressResponse, type(result)) 95 | # self.assertEqual(expected_response_result_sorted, etherscan_response_sorted) 96 | self.base_etherscan_response_status(result) 97 | 98 | def test_get_transactions_by_address_with_block_offset(self): 99 | address = '0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3' 100 | startblock = 0 101 | endblock = 2702578 102 | offset = 10 103 | page = 1 104 | 105 | result = self.client.get_transactions_by_address( 106 | address=address, 107 | startblock=startblock, 108 | endblock=endblock, 109 | offset=offset, 110 | page=page 111 | ) 112 | 113 | self.assertEqual(response.TransactionsByAddressResponse, type(result)) 114 | self.base_etherscan_response_status(result) 115 | 116 | with self.assertRaises(error.EtherscanTransactionError): 117 | self.client.get_transactions_by_address( 118 | address=address, 119 | startblock=startblock, 120 | endblock=endblock, 121 | offset=offset 122 | ) 123 | 124 | def test_get_transaction_by_hash(self): 125 | transaction = { 126 | u'contractAddress': u'', 127 | u'from': u'0x2cac6e4b11d6b58f6d3c1c9d5fe8faa89f60e5a2', 128 | u'timeStamp': u'1466489498', 129 | u'gas': u'2300', 130 | u'errCode': u'', 131 | u'value': u'7106740000000000', 132 | u'blockNumber': u'1743059', 133 | u'to': u'0x66a1c3eaf0f1ffc28d209c0763ed0ca614f3b002', 134 | u'input': u'', 135 | u'type': u'call', 136 | u'isError': u'0', 137 | u'gasUsed': u'0' 138 | } 139 | expected_response = { 140 | u'status': u'1', 141 | u'message': u'OK', 142 | u'result': [transaction] 143 | } 144 | hash = '0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170' 145 | result = self.client.get_transaction_by_hash(hash) 146 | 147 | self.assertEqual(response.TransactionsByHashResponse, type(result)) 148 | self.assertEqual(expected_response, result.etherscan_response) 149 | self.assertEqual(transaction, result.transaction) 150 | self.base_etherscan_response_status(result) 151 | 152 | def test_get_blocks_mined_by_address(self): 153 | expected_response = { 154 | u'status': u'1', 155 | u'message': u'OK', 156 | u'result': [ 157 | { 158 | u'timeStamp': u'1491118514', 159 | u'blockReward': u'5194770940000000000', 160 | u'blockNumber': u'3462296' 161 | } 162 | ] 163 | } 164 | address = '0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b' 165 | result = self.client.get_blocks_mined_by_address(address) 166 | 167 | self.assertEqual(response.BlocksMinedByAddressResponse, type(result)) 168 | 169 | eth_response_result = result.etherscan_response.get('result')[0] 170 | expected_response_result = expected_response.get('result')[0] 171 | self.assertEqual(eth_response_result, expected_response_result) 172 | self.assertEqual( 173 | expected_response.get('status'), 174 | result.etherscan_response.get('status') 175 | ) 176 | self.assertEqual( 177 | expected_response.get('message'), 178 | result.etherscan_response.get('message') 179 | ) 180 | self.base_etherscan_response_status(result) 181 | 182 | def test_get_blocks_mined_by_address_with_block_offset(self): 183 | address = '0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b' 184 | startblock = 0 185 | endblock = 3462296 186 | offset = 10 187 | page = 1 188 | 189 | result = self.client.get_transactions_by_address( 190 | address=address, 191 | startblock=startblock, 192 | endblock=endblock, 193 | offset=offset, 194 | page=page 195 | ) 196 | 197 | self.assertEqual(response.TransactionsByAddressResponse, type(result)) 198 | self.base_etherscan_response_status(result) 199 | 200 | with self.assertRaises(error.EtherscanTransactionError): 201 | self.client.get_transactions_by_address( 202 | address=address, 203 | startblock=startblock, 204 | endblock=endblock, 205 | offset=offset 206 | ) 207 | 208 | 209 | class TestContractEndpoint(BaseClientTestCase): 210 | 211 | def test_get_contract_abi(self): 212 | contract_abi = { 213 | "constant":True, 214 | "inputs":[ 215 | { 216 | "name":"", 217 | "type":"uint256" 218 | } 219 | ], 220 | "name":"proposals", 221 | "outputs":[ 222 | { 223 | "name":"recipient", 224 | "type":"address" 225 | },{ 226 | "name":"amount", 227 | "type":"uint256" 228 | },{ 229 | "name":"description", 230 | "type":"string" 231 | },{ 232 | "name":"votingDeadline", 233 | "type":"uint256" 234 | },{ 235 | "name":"open", 236 | "type":"bool" 237 | },{ 238 | "name":"proposalPassed", 239 | "type":"bool" 240 | },{ 241 | "name":"proposalHash", 242 | "type":"bytes32" 243 | },{ 244 | "name":"proposalDeposit", 245 | "type":"uint256" 246 | },{ 247 | "name":"newCurator", 248 | "type":"bool" 249 | },{ 250 | "name":"yea", 251 | "type":"uint256" 252 | },{ 253 | "name":"nay", 254 | "type":"uint256" 255 | },{ 256 | "name":"creator", 257 | "type":"address" 258 | } 259 | ], 260 | "type":"function" 261 | } 262 | 263 | expected_response = { 264 | u'status': u'1', 265 | u'message': u'OK', 266 | u'result': contract_abi 267 | } 268 | 269 | address = '0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413' 270 | result = self.client.get_contract_abi(address) 271 | 272 | self.assertEqual(response.ContractABIByAddressResponse, type(result)) 273 | 274 | truncated_response = json.loads( 275 | result.etherscan_response.get('result'))[0] 276 | exp_truncated = expected_response.get('result') 277 | 278 | self.assertEqual( 279 | exp_truncated, 280 | truncated_response 281 | ) 282 | 283 | self.assertEqual( 284 | expected_response.get('status'), 285 | result.etherscan_response.get('status') 286 | ) 287 | 288 | self.assertEqual( 289 | expected_response.get('message'), 290 | result.etherscan_response.get('message') 291 | ) 292 | 293 | self.base_etherscan_response_status(result) 294 | 295 | 296 | class TestTransactionsEndpoint(BaseClientTestCase): 297 | 298 | def test_get_contract_execution_status(self): 299 | expected_response = { 300 | u'status': u'1', 301 | u'message': u'OK', 302 | u'result': { 303 | u'isError': u'1', 304 | u'errDescription': 305 | u'Bad jump destination' 306 | } 307 | } 308 | hash = '0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a' 309 | result = self.client.get_contract_execution_status(hash) 310 | 311 | self.assertEqual(response.ContractStatusResponse, type(result)) 312 | self.assertEqual(expected_response, result.etherscan_response) 313 | self.base_etherscan_response_status(result) 314 | 315 | 316 | class TestTokenEndpoint(BaseClientTestCase): 317 | 318 | def test_get_token_supply_by_address(self): 319 | expected_response = { 320 | u'status': u'1', 321 | u'message': u'OK', 322 | u'result': u'21265524714464' 323 | } 324 | address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 325 | result = self.client.get_token_supply_by_address(address) 326 | 327 | self.assertEqual(response.TokenSupplyResponse, type(result)) 328 | self.assertEqual(expected_response, result.etherscan_response) 329 | self.assertEqual(21265524714464.0, result.total_supply) 330 | self.base_etherscan_response_status(result) 331 | 332 | def test_get_token_balance_by_address(self): 333 | expected_response = { 334 | u'status': u'1', 335 | u'message': u'OK', 336 | u'result': u'135499' 337 | } 338 | 339 | contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 340 | account_address = '0xe04f27eb70e025b78871a2ad7eabe85e61212761' 341 | result = self.client.get_token_balance_by_address( 342 | contract_address, 343 | account_address 344 | ) 345 | 346 | self.assertEqual(response.TokenAccountBalanceResponse, type(result)) 347 | self.assertEqual(expected_response, result.etherscan_response) 348 | self.assertEqual(135499.0, result.balance) 349 | self.base_etherscan_response_status(result) 350 | 351 | 352 | class TestBlockEndpoint(BaseClientTestCase): 353 | 354 | def test_get_block_rewards(self): 355 | expected_response = { 356 | "status": "1", 357 | "message": "OK", 358 | "result": { 359 | "blockNumber": "2165403", 360 | "timeStamp": "1472533979", 361 | "blockMiner": "0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3", 362 | "blockReward": "5314181600000000000", 363 | "uncles": [ 364 | { 365 | "miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1", 366 | "unclePosition": "0", 367 | "blockreward": "3750000000000000000" 368 | }, { 369 | "miner": "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce", 370 | "unclePosition": "1", 371 | "blockreward": "3750000000000000000" 372 | } 373 | ], 374 | "uncleInclusionReward": "312500000000000000" 375 | } 376 | } 377 | block_number = 2165403 378 | result = self.client.get_block_and_uncle_rewards_by_block_number( 379 | block_number 380 | ) 381 | 382 | self.assertEqual(response.BlockRewardsResponse, type(result)) 383 | self.assertEqual(expected_response, result.etherscan_response) 384 | self.base_etherscan_response_status(result) 385 | -------------------------------------------------------------------------------- /tests/test_ethereum.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import datetime 3 | 4 | from pyetherscan import response, ethereum, error 5 | 6 | 7 | class BaseEthereumTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | pass 11 | 12 | 13 | class TestAddressObject(BaseEthereumTestCase): 14 | 15 | def test_retrieve_balance(self): 16 | _address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 17 | address = ethereum.Address(address=_address) 18 | self.assertEqual(address.balance, 744997704382925139479303.0) 19 | 20 | with self.assertRaises(error.EtherscanInitializationError): 21 | _bad_address = 5 22 | ethereum.Address(_bad_address) 23 | 24 | def test_transaction_property(self): 25 | _address = '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae' 26 | address = ethereum.Address(address=_address) 27 | self.assertIsInstance( 28 | address.transactions, 29 | ethereum.TransactionContainer 30 | ) 31 | 32 | def test_token_balance(self): 33 | contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 34 | _address = '0xe04f27eb70e025b78871a2ad7eabe85e61212761' 35 | address = ethereum.Address(address=_address) 36 | 37 | token_balance = address.token_balance(contract_address) 38 | self.assertEqual(token_balance, 135499.0) 39 | 40 | def test_blocks_mined(self): 41 | _address = '0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b' 42 | address = ethereum.Address(address=_address) 43 | 44 | expected_block_number = 3462296 45 | block_number = address.blocks_mined[0].block_number 46 | self.assertEqual(expected_block_number, block_number) 47 | 48 | 49 | class TestTransactionObject(BaseEthereumTestCase): 50 | 51 | data = { 52 | "blockNumber": "80240", 53 | "timeStamp": "1439482422", 54 | "hash": "0x72f2508c262763d5ae0e51d71c0d50c881cc75c872152716b04256" 55 | "fe07797dcd", 56 | "nonce": "2", 57 | "blockHash": "0xb9367a1bc9094d6275ab50f4a58ce13186e35a46de68f505" 58 | "3487a578abf00361", 59 | "transactionIndex": "0", 60 | "from": "0xc5a96db085dda36ffbe390f455315d30d6d3dc52", 61 | "to": "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", 62 | "value": "0", 63 | "gas": "377583", 64 | "gasPrice": "500000000000", 65 | "isError": "0", 66 | "input": "0xf00d4b5d00000000000000000000000005096a47749d8bfab0a90" 67 | "c1bb7a95115dbe4cea60000000000000000000000005ed8cee6b63b1c6a" 68 | "fce3ad7c92f4fd7e1b8fad9f", 69 | "contractAddress": "", 70 | "cumulativeGasUsed": "122207", 71 | "gasUsed": "122207", 72 | "confirmations": "3929454" 73 | } 74 | 75 | def test_initialization(self): 76 | with self.assertRaises(error.EtherscanInitializationError): 77 | ethereum.Transaction('') 78 | 79 | def test_transaction_attributes(self): 80 | 81 | transaction = ethereum.Transaction(data=self.data) 82 | 83 | self.assertEqual(transaction._data, self.data) 84 | self.assertEqual(transaction.from_, self.data.get('from')) 85 | self.assertEqual(transaction.hash, self.data.get('hash')) 86 | self.assertEqual(transaction.nonce, self.data.get('nonce')) 87 | self.assertEqual(transaction.block_hash, self.data.get('blockHash')) 88 | self.assertEqual(transaction.to, self.data.get('to')) 89 | self.assertEqual(transaction.value, float(self.data.get('value'))) 90 | self.assertEqual(transaction.gas, float(self.data.get('gas'))) 91 | self.assertEqual(transaction.input, self.data.get('input')) 92 | self.assertEqual(transaction.gas_used, float(self.data.get('gasUsed'))) 93 | self.assertEqual( 94 | transaction.gas_price, 95 | float(self.data.get('gasPrice'))) 96 | self.assertEqual( 97 | transaction.confirmations, 98 | self.data.get('confirmations')) 99 | self.assertEqual( 100 | transaction.cumulative_gas_used, 101 | float(self.data.get('cumulativeGasUsed'))) 102 | self.assertEqual( 103 | transaction.contract_address, 104 | self.data.get('contractAddress')) 105 | self.assertEqual( 106 | transaction.transaction_index, 107 | int(self.data.get('transactionIndex'))) 108 | self.assertEqual( 109 | transaction.time_stamp, 110 | int(self.data.get('timeStamp'))) 111 | self.assertEqual( 112 | transaction.block_number, 113 | int(self.data.get('blockNumber'))) 114 | 115 | datetime_ex = datetime.datetime.utcfromtimestamp( 116 | int(self.data.get('timeStamp')) 117 | ) 118 | self.assertEqual(transaction.datetime_executed, datetime_ex) 119 | 120 | def test_transaction_block(self): 121 | transaction = ethereum.Transaction(data=self.data) 122 | block = ethereum.Block(80240) 123 | expected_miner = block.block_miner 124 | expected_reward = block.block_reward 125 | expected_datetime_mined = block.datetime_mined 126 | 127 | self.assertEqual( 128 | expected_miner, 129 | transaction.block.block_miner 130 | ) 131 | self.assertEqual( 132 | expected_reward, 133 | transaction.block.block_reward 134 | ) 135 | self.assertEqual( 136 | expected_datetime_mined, 137 | transaction.block.datetime_mined 138 | ) 139 | 140 | def test_transaction_type(self): 141 | data = { 142 | "blockNumber": "2535368", 143 | "timeStamp": "1477837690", 144 | "hash": "0x8a1a9989bda84f80143181a68bc137ecefa64d0d4ebde45dd9' \ 145 | '4fc0cf49e70cb6", 146 | "from": "0x20d42f2e99a421147acf198d775395cac2e8b03d", 147 | "to": "", 148 | "value": "0", 149 | "contractAddress": "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3", 150 | "input": "", 151 | "type": "create", 152 | "gas": "254791", 153 | "gasUsed": "46750", 154 | "traceId": "0", 155 | "isError": "0", 156 | "errCode": "" 157 | } 158 | 159 | transaction = ethereum.Transaction(data=data) 160 | self.assertEqual(transaction.type, 'create') 161 | 162 | 163 | class TestTransactionContainer(BaseEthereumTestCase): 164 | 165 | data = { 166 | "blockNumber": "80240", 167 | "timeStamp": "1439482422", 168 | "hash": "0x72f2508c262763d5ae0e51d71c0d50c881cc75c872152716b04256" 169 | "fe07797dcd", 170 | "nonce": "2", 171 | "blockHash": "0xb9367a1bc9094d6275ab50f4a58ce13186e35a46de68f505" 172 | "3487a578abf00361", 173 | "transactionIndex": "0", 174 | "from": "0xc5a96db085dda36ffbe390f455315d30d6d3dc52", 175 | "to": "0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", 176 | "value": "0", 177 | "gas": "377583", 178 | "gasPrice": "500000000000", 179 | "isError": "0", 180 | "input": "0xf00d4b5d00000000000000000000000005096a47749d8bfab0a90" 181 | "c1bb7a95115dbe4cea60000000000000000000000005ed8cee6b63b1c6a" 182 | "fce3ad7c92f4fd7e1b8fad9f", 183 | "contractAddress": "", 184 | "cumulativeGasUsed": "122207", 185 | "gasUsed": "122207", 186 | "confirmations": "3929454" 187 | } 188 | 189 | def test_retrieval(self): 190 | data_list = [self.data for n in range(5)] 191 | container = ethereum.TransactionContainer(data_list) 192 | self.assertEqual( 193 | container[0].hash, 194 | ethereum.Transaction(self.data).hash 195 | ) 196 | for txn in container: 197 | self.assertEqual( 198 | txn.hash, 199 | ethereum.Transaction(self.data).hash 200 | ) 201 | 202 | 203 | class TestBlockObject(BaseEthereumTestCase): 204 | 205 | data = { 206 | "blockNumber": "2165403", 207 | "timeStamp": "1472533979", 208 | "blockMiner": "0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3", 209 | "blockReward": "5314181600000000000", 210 | "uncles": [ 211 | { 212 | "miner": "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1", 213 | "unclePosition": "0", 214 | "blockreward": "3750000000000000000" 215 | }, { 216 | "miner": "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce", 217 | "unclePosition": "1", 218 | "blockreward": "3750000000000000000" 219 | } 220 | ], 221 | "uncleInclusionReward": "312500000000000000" 222 | } 223 | 224 | uncles = [ 225 | { 226 | "miner": ethereum.Address( 227 | "0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1"), 228 | "block_reward": float("3750000000000000000") 229 | }, { 230 | "miner": ethereum.Address( 231 | "0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce"), 232 | "block_reward": float("3750000000000000000") 233 | } 234 | ] 235 | 236 | def test_initialization(self): 237 | with self.assertRaises(error.EtherscanInitializationError): 238 | ethereum.Block(2.0) 239 | 240 | def test_block_attributes(self): 241 | 242 | block_rewards = ethereum.Block(2165403) 243 | 244 | self.assertEqual( 245 | block_rewards.time_stamp, 246 | int(self.data.get( 247 | 'timeStamp') 248 | ) 249 | ) 250 | self.assertEqual( 251 | block_rewards.block_miner, 252 | self.data.get('blockMiner') 253 | ) 254 | self.assertEqual( 255 | block_rewards.block_reward, 256 | float(self.data.get( 257 | 'blockReward') 258 | ) 259 | ) 260 | self.assertEqual( 261 | block_rewards.uncle_inclusion_reward, 262 | float(self.data.get('uncleInclusionReward')) 263 | ) 264 | 265 | datetime_mined = datetime.datetime.utcfromtimestamp( 266 | int(self.data.get('timeStamp')) 267 | ) 268 | self.assertEqual(block_rewards.datetime_mined, datetime_mined) 269 | 270 | # test uncles 271 | uncle_one_address = block_rewards.uncles[0]['miner'] 272 | uncle_one_reward = block_rewards.uncles[0]['block_reward'] 273 | 274 | expected_uncle_address = self.uncles[0]['miner'].address 275 | expected_uncle_reward = self.uncles[0]['block_reward'] 276 | 277 | self.assertEqual(uncle_one_address, expected_uncle_address) 278 | self.assertEqual(uncle_one_reward, expected_uncle_reward) 279 | 280 | 281 | class TestBlockContainer(BaseEthereumTestCase): 282 | 283 | data = { 284 | "blockNumber": "2691400", 285 | "timeStamp": "1480072029", 286 | "blockReward": "5086562212310617100" 287 | } 288 | 289 | def test_retrieval(self): 290 | data_list = [self.data for _ in range(5)] 291 | 292 | container = ethereum.BlockContainer(data_list) 293 | expected_block_number = int(ethereum.Block( 294 | self.data.get('blockNumber') 295 | ).block_number) 296 | 297 | self.assertEqual( 298 | container[0].block_number, 299 | expected_block_number 300 | ) 301 | 302 | for block in container: 303 | self.assertEqual( 304 | block.block_number, 305 | expected_block_number 306 | ) 307 | 308 | 309 | class TestTokenObject(BaseEthereumTestCase): 310 | 311 | def test_initialization(self): 312 | with self.assertRaises(error.EtherscanInitializationError): 313 | _bad_address = 5 314 | ethereum.Token(_bad_address) 315 | 316 | def test_token_balance(self): 317 | expected = { 318 | "status": "1", 319 | "message": "OK", 320 | "result": "135499" 321 | } 322 | 323 | _contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 324 | _address = '0xe04f27eb70e025b78871a2ad7eabe85e61212761' 325 | token = ethereum.Token(contract_address=_contract_address) 326 | 327 | self.assertEqual( 328 | token.token_balance(_address), 329 | float(expected.get('result')) 330 | ) 331 | 332 | def test_token_supply(self): 333 | expected = 21265524714464.0 334 | _contract_address = '0x57d90b64a1a57749b0f932f1a3395792e12e7055' 335 | token = ethereum.Token(contract_address=_contract_address) 336 | self.assertEqual( 337 | token.supply, 338 | expected 339 | ) 340 | -------------------------------------------------------------------------------- /tests/test_response.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests related to response objects. 3 | """ 4 | import unittest 5 | import requests 6 | 7 | from pyetherscan import client, response, error 8 | 9 | 10 | class FakeResponse(requests.Response): 11 | """Fake instance of a Response object""" 12 | 13 | def __init__(self, status_code, text): 14 | requests.Response.__init__(self) 15 | 16 | self.status_code = status_code 17 | self._text = text 18 | 19 | @property 20 | def text(self): 21 | return self._text 22 | 23 | 24 | class BaseResponseTestCase(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.client = client.Client() 28 | 29 | def base_request_error(self, code, text): 30 | """Abstract testing for request errors""" 31 | resp = FakeResponse(code, text) 32 | with self.assertRaises(error.EtherscanRequestError): 33 | response.SingleAddressBalanceResponse(resp) 34 | 35 | 36 | class TestInitializationResponses(BaseResponseTestCase): 37 | 38 | def test_rate_limit_error(self): 39 | self.base_request_error(403, '') 40 | 41 | def test_invalid_request(self): 42 | self.base_request_error(200, '') 43 | 44 | def test_bad_code_error(self): 45 | self.base_request_error(405, '') 46 | 47 | def test_data_error(self): 48 | text = "{\"message\":\"NOTOK\", \"result\":\"Error!\"}" 49 | resp = FakeResponse(200, text) 50 | 51 | with self.assertRaises(error.EtherscanDataError): 52 | response.SingleAddressBalanceResponse(resp) 53 | --------------------------------------------------------------------------------