├── .github └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── aioetherscan ├── __init__.py ├── client.py ├── common.py ├── exceptions.py ├── modules │ ├── account.py │ ├── base.py │ ├── block.py │ ├── contract.py │ ├── extra │ │ ├── __init__.py │ │ ├── contract.py │ │ ├── generators │ │ │ ├── blocks_parser.py │ │ │ ├── blocks_range.py │ │ │ ├── generator_utils.py │ │ │ └── helpers.py │ │ └── link.py │ ├── gas_tracker.py │ ├── logs.py │ ├── proxy.py │ ├── stats.py │ ├── token.py │ └── transaction.py ├── network.py └── url_builder.py ├── pyproject.toml └── tests ├── extra ├── generators │ ├── test_blocks_parser.py │ ├── test_blocks_range.py │ ├── test_generator_utils.py │ └── test_helpers.py ├── test_contract_utils.py └── test_link_utils.py ├── test_account.py ├── test_block.py ├── test_client.py ├── test_common.py ├── test_contract.py ├── test_exceptions.py ├── test_gas_tracker.py ├── test_logs.py ├── test_network.py ├── test_proxy.py ├── test_stats.py ├── test_token.py ├── test_transaction.py └── test_url_builder.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: push 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.9", "3.10", "3.11", "3.12"] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Load cached Poetry installation 19 | id: cached-poetry 20 | uses: actions/cache@v4 21 | with: 22 | path: ~/.local # the path depends on the OS 23 | key: poetry-0 # increment to reset cache 24 | - name: Install Poetry 25 | if: steps.cached-poetry.outputs.cache-hit != 'true' 26 | uses: snok/install-poetry@v1 27 | with: 28 | virtualenvs-create: true 29 | virtualenvs-in-project: true 30 | - name: Load cached venv 31 | id: cached-poetry-dependencies 32 | uses: actions/cache@v4 33 | with: 34 | path: .venv 35 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 36 | - name: Install dependencies 37 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 38 | run: poetry install --no-interaction --no-root --with=dev 39 | - name: Test with pytest 40 | run: | 41 | source .venv/bin/activate 42 | coverage run --source=aioetherscan -m pytest tests/ 43 | - name: Coveralls 44 | uses: AndreMiras/coveralls-python-action@develop 45 | with: 46 | parallel: true 47 | flag-name: Unit Test 48 | 49 | coveralls_finish: 50 | needs: build 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Coveralls Finished 54 | uses: AndreMiras/coveralls-python-action@develop 55 | with: 56 | parallel-finished: true 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | # Edit at https://www.gitignore.io/?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | # Pyre type checker 119 | .pyre/ 120 | 121 | ### Python Patch ### 122 | .venv/ 123 | 124 | ### Python.VirtualEnv Stack ### 125 | # Virtualenv 126 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 127 | [Bb]in 128 | [Ii]nclude 129 | [Ll]ib 130 | [Ll]ib64 131 | [Ll]ocal 132 | [Ss]cripts 133 | pyvenv.cfg 134 | pip-selfcheck.json 135 | 136 | # End of https://www.gitignore.io/api/python 137 | .idea/ 138 | test.py 139 | tst.py 140 | 141 | .DS_Store 142 | 143 | # vscode 144 | .vscode/ 145 | 146 | # do not commit pinned dependencies 147 | poetry.lock 148 | requirements.dev.txt 149 | requirements.txt 150 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-toml 7 | - id: debug-statements 8 | - id: detect-private-key 9 | - id: name-tests-test 10 | args: [--pytest-test-first] 11 | - id: end-of-file-fixer 12 | - id: trailing-whitespace 13 | - id: double-quote-string-fixer 14 | - repo: https://github.com/python-poetry/poetry 15 | rev: 1.8.3 16 | hooks: 17 | - id: poetry-check 18 | - repo: https://github.com/astral-sh/ruff-pre-commit 19 | # Ruff version. 20 | rev: v0.4.9 21 | hooks: 22 | - id: ruff 23 | args: [ --fix, --show-fixes] 24 | - id: ruff-format 25 | - repo: https://github.com/asottile/pyupgrade 26 | rev: v3.16.0 27 | hooks: 28 | - id: pyupgrade 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ape364 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aioetherscan 2 | 3 | [![PyPi](https://img.shields.io/pypi/v/aioetherscan.svg)](https://pypi.org/project/aioetherscan/) 4 | [![License](https://img.shields.io/pypi/l/aioetherscan.svg)](https://pypi.org/project/aioetherscan/) 5 | [![Coveralls](https://img.shields.io/coveralls/ape364/aioetherscan.svg)](https://coveralls.io/github/ape364/aioetherscan) 6 | [![Versions](https://img.shields.io/pypi/pyversions/aioetherscan.svg)](https://pypi.org/project/aioetherscan/) 7 | 8 | 9 | [Etherscan.io](https://etherscan.io) [API](https://etherscan.io/apis) async Python non-official wrapper. 10 | 11 | ## Features 12 | 13 | ### API modules 14 | 15 | Supports all API modules: 16 | 17 | * [Accounts](https://docs.etherscan.io/api-endpoints/accounts) 18 | * [Contracts](https://docs.etherscan.io/api-endpoints/contracts) 19 | * [Transactions](https://docs.etherscan.io/api-endpoints/stats) 20 | * [Blocks](https://docs.etherscan.io/api-endpoints/blocks) 21 | * [Event logs](https://docs.etherscan.io/api-endpoints/logs) 22 | * [GETH/Parity proxy](https://docs.etherscan.io/api-endpoints/geth-parity-proxy) 23 | * [Tokens](https://docs.etherscan.io/api-endpoints/tokens) 24 | * [Gas Tracker](https://docs.etherscan.io/api-endpoints/gas-tracker) 25 | * [Stats](https://docs.etherscan.io/api-endpoints/stats-1) 26 | 27 | Also provides `extra` module, which supports: 28 | * `link` helps to compose links to address/tx/etc 29 | * `contract` helps to fetch contract data 30 | * `generators` allows to fetch a lot of transactions without timeouts and not getting banned 31 | 32 | ### Blockchains 33 | 34 | Supports blockchain explorers: 35 | 36 | * [Etherscan](https://docs.etherscan.io/getting-started/endpoint-urls) 37 | * [BscScan](https://docs.bscscan.com/getting-started/endpoint-urls) 38 | * [SnowTrace](https://snowtrace.io/documentation/etherscan-compatibility/accounts) 39 | * [PolygonScan](https://docs.polygonscan.com/getting-started/endpoint-urls) 40 | * [Optimism](https://docs.optimism.etherscan.io/getting-started/endpoint-urls) 41 | * [Arbiscan](https://docs.arbiscan.io/getting-started/endpoint-urls) 42 | * [FtmScan](https://docs.ftmscan.com/getting-started/endpoint-urls) 43 | * [Basescan](https://docs.basescan.org/getting-started/endpoint-urls) 44 | * [Taikoscan](https://docs.taikoscan.io/getting-started/endpoint-urls) 45 | * [SnowScan](https://docs.snowscan.xyz/getting-started/endpoint-urls) 46 | 47 | ## Installation 48 | 49 | ```sh 50 | pip install -U aioetherscan 51 | ``` 52 | 53 | ## Usage 54 | Register Etherscan account and [create free API key](https://etherscan.io/myapikey). 55 | 56 | ```python 57 | import asyncio 58 | import logging 59 | 60 | from aiohttp_retry import ExponentialRetry 61 | from asyncio_throttle import Throttler 62 | 63 | from aioetherscan import Client 64 | 65 | logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) 66 | 67 | 68 | async def main(): 69 | throttler = Throttler(rate_limit=4, period=1.0) 70 | retry_options = ExponentialRetry(attempts=2) 71 | 72 | c = Client('YourApiKeyToken', throttler=throttler, retry_options=retry_options) 73 | 74 | try: 75 | print(await c.stats.eth_price()) 76 | print(await c.block.block_reward(123456)) 77 | 78 | address = '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2' 79 | async for t in c.extra.generators.token_transfers( 80 | address=address, 81 | start_block=19921833, 82 | end_block=19960851 83 | ): 84 | print(t) 85 | print(c.extra.link.get_tx_link(t['hash'])) 86 | 87 | print(c.extra.link.get_address_link(address)) 88 | finally: 89 | await c.close() 90 | 91 | 92 | if __name__ == '__main__': 93 | asyncio.run(main()) 94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /aioetherscan/__init__.py: -------------------------------------------------------------------------------- 1 | from aioetherscan.client import Client # noqa: F401 2 | -------------------------------------------------------------------------------- /aioetherscan/client.py: -------------------------------------------------------------------------------- 1 | from asyncio import AbstractEventLoop 2 | from typing import AsyncContextManager 3 | 4 | from aiohttp import ClientTimeout 5 | from aiohttp_retry import RetryOptionsBase 6 | 7 | from aioetherscan.modules.account import Account 8 | from aioetherscan.modules.block import Block 9 | from aioetherscan.modules.contract import Contract 10 | from aioetherscan.modules.extra import ExtraModules 11 | from aioetherscan.modules.gas_tracker import GasTracker 12 | from aioetherscan.modules.logs import Logs 13 | from aioetherscan.modules.proxy import Proxy 14 | from aioetherscan.modules.stats import Stats 15 | from aioetherscan.modules.token import Token 16 | from aioetherscan.modules.transaction import Transaction 17 | from aioetherscan.network import Network, UrlBuilder 18 | 19 | 20 | class Client: 21 | def __init__( 22 | self, 23 | api_key: str, 24 | api_kind: str = 'eth', 25 | network: str = 'main', 26 | loop: AbstractEventLoop = None, 27 | timeout: ClientTimeout = None, 28 | proxy: str = None, 29 | throttler: AsyncContextManager = None, 30 | retry_options: RetryOptionsBase = None, 31 | ) -> None: 32 | self._url_builder = UrlBuilder(api_key, api_kind, network) 33 | self._http = Network(self._url_builder, loop, timeout, proxy, throttler, retry_options) 34 | 35 | self.account = Account(self) 36 | self.block = Block(self) 37 | self.contract = Contract(self) 38 | self.transaction = Transaction(self) 39 | self.stats = Stats(self) 40 | self.logs = Logs(self) 41 | self.proxy = Proxy(self) 42 | self.token = Token(self) 43 | self.gas_tracker = GasTracker(self) 44 | 45 | self.extra = ExtraModules(self, self._url_builder) 46 | 47 | @property 48 | def currency(self) -> str: 49 | return self._url_builder.currency 50 | 51 | @property 52 | def api_kind(self) -> str: 53 | return self._url_builder.api_kind.title() 54 | 55 | @property 56 | def scaner_url(self) -> str: 57 | return self._url_builder.BASE_URL 58 | 59 | async def close(self): 60 | await self._http.close() 61 | -------------------------------------------------------------------------------- /aioetherscan/common.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from typing import Union 3 | 4 | 5 | def check_value(value: str, values: tuple[str, ...]) -> str: 6 | if value and value.lower() not in values: 7 | raise ValueError(f'Invalid value {value!r}, only {values} are supported.') 8 | return value 9 | 10 | 11 | def check_hex(number: Union[str, int]) -> str: 12 | if isinstance(number, int): 13 | return hex(number) 14 | try: 15 | int(number, 16) 16 | except ValueError as e: 17 | raise ValueError(f'Invalid hex parameter {number!r}: {e}') 18 | else: 19 | return number 20 | 21 | 22 | def check_tag(tag: Union[str, int]) -> str: 23 | _TAGS = ( 24 | 'earliest', # the earliest/genesis block 25 | 'latest', # the latest mined block 26 | 'pending', # for the pending state/transactions 27 | ) 28 | 29 | if tag in _TAGS: 30 | return tag 31 | return check_hex(tag) 32 | 33 | 34 | def check_sort_direction(sort: str) -> str: 35 | _SORT_ORDERS = ( 36 | 'asc', # ascending order 37 | 'desc', # descending order 38 | ) 39 | return check_value(sort, _SORT_ORDERS) 40 | 41 | 42 | def check_blocktype(blocktype: str) -> str: 43 | _BLOCK_TYPES = ( 44 | 'blocks', # full blocks only 45 | 'uncles', # uncle blocks only 46 | ) 47 | return check_value(blocktype, _BLOCK_TYPES) 48 | 49 | 50 | def check_closest_value(closest_value: str) -> str: 51 | _CLOSEST_VALUES = ( 52 | 'before', # ascending order 53 | 'after', # descending order 54 | ) 55 | 56 | return check_value(closest_value, _CLOSEST_VALUES) 57 | 58 | 59 | def check_client_type(client_type: str) -> str: 60 | _CLIENT_TYPES = ( 61 | 'geth', 62 | 'parity', 63 | ) 64 | 65 | return check_value(client_type, _CLIENT_TYPES) 66 | 67 | 68 | def check_sync_mode(sync_mode: str) -> str: 69 | _SYNC_MODES = ( 70 | 'default', 71 | 'archive', 72 | ) 73 | 74 | return check_value(sync_mode, _SYNC_MODES) 75 | 76 | 77 | def check_token_standard(token_standard: str) -> str: 78 | _TOKEN_STANDARDS = ( 79 | 'erc20', 80 | 'erc721', 81 | 'erc1155', 82 | ) 83 | 84 | return check_value(token_standard, _TOKEN_STANDARDS) 85 | 86 | 87 | def get_daily_stats_params(action: str, start_date: date, end_date: date, sort: str) -> dict: 88 | return dict( 89 | module='stats', 90 | action=action, 91 | startdate=start_date.isoformat(), 92 | enddate=end_date.isoformat(), 93 | sort=check_sort_direction(sort), 94 | ) 95 | -------------------------------------------------------------------------------- /aioetherscan/exceptions.py: -------------------------------------------------------------------------------- 1 | class EtherscanClientError(Exception): 2 | pass 3 | 4 | 5 | class EtherscanClientContentTypeError(EtherscanClientError): 6 | def __init__(self, status, content): 7 | self.status = status 8 | self.content = content 9 | 10 | def __str__(self): 11 | return f'[{self.status}] {self.content!r}' 12 | 13 | 14 | class EtherscanClientApiError(EtherscanClientError): 15 | def __init__(self, message, result): 16 | self.message = message 17 | self.result = result 18 | 19 | def __str__(self): 20 | return f'[{self.message}] {self.result}' 21 | 22 | 23 | class EtherscanClientProxyError(EtherscanClientError): 24 | """JSON-RPC 2.0 Specification 25 | 26 | https://www.jsonrpc.org/specification#error_object 27 | """ 28 | 29 | def __init__(self, code, message): 30 | self.code = code 31 | self.message = message 32 | 33 | def __str__(self): 34 | return f'[{self.code}] {self.message}' 35 | -------------------------------------------------------------------------------- /aioetherscan/modules/account.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | 3 | from aioetherscan.common import ( 4 | check_tag, 5 | check_sort_direction, 6 | check_blocktype, 7 | check_token_standard, 8 | ) 9 | from aioetherscan.modules.base import BaseModule 10 | 11 | 12 | class Account(BaseModule): 13 | """Accounts 14 | 15 | https://docs.etherscan.io/api-endpoints/accounts 16 | """ 17 | 18 | @property 19 | def _module(self) -> str: 20 | return 'account' 21 | 22 | async def balance(self, address: str, tag: str = 'latest') -> str: 23 | """Get Ether Balance for a single Address.""" 24 | return await self._get(action='balance', address=address, tag=check_tag(tag)) 25 | 26 | async def balances(self, addresses: Iterable[str], tag: str = 'latest') -> list[dict]: 27 | """Get Ether Balance for multiple Addresses in a single call.""" 28 | return await self._get( 29 | action='balancemulti', address=','.join(addresses), tag=check_tag(tag) 30 | ) 31 | 32 | async def normal_txs( 33 | self, 34 | address: str, 35 | start_block: Optional[int] = None, 36 | end_block: Optional[int] = None, 37 | sort: Optional[str] = None, 38 | page: Optional[int] = None, 39 | offset: Optional[int] = None, 40 | ) -> list[dict]: 41 | """Get a list of 'Normal' Transactions By Address.""" 42 | return await self._get( 43 | action='txlist', 44 | address=address, 45 | startblock=start_block, 46 | endblock=end_block, 47 | sort=check_sort_direction(sort), 48 | page=page, 49 | offset=offset, 50 | ) 51 | 52 | async def internal_txs( 53 | self, 54 | address: str, 55 | start_block: Optional[int] = None, 56 | end_block: Optional[int] = None, 57 | sort: Optional[str] = None, 58 | page: Optional[int] = None, 59 | offset: Optional[int] = None, 60 | txhash: Optional[str] = None, 61 | ) -> list[dict]: 62 | """Get a list of 'Internal' Transactions by Address or Transaction Hash.""" 63 | return await self._get( 64 | action='txlistinternal', 65 | address=address, 66 | startblock=start_block, 67 | endblock=end_block, 68 | sort=check_sort_direction(sort), 69 | page=page, 70 | offset=offset, 71 | txhash=txhash, 72 | ) 73 | 74 | async def token_transfers( 75 | self, 76 | address: Optional[str] = None, 77 | contract_address: Optional[str] = None, 78 | start_block: Optional[int] = None, 79 | end_block: Optional[int] = None, 80 | sort: Optional[str] = None, 81 | page: Optional[int] = None, 82 | offset: Optional[int] = None, 83 | token_standard: str = 'erc20', 84 | ) -> list[dict]: 85 | """Get a list of "ERC20 - Token Transfer Events" by Address""" 86 | if not address and not contract_address: 87 | raise ValueError('At least one of address or contract_address must be specified.') 88 | 89 | token_standard = check_token_standard(token_standard) 90 | actions = dict(erc20='tokentx', erc721='tokennfttx', erc1155='token1155tx') 91 | 92 | return await self._get( 93 | action=actions.get(token_standard), 94 | address=address, 95 | startblock=start_block, 96 | endblock=end_block, 97 | sort=check_sort_direction(sort), 98 | page=page, 99 | offset=offset, 100 | contractaddress=contract_address, 101 | ) 102 | 103 | async def mined_blocks( 104 | self, 105 | address: str, 106 | blocktype: str = 'blocks', 107 | page: Optional[int] = None, 108 | offset: Optional[int] = None, 109 | ) -> list: 110 | """Get list of Blocks Validated by Address""" 111 | return await self._get( 112 | action='getminedblocks', 113 | address=address, 114 | blocktype=check_blocktype(blocktype), 115 | page=page, 116 | offset=offset, 117 | ) 118 | 119 | async def beacon_chain_withdrawals( 120 | self, 121 | address: str, 122 | start_block: Optional[int] = None, 123 | end_block: Optional[int] = None, 124 | sort: Optional[str] = None, 125 | page: Optional[int] = None, 126 | offset: Optional[int] = None, 127 | ) -> list[dict]: 128 | """Get Beacon Chain Withdrawals by Address and Block Range""" 129 | return await self._get( 130 | action='txsBeaconWithdrawal', 131 | address=address, 132 | startblock=start_block, 133 | endblock=end_block, 134 | sort=check_sort_direction(sort), 135 | page=page, 136 | offset=offset, 137 | ) 138 | 139 | async def account_balance_by_blockno(self, address: str, blockno: int) -> str: 140 | """Get Historical Ether Balance for a Single Address By BlockNo""" 141 | return await self._get( 142 | module='account', action='balancehistory', address=address, blockno=blockno 143 | ) 144 | -------------------------------------------------------------------------------- /aioetherscan/modules/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class BaseModule(ABC): 5 | def __init__(self, client): 6 | self._client = client 7 | 8 | @property 9 | @abstractmethod 10 | def _module(self) -> str: 11 | """Returns API module name.""" 12 | 13 | async def _get(self, **params): 14 | return await self._client._http.get(params={**dict(module=self._module), **params}) 15 | 16 | async def _post(self, **params): 17 | return await self._client._http.post(data={**dict(module=self._module), **params}) 18 | -------------------------------------------------------------------------------- /aioetherscan/modules/block.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from typing import Optional 3 | 4 | from aioetherscan.common import check_closest_value, get_daily_stats_params 5 | from aioetherscan.modules.base import BaseModule 6 | 7 | 8 | class Block(BaseModule): 9 | """Blocks 10 | 11 | https://docs.etherscan.io/api-endpoints/blocks 12 | """ 13 | 14 | @property 15 | def _module(self) -> str: 16 | return 'block' 17 | 18 | async def block_reward(self, blockno: int) -> dict: 19 | """Get Block And Uncle Rewards by BlockNo""" 20 | return await self._get(action='getblockreward', blockno=blockno) 21 | 22 | async def est_block_countdown_time(self, blockno: int) -> dict: 23 | """Get Estimated Block Countdown Time by BlockNo""" 24 | return await self._get(action='getblockcountdown', blockno=blockno) 25 | 26 | async def block_number_by_ts(self, ts: int, closest: str) -> dict: 27 | """Get Block Number by Timestamp""" 28 | return await self._get( 29 | action='getblocknobytime', timestamp=ts, closest=check_closest_value(closest) 30 | ) 31 | 32 | async def daily_average_block_size( 33 | self, start_date: date, end_date: date, sort: Optional[str] = None 34 | ) -> dict: 35 | """Get Daily Average Block Size""" 36 | return await self._get( 37 | **get_daily_stats_params('dailyavgblocksize', start_date, end_date, sort) 38 | ) 39 | 40 | async def daily_block_count( 41 | self, start_date: date, end_date: date, sort: Optional[str] = None 42 | ) -> dict: 43 | """Get Daily Block Count and Rewards""" 44 | return await self._get( 45 | **get_daily_stats_params('dailyblkcount', start_date, end_date, sort) 46 | ) 47 | 48 | async def daily_block_rewards( 49 | self, start_date: date, end_date: date, sort: Optional[str] = None 50 | ) -> dict: 51 | """Get Daily Block Rewards""" 52 | return await self._get( 53 | **get_daily_stats_params('dailyblockrewards', start_date, end_date, sort) 54 | ) 55 | 56 | async def daily_average_time_for_a_block( 57 | self, start_date: date, end_date: date, sort: Optional[str] = None 58 | ) -> dict: 59 | """Get Daily Average Time for A Block to be Included in the Ethereum Blockchain""" 60 | return await self._get( 61 | **get_daily_stats_params('dailyavgblocktime', start_date, end_date, sort) 62 | ) 63 | 64 | async def daily_uncle_block_count( 65 | self, start_date: date, end_date: date, sort: Optional[str] = None 66 | ) -> dict: 67 | """Get Daily Uncle Block Count and Rewards""" 68 | return await self._get( 69 | **get_daily_stats_params('dailyuncleblkcount', start_date, end_date, sort) 70 | ) 71 | -------------------------------------------------------------------------------- /aioetherscan/modules/contract.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from aioetherscan.modules.base import BaseModule 4 | 5 | 6 | class Contract(BaseModule): 7 | """Contracts 8 | 9 | https://docs.etherscan.io/api-endpoints/contracts 10 | """ 11 | 12 | @property 13 | def _module(self) -> str: 14 | return 'contract' 15 | 16 | async def contract_abi(self, address: str) -> str: 17 | """Get Contract ABI for Verified Contract Source Codes""" 18 | return await self._get(action='getabi', address=address) 19 | 20 | async def contract_source_code(self, address: str) -> list[dict]: 21 | """Get Contract Source Code for Verified Contract Source Codes""" 22 | return await self._get(action='getsourcecode', address=address) 23 | 24 | async def contract_creation(self, addresses: Iterable[str]) -> list[dict]: 25 | """Get Contract Creator and Creation Tx Hash""" 26 | return await self._get(action='getcontractcreation', contractaddresses=','.join(addresses)) 27 | 28 | async def verify_contract_source_code( 29 | self, 30 | contract_address: str, 31 | source_code: str, 32 | contract_name: str, 33 | compiler_version: str, 34 | optimization_used: bool = False, 35 | runs: int = 200, 36 | constructor_arguements: str = None, 37 | libraries: dict[str, str] = None, 38 | ) -> str: 39 | """Submits a contract source code to Etherscan for verification.""" 40 | return await self._post( 41 | module='contract', 42 | action='verifysourcecode', 43 | contractaddress=contract_address, 44 | sourceCode=source_code, 45 | contractname=contract_name, 46 | compilerversion=compiler_version, 47 | optimizationUsed=1 if optimization_used else 0, 48 | runs=runs, 49 | constructorArguements=constructor_arguements, 50 | **self._parse_libraries(libraries or {}), 51 | ) 52 | 53 | async def check_verification_status(self, guid: str) -> str: 54 | """Check Source code verification submission status""" 55 | return await self._get(action='checkverifystatus', guid=guid) 56 | 57 | async def verify_proxy_contract(self, address: str, expected_implementation: str = None) -> str: 58 | """Submits a proxy contract source code to Etherscan for verification.""" 59 | return await self._post( 60 | module='contract', 61 | action='verifyproxycontract', 62 | address=address, 63 | expectedimplementation=expected_implementation, 64 | ) 65 | 66 | async def check_proxy_contract_verification(self, guid: str) -> str: 67 | """Checking Proxy Contract Verification Submission Status""" 68 | return await self._get(action='checkproxyverification', guid=guid) 69 | 70 | @staticmethod 71 | def _parse_libraries(libraries: dict[str, str]) -> dict[str, str]: 72 | return dict( 73 | part 74 | for i, (name, address) in enumerate(libraries.items(), start=1) 75 | for part in ((f'libraryname{i}', name), (f'libraryaddress{i}', address)) 76 | ) 77 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from aioetherscan.modules.extra.contract import ContractUtils 4 | from aioetherscan.modules.extra.generators.generator_utils import GeneratorUtils 5 | from aioetherscan.modules.extra.link import LinkUtils 6 | from aioetherscan.url_builder import UrlBuilder 7 | 8 | if TYPE_CHECKING: # pragma: no cover 9 | from aioetherscan import Client 10 | 11 | 12 | class ExtraModules: 13 | def __init__(self, client: 'Client', url_builder: UrlBuilder): 14 | self._client = client 15 | self._url_builder = url_builder 16 | 17 | self.link = LinkUtils(self._url_builder) 18 | self.contract = ContractUtils(self._client) 19 | self.generators = GeneratorUtils(self._client) 20 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/contract.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from typing import TYPE_CHECKING 3 | 4 | from aioetherscan.exceptions import EtherscanClientApiError 5 | 6 | if TYPE_CHECKING: # pragma: no cover 7 | from aioetherscan import Client 8 | 9 | 10 | class ContractUtils: 11 | """Helper methods which use the combination of documented APIs.""" 12 | 13 | def __init__(self, client: 'Client'): 14 | self._client = client 15 | 16 | async def is_contract(self, address: str) -> bool: 17 | try: 18 | response = await self._client.contract.contract_abi(address=address) 19 | except EtherscanClientApiError as e: 20 | if ( 21 | e.message.upper() == 'NOTOK' 22 | and e.result.lower() == 'contract source code not verified' 23 | ): 24 | return False 25 | raise 26 | else: 27 | return True if response else False 28 | 29 | async def get_contract_creator(self, contract_address: str) -> Optional[str]: 30 | try: 31 | response = await self._client.account.internal_txs( 32 | address=contract_address, start_block=1, page=1, offset=1 33 | ) # try to find first internal transaction 34 | except EtherscanClientApiError as e: 35 | if e.message == 'No transactions found': 36 | raise 37 | else: 38 | response = None 39 | 40 | if not response: 41 | try: 42 | response = await self._client.account.normal_txs( 43 | address=contract_address, start_block=1, page=1, offset=1 44 | ) # try to find first normal transaction 45 | except EtherscanClientApiError as e: 46 | if e.message == 'No transactions found': 47 | raise 48 | 49 | return next((i['from'].lower() for i in response), None) 50 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/generators/blocks_parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import AsyncIterator, Any, Iterable 3 | from typing import Callable 4 | 5 | from aioetherscan.exceptions import EtherscanClientApiError 6 | from aioetherscan.modules.extra.generators.blocks_range import BlocksRange 7 | from aioetherscan.modules.extra.generators.helpers import get_max_block_number, drop_block 8 | 9 | Transfer = dict[str, Any] 10 | 11 | 12 | class BlocksParser: 13 | _OFFSET: int = 10_000 14 | 15 | def __init__( 16 | self, 17 | api_method: Callable, 18 | request_params: dict[str, Any], 19 | start_block: int, 20 | end_block: int, 21 | blocks_limit: int, 22 | blocks_limit_divider: int, 23 | ) -> None: 24 | self._api_method = api_method 25 | self._request_params = request_params 26 | 27 | self._blocks_range = BlocksRange(start_block, end_block, blocks_limit, blocks_limit_divider) 28 | 29 | self._logger = logging.getLogger(__name__) 30 | self._total_txs = 0 31 | 32 | async def txs_generator(self) -> AsyncIterator[Transfer]: 33 | while self._blocks_range.blocks_left: 34 | try: 35 | blocks_range = self._blocks_range.get_blocks_range() 36 | last_seen_block, transfers = await self._fetch_blocks_range(blocks_range) 37 | except EtherscanClientApiError as e: 38 | self._logger.error(f'Error: {e}') 39 | self._blocks_range.limit.reduce() 40 | else: 41 | self._blocks_range.current_block = last_seen_block + 1 42 | self._blocks_range.limit.restore() 43 | 44 | for transfer in transfers: 45 | yield transfer 46 | 47 | self._logger.info( 48 | f'[{self._blocks_range.blocks_done / self._blocks_range.size:.2%}] ' 49 | f'Current block {self._blocks_range.current_block:,} ' 50 | f'({self._blocks_range.blocks_left:,} blocks left)' 51 | ) 52 | 53 | def _make_request_params(self, blocks_range: range) -> Transfer: 54 | current_params = dict( 55 | start_block=blocks_range.start, 56 | end_block=blocks_range.stop, 57 | page=1, 58 | offset=self._OFFSET, 59 | ) 60 | params = self._request_params | current_params 61 | self._logger.debug(f'Request params: {params}') 62 | return params 63 | 64 | async def _fetch_blocks_range(self, blocks_range: range) -> tuple[int, Iterable[Transfer]]: 65 | try: 66 | request_params = self._make_request_params(blocks_range) 67 | transfers = await self._api_method(**request_params) 68 | except EtherscanClientApiError as e: 69 | if e.message == 'No transactions found': 70 | return blocks_range.stop, [] 71 | raise 72 | else: 73 | if not transfers: 74 | self._logger.debug('No transfers found') 75 | return blocks_range.stop, [] 76 | 77 | transfers_count = len(transfers) 78 | self._total_txs += transfers_count 79 | self._logger.debug(f'Got {transfers_count:,} transfers, {self._total_txs:,} total') 80 | 81 | transfers_max_block = get_max_block_number(transfers) 82 | 83 | if transfers_count == self._OFFSET: 84 | self._logger.debug( 85 | f'Probably not all txs have been fetched, dropping txs with the last block {transfers_max_block:,}' 86 | ) 87 | return transfers_max_block - 1, drop_block(transfers, transfers_max_block) 88 | else: 89 | self._logger.debug('All txs have been fetched') 90 | return transfers_max_block, transfers 91 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/generators/blocks_range.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class Limit: 5 | def __init__(self, limit: int, blocks_range_divider: int) -> None: 6 | self._initial_limit = limit 7 | self._limit = self._initial_limit 8 | 9 | self._blocks_range_divider = blocks_range_divider 10 | 11 | self._logger = logging.getLogger(__name__) 12 | 13 | def get(self) -> int: 14 | self._logger.debug(f'Limit initial/current: {self._initial_limit:,}/{self._limit:,}') 15 | return self._limit 16 | 17 | def reduce(self) -> None: 18 | new_limit = self._limit // self._blocks_range_divider 19 | if new_limit == 0: 20 | raise Exception('Limit is 0') 21 | self._logger.debug(f'Reducing limit from {self._limit:,} to {new_limit:,}') 22 | self._limit = new_limit 23 | 24 | def restore(self) -> None: 25 | self._limit = self._initial_limit 26 | 27 | 28 | class BlocksRange: 29 | def __init__( 30 | self, start_block: int, end_block: int, blocks_limit: int, blocks_limit_divider: int 31 | ) -> None: 32 | self.start_block = start_block 33 | self.end_block = end_block 34 | 35 | self._current_block = start_block 36 | 37 | self.limit = Limit(blocks_limit, blocks_limit_divider) 38 | 39 | self._logger = logging.getLogger(__name__) 40 | 41 | self._logger.debug( 42 | f'Initial blocks range: {self.start_block:,}..{self.end_block:,} ({self.size:,})' 43 | ) 44 | 45 | @property 46 | def current_block(self) -> int: 47 | return self._current_block 48 | 49 | @current_block.setter 50 | def current_block(self, value: int) -> None: 51 | block = min(value, self.end_block) 52 | self._logger.info(f'Current block is changed from {self._current_block:,} to {block:,}') 53 | self._current_block = block 54 | 55 | def get_blocks_range(self) -> range: 56 | start_block = self._current_block 57 | end_block = min(self.end_block, self._current_block + self.limit.get() - 1) 58 | rng = range(start_block, end_block) 59 | self._logger.debug( 60 | f'Returning blocks range: {rng.start:,}..{rng.stop:,} ({rng.stop - rng.start + 1:,})' 61 | ) 62 | return rng 63 | 64 | @property 65 | def blocks_done(self) -> int: 66 | return self._current_block - self.start_block 67 | 68 | @property 69 | def blocks_left(self) -> int: 70 | return self.end_block - self.current_block 71 | 72 | @property 73 | def size(self) -> int: 74 | return self.end_block - self.start_block + 1 75 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/generators/generator_utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import sys 3 | from itertools import count 4 | from typing import Callable, Any, Optional, TYPE_CHECKING, AsyncIterator 5 | 6 | from aioetherscan.exceptions import EtherscanClientApiError 7 | from aioetherscan.modules.extra.generators.blocks_parser import BlocksParser, Transfer 8 | 9 | if TYPE_CHECKING: # pragma: no cover 10 | from aioetherscan import Client 11 | 12 | 13 | class GeneratorUtils: 14 | _DEFAULT_START_BLOCK: int = 0 15 | _DEFAULT_END_BLOCK: int = sys.maxsize 16 | _DEFAULT_BLOCKS_LIMIT: int = 2048 17 | _DEFAULT_BLOCKS_LIMIT_DIVIDER: int = 2 18 | 19 | def __init__(self, client: 'Client') -> None: 20 | self._client = client 21 | 22 | async def token_transfers( 23 | self, 24 | contract_address: str = None, 25 | address: str = None, 26 | start_block: int = _DEFAULT_START_BLOCK, 27 | end_block: int = _DEFAULT_END_BLOCK, 28 | blocks_limit: int = _DEFAULT_BLOCKS_LIMIT, 29 | blocks_limit_divider: int = _DEFAULT_BLOCKS_LIMIT_DIVIDER, 30 | ) -> AsyncIterator[Transfer]: 31 | parser_params = self._get_parser_params(self._client.account.token_transfers, locals()) 32 | async for transfer in self._parse_by_blocks(**parser_params): 33 | yield transfer 34 | 35 | async def normal_txs( 36 | self, 37 | address: str, 38 | start_block: int = _DEFAULT_START_BLOCK, 39 | end_block: int = _DEFAULT_END_BLOCK, 40 | blocks_limit: int = _DEFAULT_BLOCKS_LIMIT, 41 | blocks_limit_divider: int = _DEFAULT_BLOCKS_LIMIT_DIVIDER, 42 | ) -> AsyncIterator[Transfer]: 43 | parser_params = self._get_parser_params(self._client.account.normal_txs, locals()) 44 | async for transfer in self._parse_by_blocks(**parser_params): 45 | yield transfer 46 | 47 | async def internal_txs( 48 | self, 49 | address: str, 50 | start_block: int = _DEFAULT_START_BLOCK, 51 | end_block: int = _DEFAULT_END_BLOCK, 52 | blocks_limit: int = _DEFAULT_BLOCKS_LIMIT, 53 | blocks_limit_divider: int = _DEFAULT_BLOCKS_LIMIT_DIVIDER, 54 | txhash: Optional[str] = None, 55 | ) -> AsyncIterator[Transfer]: 56 | parser_params = self._get_parser_params(self._client.account.internal_txs, locals()) 57 | async for transfer in self._parse_by_blocks(**parser_params): 58 | yield transfer 59 | 60 | async def mined_blocks( 61 | self, address: str, blocktype: str, offset: int = 10_000 62 | ) -> AsyncIterator[Transfer]: 63 | parser_params = self._get_parser_params(self._client.account.mined_blocks, locals()) 64 | async for transfer in self._parse_by_pages(**parser_params): 65 | yield transfer 66 | 67 | async def _parse_by_blocks( 68 | self, 69 | api_method: Callable, 70 | request_params: dict[str, Any], 71 | start_block: int, 72 | end_block: int, 73 | blocks_limit: int, 74 | blocks_limit_divider: int, 75 | ) -> AsyncIterator[Transfer]: 76 | blocks_parser = self._get_blocks_parser( 77 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 78 | ) 79 | async for tx in blocks_parser.txs_generator(): 80 | yield tx 81 | 82 | @staticmethod 83 | async def _parse_by_pages( 84 | api_method: Callable, request_params: dict[str, Any] 85 | ) -> AsyncIterator[Transfer]: 86 | page = count(1) 87 | while True: 88 | request_params['page'] = next(page) 89 | try: 90 | result = await api_method(**request_params) 91 | except EtherscanClientApiError as e: 92 | if e.message == 'No transactions found': 93 | return 94 | raise 95 | else: 96 | for row in result: 97 | yield row 98 | 99 | @staticmethod 100 | def _without_keys(params: dict, excluded_keys: tuple[str, ...] = ('self',)) -> dict: 101 | return {k: v for k, v in params.items() if k not in excluded_keys} 102 | 103 | def _get_parser_params(self, api_method: Callable, params: dict[str, Any]) -> dict[str, Any]: 104 | request_params = self._get_request_params(api_method, params) 105 | return self._without_keys( 106 | dict( 107 | api_method=api_method, 108 | request_params=request_params, 109 | **{k: v for k, v in params.items() if k not in request_params}, 110 | ) 111 | ) 112 | 113 | def _get_request_params(self, api_method: Callable, params: dict[str, Any]) -> dict[str, Any]: 114 | api_method_params = inspect.getfullargspec(api_method).args 115 | return self._without_keys( 116 | {k: v for k, v in params.items() if k in api_method_params}, 117 | ('self', 'start_block', 'end_block'), 118 | ) 119 | 120 | @staticmethod 121 | def _get_blocks_parser( 122 | api_method: Callable, 123 | request_params: dict[str, Any], 124 | start_block: int, 125 | end_block: int, 126 | blocks_limit: int, 127 | blocks_limit_divider: int, 128 | ) -> BlocksParser: 129 | return BlocksParser( 130 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 131 | ) 132 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/generators/helpers.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Iterable 2 | 3 | if TYPE_CHECKING: # pragma: no cover 4 | from aioetherscan.modules.extra.generators.blocks_parser import Transfer 5 | 6 | 7 | def tx_block_number(tx: dict) -> int: 8 | return int(tx['blockNumber']) 9 | 10 | 11 | def drop_block(transfers: list['Transfer'], block_number: int) -> Iterable['Transfer']: 12 | return filter(lambda x: tx_block_number(x) != block_number, transfers) 13 | 14 | 15 | def get_max_block_number(transfers: list['Transfer']) -> int: 16 | tx_with_max_block_number = max(transfers, key=tx_block_number) 17 | return tx_block_number(tx_with_max_block_number) 18 | -------------------------------------------------------------------------------- /aioetherscan/modules/extra/link.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlencode 2 | 3 | from aioetherscan.url_builder import UrlBuilder 4 | 5 | 6 | class LinkUtils: 7 | def __init__(self, url_builder: UrlBuilder): 8 | self._url_builder = url_builder 9 | 10 | def get_address_link(self, address: str) -> str: 11 | return self._url_builder.get_link(f'address/{address}') 12 | 13 | def get_tx_link(self, tx_hash: str) -> str: 14 | return self._url_builder.get_link(f'tx/{tx_hash}') 15 | 16 | def get_block_link(self, block_number: int) -> str: 17 | return self._url_builder.get_link(f'block/{block_number}') 18 | 19 | def get_block_txs_link(self, block_number: int) -> str: 20 | return self._url_builder.get_link(f'txs?{urlencode({"block": block_number})}') 21 | -------------------------------------------------------------------------------- /aioetherscan/modules/gas_tracker.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from typing import Optional 3 | 4 | from aioetherscan.common import check_sort_direction 5 | from aioetherscan.modules.base import BaseModule 6 | 7 | 8 | class GasTracker(BaseModule): 9 | """Gas Tracker 10 | 11 | https://docs.etherscan.io/api-endpoints/gas-tracker 12 | """ 13 | 14 | @property 15 | def _module(self) -> str: 16 | return 'gastracker' 17 | 18 | async def estimation_of_confirmation_time(self, gas_price: int) -> str: 19 | """Get Estimation of Confirmation Time""" 20 | return await self._get(action='gasestimate', gasprice=gas_price) 21 | 22 | async def gas_oracle(self) -> dict: 23 | """Get Gas Oracle""" 24 | return await self._get(action='gasoracle') 25 | 26 | async def daily_average_gas_limit( 27 | self, start_date: date, end_date: date, sort: Optional[str] = None 28 | ) -> list[dict]: 29 | """Get Daily Average Gas Limit""" 30 | return await self._get( 31 | module='stats', 32 | action='dailyavggaslimit', 33 | startdate=start_date.isoformat(), 34 | enddate=end_date.isoformat(), 35 | sort=check_sort_direction(sort), 36 | ) 37 | 38 | async def daily_total_gas_used( 39 | self, start_date: date, end_date: date, sort: Optional[str] = None 40 | ) -> dict: 41 | """Get Ethereum Daily Total Gas Used""" 42 | return await self._get( 43 | module='stats', 44 | action='dailygasused', 45 | startdate=start_date.isoformat(), 46 | enddate=end_date.isoformat(), 47 | sort=check_sort_direction(sort), 48 | ) 49 | 50 | async def daily_average_gas_price( 51 | self, start_date: date, end_date: date, sort: Optional[str] = None 52 | ) -> dict: 53 | """Get Daily Average Gas Price""" 54 | return await self._get( 55 | module='stats', 56 | action='dailyavggasprice', 57 | startdate=start_date.isoformat(), 58 | enddate=end_date.isoformat(), 59 | sort=check_sort_direction(sort), 60 | ) 61 | -------------------------------------------------------------------------------- /aioetherscan/modules/logs.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Literal 2 | 3 | from aioetherscan.modules.base import BaseModule 4 | 5 | TopicNumber = Literal[0, 1, 2, 3] 6 | TopicOperator = Literal['and', 'or'] 7 | 8 | Topics = dict[TopicNumber, str] 9 | TopicOperators = set[tuple[TopicNumber, TopicNumber, TopicOperator]] 10 | 11 | 12 | class Logs(BaseModule): 13 | """Logs 14 | 15 | https://docs.etherscan.io/api-endpoints/logs 16 | """ 17 | 18 | @property 19 | def _module(self) -> str: 20 | return 'logs' 21 | 22 | async def get_logs( 23 | self, 24 | address: Optional[str] = None, 25 | topics: Optional[Topics] = None, 26 | operators: Optional[TopicOperators] = None, 27 | from_block: Optional[int] = None, 28 | to_block: Optional[int] = None, 29 | page: Optional[int] = None, 30 | offset: Optional[int] = None, 31 | ) -> list[dict]: 32 | """Get Event Logs by address and/or topics""" 33 | 34 | if address is None and topics is None: 35 | raise ValueError('Either address or topics must be passed.') 36 | 37 | return await self._get( 38 | action='getLogs', 39 | fromBlock=from_block, 40 | toBlock=to_block, 41 | address=address, 42 | page=page, 43 | offset=offset, 44 | **self._fill_topics(topics, operators), 45 | ) 46 | 47 | def _fill_topics( 48 | self, topics: Optional[Topics], operators: Optional[TopicOperators] 49 | ) -> dict[str, str]: 50 | if not topics: 51 | return {} 52 | 53 | if len(topics) == 1: 54 | topic_number, topic = topics.popitem() 55 | return {f'topic{topic_number}': topic} 56 | 57 | if not operators: 58 | raise ValueError('Topic operators are required when more than 1 topic passed.') 59 | 60 | return { 61 | **{f'topic{topic_number}': topic for topic_number, topic in topics.items()}, 62 | **self._fill_topic_operators(operators), 63 | } 64 | 65 | @staticmethod 66 | def _fill_topic_operators(operators: TopicOperators) -> dict[str, str]: 67 | same_topic_twice = 1 in (len(set(i[:2])) for i in operators) 68 | duplicate = len({frozenset(sorted(i[:2])) for i in operators}) != len(operators) 69 | 70 | if same_topic_twice or duplicate: 71 | raise ValueError( 72 | 'Topic operators must be used with 2 different topics without duplicates.' 73 | ) 74 | 75 | return {f'topic{first}_{second}_opr': opr for first, second, opr in operators} 76 | -------------------------------------------------------------------------------- /aioetherscan/modules/proxy.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from aioetherscan.common import check_hex, check_tag 4 | from aioetherscan.modules.base import BaseModule 5 | 6 | 7 | class Proxy(BaseModule): 8 | """Geth/Parity Proxy 9 | 10 | https://docs.etherscan.io/api-endpoints/geth-parity-proxy 11 | """ 12 | 13 | @property 14 | def _module(self) -> str: 15 | return 'proxy' 16 | 17 | async def block_number(self) -> str: 18 | """Returns the number of most recent block.""" 19 | return await self._get(action='eth_blockNumber') 20 | 21 | async def block_by_number(self, full: bool, tag: Union[int, str] = 'latest') -> dict: 22 | """Returns information about a block by block number.""" 23 | return await self._get( 24 | action='eth_getBlockByNumber', 25 | boolean=full, 26 | tag=check_tag(tag), 27 | ) 28 | 29 | async def uncle_block_by_number_and_index( 30 | self, index: Union[int, str], tag: Union[int, str] = 'latest' 31 | ) -> dict: 32 | """Returns information about a uncle by block number.""" 33 | return await self._get( 34 | action='eth_getUncleByBlockNumberAndIndex', 35 | index=check_hex(index), 36 | tag=check_tag(tag), 37 | ) 38 | 39 | async def block_tx_count_by_number(self, tag: Union[int, str] = 'latest') -> str: 40 | """Returns the number of transactions in a block from a block matching the given block number.""" 41 | return await self._get( 42 | action='eth_getBlockTransactionCountByNumber', 43 | tag=check_tag(tag), 44 | ) 45 | 46 | async def tx_by_hash(self, txhash: Union[int, str]) -> dict: 47 | """Returns the information about a transaction requested by transaction hash.""" 48 | return await self._get( 49 | action='eth_getTransactionByHash', 50 | txhash=check_hex(txhash), 51 | ) 52 | 53 | async def tx_by_number_and_index( 54 | self, index: Union[int, str], tag: Union[int, str] = 'latest' 55 | ) -> dict: 56 | """Returns information about a transaction by block number and transaction index position.""" 57 | return await self._get( 58 | action='eth_getTransactionByBlockNumberAndIndex', 59 | index=check_hex(index), 60 | tag=check_tag(tag), 61 | ) 62 | 63 | async def tx_count(self, address: str, tag: Union[int, str] = 'latest') -> str: 64 | """Returns the number of transactions sent from an address.""" 65 | return await self._get( 66 | action='eth_getTransactionCount', 67 | address=address, 68 | tag=check_tag(tag), 69 | ) 70 | 71 | async def send_raw_tx(self, raw_hex: str) -> dict: 72 | """Creates new message call transaction or a contract creation for signed transactions.""" 73 | return await self._post(module='proxy', action='eth_sendRawTransaction', hex=raw_hex) 74 | 75 | async def tx_receipt(self, txhash: str) -> dict: 76 | """Returns the receipt of a transaction by transaction hash.""" 77 | return await self._get( 78 | action='eth_getTransactionReceipt', 79 | txhash=check_hex(txhash), 80 | ) 81 | 82 | async def call(self, to: str, data: str, tag: Union[int, str] = 'latest') -> str: 83 | """Executes a new message call immediately without creating a transaction on the block chain.""" 84 | return await self._get( 85 | action='eth_call', 86 | to=check_hex(to), 87 | data=check_hex(data), 88 | tag=check_tag(tag), 89 | ) 90 | 91 | async def code(self, address: str, tag: Union[int, str] = 'latest') -> str: 92 | """Returns code at a given address.""" 93 | return await self._get( 94 | action='eth_getCode', 95 | address=address, 96 | tag=check_tag(tag), 97 | ) 98 | 99 | async def storage_at(self, address: str, position: str, tag: Union[int, str] = 'latest') -> str: 100 | """Returns the value from a storage position at a given address.""" 101 | return await self._get( 102 | action='eth_getStorageAt', 103 | address=address, 104 | position=position, 105 | tag=check_tag(tag), 106 | ) 107 | 108 | async def gas_price(self) -> str: 109 | """Returns the current price per gas in wei.""" 110 | return await self._get( 111 | action='eth_gasPrice', 112 | ) 113 | 114 | async def estimate_gas(self, to: str, value: str, gas_price: str, gas: str) -> str: 115 | """Makes a call or transaction, which won't be added to the blockchain and returns the used gas. 116 | 117 | Can be used for estimating the used gas. 118 | """ 119 | return await self._get( 120 | action='eth_estimateGas', 121 | to=check_hex(to), 122 | value=value, 123 | gasPrice=gas_price, 124 | gas=gas, 125 | ) 126 | -------------------------------------------------------------------------------- /aioetherscan/modules/stats.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from typing import Optional 3 | 4 | from aioetherscan.common import check_client_type, check_sync_mode, get_daily_stats_params 5 | from aioetherscan.modules.base import BaseModule 6 | 7 | 8 | class Stats(BaseModule): 9 | """Stats 10 | 11 | https://docs.etherscan.io/api-endpoints/stats-1 12 | """ 13 | 14 | @property 15 | def _module(self) -> str: 16 | return 'stats' 17 | 18 | async def eth_supply(self) -> str: 19 | """Get Total Supply of Ether""" 20 | return await self._get(action='ethsupply') 21 | 22 | async def eth2_supply(self) -> str: 23 | """Get Total Supply of Ether""" 24 | return await self._get(action='ethsupply2') 25 | 26 | async def eth_price(self) -> dict: 27 | """Get ETHER LastPrice Price""" 28 | return await self._get(action='ethprice') 29 | 30 | async def eth_nodes_size( 31 | self, 32 | start_date: date, 33 | end_date: date, 34 | client_type: str, 35 | sync_mode: str, 36 | sort: Optional[str] = None, 37 | ) -> dict: 38 | """Get Ethereum Nodes Size""" 39 | return await self._get( 40 | **get_daily_stats_params('chainsize', start_date, end_date, sort), 41 | clienttype=check_client_type(client_type), 42 | syncmode=check_sync_mode(sync_mode), 43 | ) 44 | 45 | async def total_nodes_count(self) -> dict: 46 | """Get Total Nodes Count""" 47 | return await self._get(action='nodecount') 48 | 49 | async def daily_network_tx_fee( 50 | self, start_date: date, end_date: date, sort: Optional[str] = None 51 | ) -> dict: 52 | """Get Daily Network Transaction Fee""" 53 | return await self._get(**get_daily_stats_params('dailytxnfee', start_date, end_date, sort)) 54 | 55 | async def daily_new_address_count( 56 | self, start_date: date, end_date: date, sort: Optional[str] = None 57 | ) -> dict: 58 | """Get Daily New Address Count""" 59 | return await self._get( 60 | **get_daily_stats_params('dailynewaddress', start_date, end_date, sort) 61 | ) 62 | 63 | async def daily_network_utilization( 64 | self, start_date: date, end_date: date, sort: Optional[str] = None 65 | ) -> dict: 66 | """Get Daily Network Utilization""" 67 | return await self._get( 68 | **get_daily_stats_params('dailynetutilization', start_date, end_date, sort) 69 | ) 70 | 71 | async def daily_average_network_hash_rate( 72 | self, start_date: date, end_date: date, sort: Optional[str] = None 73 | ) -> dict: 74 | """Get Daily Average Network Hash Rate""" 75 | return await self._get( 76 | **get_daily_stats_params('dailyavghashrate', start_date, end_date, sort) 77 | ) 78 | 79 | async def daily_transaction_count( 80 | self, start_date: date, end_date: date, sort: Optional[str] = None 81 | ) -> dict: 82 | """Get Daily Transaction Count""" 83 | return await self._get(**get_daily_stats_params('dailytx', start_date, end_date, sort)) 84 | 85 | async def daily_average_network_difficulty( 86 | self, start_date: date, end_date: date, sort: Optional[str] = None 87 | ) -> dict: 88 | """Get Daily Average Network Difficulty""" 89 | return await self._get( 90 | **get_daily_stats_params('dailyavgnetdifficulty', start_date, end_date, sort) 91 | ) 92 | 93 | async def ether_historical_daily_market_cap( 94 | self, start_date: date, end_date: date, sort: Optional[str] = None 95 | ) -> dict: 96 | """Get Ether Historical Daily Market Cap""" 97 | return await self._get( 98 | **get_daily_stats_params('ethdailymarketcap', start_date, end_date, sort) 99 | ) 100 | 101 | async def ether_historical_price( 102 | self, start_date: date, end_date: date, sort: Optional[str] = None 103 | ) -> dict: 104 | """Get Ether Historical Price""" 105 | return await self._get( 106 | **get_daily_stats_params('ethdailyprice', start_date, end_date, sort) 107 | ) 108 | -------------------------------------------------------------------------------- /aioetherscan/modules/token.py: -------------------------------------------------------------------------------- 1 | from aioetherscan.common import check_tag 2 | from aioetherscan.modules.base import BaseModule 3 | 4 | 5 | class Token(BaseModule): 6 | """Tokens 7 | 8 | https://docs.etherscan.io/api-endpoints/tokens 9 | """ 10 | 11 | @property 12 | def _module(self) -> str: 13 | return 'token' 14 | 15 | async def total_supply(self, contract_address: str) -> str: 16 | """Get ERC20-Token TotalSupply by ContractAddress""" 17 | return await self._get( 18 | module='stats', 19 | action='tokensupply', 20 | contractaddress=contract_address, 21 | ) 22 | 23 | async def account_balance( 24 | self, address: str, contract_address: str, tag: str = 'latest' 25 | ) -> str: 26 | """Get ERC20-Token Account Balance for TokenContractAddress""" 27 | return await self._get( 28 | module='account', 29 | action='tokenbalance', 30 | address=address, 31 | contractaddress=contract_address, 32 | tag=check_tag(tag), 33 | ) 34 | 35 | async def total_supply_by_blockno(self, contract_address: str, blockno: int) -> str: 36 | """Get Historical ERC20-Token TotalSupply by ContractAddress & BlockNo""" 37 | return await self._get( 38 | module='stats', 39 | action='tokensupplyhistory', 40 | contractaddress=contract_address, 41 | blockno=blockno, 42 | ) 43 | 44 | async def account_balance_by_blockno( 45 | self, address: str, contract_address: str, blockno: int 46 | ) -> str: 47 | """Get Historical ERC20-Token Account Balance for TokenContractAddress by BlockNo""" 48 | return await self._get( 49 | module='account', 50 | action='tokenbalancehistory', 51 | address=address, 52 | contractaddress=contract_address, 53 | blockno=blockno, 54 | ) 55 | 56 | async def token_holder_list( 57 | self, 58 | contract_address: str, 59 | page: int = None, 60 | offset: int = None, 61 | ) -> list[dict]: 62 | """Get Token Holder list by Contract Address""" 63 | return await self._get( 64 | action='tokenholderlist', contractaddress=contract_address, page=page, offset=offset 65 | ) 66 | 67 | async def token_info( 68 | self, 69 | contract_address: str = None, 70 | ) -> list[dict]: 71 | """Get Token Info by ContractAddress""" 72 | return await self._get( 73 | action='tokeninfo', 74 | contractaddress=contract_address, 75 | ) 76 | 77 | async def token_holding_erc20( 78 | self, 79 | address: str, 80 | page: int = None, 81 | offset: int = None, 82 | ) -> list[dict]: 83 | """Get Address ERC20 Token Holding""" 84 | return await self._get( 85 | module='account', 86 | action='addresstokenbalance', 87 | address=address, 88 | page=page, 89 | offset=offset, 90 | ) 91 | 92 | async def token_holding_erc721( 93 | self, 94 | address: str, 95 | page: int = None, 96 | offset: int = None, 97 | ) -> list[dict]: 98 | """Get Address ERC721 Token Holding""" 99 | return await self._get( 100 | module='account', 101 | action='addresstokennftbalance', 102 | address=address, 103 | page=page, 104 | offset=offset, 105 | ) 106 | 107 | async def token_inventory( 108 | self, 109 | address: str, 110 | contract_address: str, 111 | page: int = None, 112 | offset: int = None, 113 | ) -> list[dict]: 114 | """Get Address ERC721 Token Inventory By Contract Address""" 115 | return await self._get( 116 | module='account', 117 | action='addresstokennftinventory', 118 | address=address, 119 | contractaddress=contract_address, 120 | page=page, 121 | offset=offset, 122 | ) 123 | -------------------------------------------------------------------------------- /aioetherscan/modules/transaction.py: -------------------------------------------------------------------------------- 1 | from aioetherscan.modules.base import BaseModule 2 | 3 | 4 | class Transaction(BaseModule): 5 | """Transactions 6 | 7 | https://docs.etherscan.io/api-endpoints/stats 8 | """ 9 | 10 | @property 11 | def _module(self) -> str: 12 | return 'transaction' 13 | 14 | async def contract_execution_status(self, txhash: str) -> dict: 15 | """[BETA] Check Contract Execution Status (if there was an error during contract execution)""" 16 | return await self._get(action='getstatus', txhash=txhash) 17 | 18 | async def tx_receipt_status(self, txhash: str) -> dict: 19 | """[BETA] Check Transaction Receipt Status (Only applicable for Post Byzantium fork transactions)""" 20 | return await self._get(action='gettxreceiptstatus', txhash=txhash) 21 | -------------------------------------------------------------------------------- /aioetherscan/network.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from asyncio import AbstractEventLoop 4 | from typing import Union, AsyncContextManager, Optional 5 | 6 | import aiohttp 7 | from aiohttp import ClientTimeout 8 | from aiohttp.client import ClientSession 9 | from aiohttp.hdrs import METH_GET, METH_POST 10 | from aiohttp_retry import RetryOptionsBase, RetryClient 11 | from asyncio_throttle import Throttler 12 | 13 | from aioetherscan.exceptions import ( 14 | EtherscanClientContentTypeError, 15 | EtherscanClientError, 16 | EtherscanClientApiError, 17 | EtherscanClientProxyError, 18 | ) 19 | from aioetherscan.url_builder import UrlBuilder 20 | 21 | 22 | class Network: 23 | def __init__( 24 | self, 25 | url_builder: UrlBuilder, 26 | loop: Optional[AbstractEventLoop], 27 | timeout: Optional[ClientTimeout], 28 | proxy: Optional[str], 29 | throttler: Optional[AsyncContextManager], 30 | retry_options: Optional[RetryOptionsBase], 31 | ) -> None: 32 | self._url_builder = url_builder 33 | 34 | self._loop = loop or asyncio.get_running_loop() 35 | self._timeout = timeout 36 | 37 | self._proxy = proxy 38 | 39 | # Defaulting to free API key rate limit 40 | self._throttler = throttler or Throttler(rate_limit=5, period=1.0) 41 | 42 | self._retry_client = None 43 | self._retry_options = retry_options 44 | 45 | self._logger = logging.getLogger(__name__) 46 | 47 | async def close(self): 48 | if self._retry_client is not None: 49 | await self._retry_client.close() 50 | 51 | async def get(self, params: dict = None) -> Union[dict, list, str]: 52 | return await self._request(METH_GET, params=self._url_builder.filter_and_sign(params)) 53 | 54 | async def post(self, data: dict = None) -> Union[dict, list, str]: 55 | return await self._request(METH_POST, data=self._url_builder.filter_and_sign(data)) 56 | 57 | def _get_retry_client(self) -> RetryClient: 58 | return RetryClient(client_session=self._get_session(), retry_options=self._retry_options) 59 | 60 | def _get_session(self) -> ClientSession: 61 | if self._timeout is not None: 62 | return ClientSession(loop=self._loop, timeout=self._timeout) 63 | return ClientSession(loop=self._loop) 64 | 65 | async def _request( 66 | self, method: str, data: dict = None, params: dict = None 67 | ) -> Union[dict, list, str]: 68 | if self._retry_client is None: 69 | self._retry_client = self._get_retry_client() 70 | session_method = getattr(self._retry_client, method.lower()) 71 | async with self._throttler: 72 | async with session_method( 73 | self._url_builder.API_URL, params=params, data=data, proxy=self._proxy 74 | ) as response: 75 | self._logger.debug( 76 | '[%s] %r %r %s', method, str(response.url), data, response.status 77 | ) 78 | return await self._handle_response(response) 79 | 80 | async def _handle_response(self, response: aiohttp.ClientResponse) -> Union[dict, list, str]: 81 | try: 82 | response_json = await response.json() 83 | except aiohttp.ContentTypeError: 84 | raise EtherscanClientContentTypeError(response.status, await response.text()) 85 | except Exception as e: 86 | raise EtherscanClientError(e) 87 | else: 88 | self._logger.debug('Response: %r', response_json) 89 | self._raise_if_error(response_json) 90 | return response_json['result'] 91 | 92 | @staticmethod 93 | def _raise_if_error(response_json: dict): 94 | if 'status' in response_json and response_json['status'] != '1': 95 | message, result = response_json.get('message'), response_json.get('result') 96 | raise EtherscanClientApiError(message, result) 97 | 98 | if 'error' in response_json: 99 | err = response_json['error'] 100 | code, message = err.get('code'), err.get('message') 101 | raise EtherscanClientProxyError(code, message) 102 | -------------------------------------------------------------------------------- /aioetherscan/url_builder.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from urllib.parse import urlunsplit, urljoin 3 | 4 | 5 | class UrlBuilder: 6 | _API_KINDS = { 7 | 'eth': ('etherscan.io', 'ETH'), 8 | 'bsc': ('bscscan.com', 'BNB'), 9 | 'avax': ('snowtrace.io', 'AVAX'), 10 | 'polygon': ('polygonscan.com', 'MATIC'), 11 | 'optimism': ('etherscan.io', 'ETH'), 12 | 'base': ('basescan.org', 'ETH'), 13 | 'arbitrum': ('arbiscan.io', 'ETH'), 14 | 'fantom': ('ftmscan.com', 'FTM'), 15 | 'taiko': ('taikoscan.io', 'ETH'), 16 | 'snowscan': ('snowscan.xyz', 'AVAX'), 17 | } 18 | 19 | BASE_URL: str = None 20 | API_URL: str = None 21 | 22 | def __init__(self, api_key: str, api_kind: str, network: str) -> None: 23 | self._API_KEY = api_key 24 | 25 | self._set_api_kind(api_kind) 26 | self._network = network.lower().strip() 27 | 28 | self.API_URL = self._get_api_url() 29 | self.BASE_URL = self._get_base_url() 30 | 31 | def _set_api_kind(self, api_kind: str) -> None: 32 | api_kind = api_kind.lower().strip() 33 | if api_kind not in self._API_KINDS: 34 | raise ValueError( 35 | f'Incorrect api_kind {api_kind!r}, supported only: {", ".join(self._API_KINDS)}' 36 | ) 37 | else: 38 | self.api_kind = api_kind 39 | 40 | @property 41 | def _is_main(self) -> bool: 42 | return self._network == 'main' 43 | 44 | @property 45 | def _base_netloc(self) -> str: 46 | netloc, _ = self._API_KINDS[self.api_kind] 47 | return netloc 48 | 49 | @property 50 | def currency(self) -> str: 51 | _, currency = self._API_KINDS[self.api_kind] 52 | return currency 53 | 54 | def get_link(self, path: str) -> str: 55 | return urljoin(self.BASE_URL, path) 56 | 57 | def _build_url(self, prefix: Optional[str], path: str = '') -> str: 58 | netloc = self._base_netloc if prefix is None else f'{prefix}.{self._base_netloc}' 59 | return urlunsplit(('https', netloc, path, '', '')) 60 | 61 | def _get_api_url(self) -> str: 62 | prefix_exceptions = { 63 | ('optimism', True): 'api-optimistic', 64 | ('optimism', False): f'api-{self._network}-optimistic', 65 | } 66 | default_prefix = 'api' if self._is_main else f'api-{self._network}' 67 | prefix = prefix_exceptions.get((self.api_kind, self._is_main), default_prefix) 68 | 69 | return self._build_url(prefix, 'api') 70 | 71 | def _get_base_url(self) -> str: 72 | network_exceptions = {('polygon', 'testnet'): 'mumbai'} 73 | network = network_exceptions.get((self.api_kind, self._network), self._network) 74 | 75 | prefix_exceptions = { 76 | ('optimism', True): 'optimistic', 77 | ('optimism', False): f'{network}-optimism', 78 | } 79 | default_prefix = None if self._is_main else network 80 | prefix = prefix_exceptions.get((self.api_kind, self._is_main), default_prefix) 81 | 82 | return self._build_url(prefix) 83 | 84 | def filter_and_sign(self, params: dict): 85 | return self._sign(self._filter_params(params or {})) 86 | 87 | def _sign(self, params: dict) -> dict: 88 | if not params: 89 | params = {} 90 | params['apikey'] = self._API_KEY 91 | return params 92 | 93 | @staticmethod 94 | def _filter_params(params: dict) -> dict: 95 | return {k: v for k, v in params.items() if v is not None} 96 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "aioetherscan" 3 | version = "0.9.4" 4 | description = "Etherscan API async Python wrapper" 5 | authors = ["ape364 "] 6 | license = "MIT" 7 | homepage = "https://github.com/ape364/aioetherscan" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | aiohttp = "^3.4" 12 | asyncio_throttle = "^1.0.1" 13 | aiohttp-retry = "^2.8.3" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^8.2.2" 17 | pytest-asyncio = "^0.23.7" 18 | pre-commit = "^3.5.0" 19 | coveralls = "^3.3.1" 20 | 21 | [build-system] 22 | requires = ["poetry>=0.12"] 23 | build-backend = "poetry.masonry.api" 24 | 25 | [tool.pytest.ini_options] 26 | pythonpath = [ 27 | "aioetherscan" 28 | ] 29 | asyncio_mode = "auto" 30 | 31 | [tool.coverage.run] 32 | relative_files = true 33 | 34 | [tool.ruff] 35 | line-length = 100 36 | 37 | [tool.ruff.format] 38 | quote-style = "single" 39 | -------------------------------------------------------------------------------- /tests/extra/generators/test_blocks_parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import AsyncMock, Mock 3 | 4 | import pytest 5 | 6 | from aioetherscan.exceptions import EtherscanClientApiError 7 | from aioetherscan.modules.extra.generators.blocks_parser import BlocksParser 8 | from aioetherscan.modules.extra.generators.blocks_range import BlocksRange 9 | 10 | 11 | @pytest.fixture() 12 | async def api_method(): 13 | return AsyncMock() 14 | 15 | 16 | @pytest.fixture 17 | def request_params(): 18 | return {'param1': 'value1', 'param2': 'value2'} 19 | 20 | 21 | @pytest.fixture 22 | def start_block(): 23 | return 100 24 | 25 | 26 | @pytest.fixture 27 | def end_block(): 28 | return 200 29 | 30 | 31 | @pytest.fixture 32 | def blocks_limit(): 33 | return 10 34 | 35 | 36 | @pytest.fixture 37 | def blocks_limit_divider(): 38 | return 2 39 | 40 | 41 | @pytest.fixture 42 | def blocks_parser( 43 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 44 | ): 45 | return BlocksParser( 46 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 47 | ) 48 | 49 | 50 | def test_blocks_parser_init( 51 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 52 | ): 53 | parser = BlocksParser( 54 | api_method, request_params, start_block, end_block, blocks_limit, blocks_limit_divider 55 | ) 56 | 57 | assert parser._api_method == api_method 58 | assert parser._request_params == request_params 59 | 60 | assert isinstance(parser._blocks_range, BlocksRange) 61 | assert parser._blocks_range.start_block == start_block 62 | assert parser._blocks_range.end_block == end_block 63 | 64 | assert isinstance(parser._logger, logging.Logger) 65 | 66 | assert parser._total_txs == 0 67 | 68 | 69 | def test_make_request_params(blocks_parser): 70 | blocks_range = range(100, 200) 71 | params = blocks_parser._make_request_params(blocks_range) 72 | assert params == { 73 | 'start_block': 100, 74 | 'end_block': 200, 75 | 'page': 1, 76 | 'offset': 10_000, 77 | **blocks_parser._request_params, 78 | } 79 | 80 | 81 | async def test_fetch_blocks_range_success(blocks_parser, api_method): 82 | api_method.return_value = [{'blockNumber': 100}] 83 | blocks_range = range(100, 101) 84 | max_block, transfers = await blocks_parser._fetch_blocks_range(blocks_range) 85 | assert max_block == 100 86 | assert transfers == [{'blockNumber': 100}] 87 | 88 | 89 | async def test_fetch_blocks_range_empty_response(blocks_parser, api_method): 90 | max_block_default = 100 91 | api_method.return_value = [{'blockNumber': max_block_default}] * BlocksParser._OFFSET 92 | blocks_range = range(100, 101) 93 | max_block, transfers = await blocks_parser._fetch_blocks_range(blocks_range) 94 | 95 | assert max_block == max_block_default - 1 96 | assert {'blockNumber': max_block_default} not in transfers 97 | 98 | 99 | async def test_fetch_blocks_range_api_error(blocks_parser, api_method): 100 | api_method.side_effect = EtherscanClientApiError('Test exception', 'result') 101 | blocks_range = range(100, 101) 102 | with pytest.raises(EtherscanClientApiError): 103 | await blocks_parser._fetch_blocks_range(blocks_range) 104 | 105 | 106 | async def test_fetch_blocks_range_no_txs_with_message(blocks_parser, api_method): 107 | api_method.side_effect = EtherscanClientApiError('No transactions found', 'result') 108 | 109 | blocks_range = range(100, 101) 110 | max_block, transfers = await blocks_parser._fetch_blocks_range(blocks_range) 111 | 112 | assert max_block == blocks_range.stop 113 | assert transfers == [] 114 | 115 | 116 | async def test_fetch_blocks_range_no_txs_no_message(blocks_parser, api_method): 117 | api_method.return_value = [] 118 | blocks_range = range(100, 101) 119 | max_block, transfers = await blocks_parser._fetch_blocks_range(blocks_range) 120 | 121 | assert max_block == blocks_range.stop 122 | assert transfers == [] 123 | 124 | 125 | async def test_txs_generator(blocks_parser, api_method): 126 | api_method.side_effect = [ 127 | EtherscanClientApiError('Test exception', 'result'), 128 | [{'blockNumber': 200, 'transfers': [{'value': 100}]}], 129 | ] 130 | blocks_parser._blocks_range.limit.reduce = Mock() 131 | blocks_parser._blocks_range.limit.restore = Mock() 132 | 133 | transfers = [] 134 | async for transfer in blocks_parser.txs_generator(): 135 | transfers.append(transfer) 136 | 137 | blocks_parser._blocks_range.limit.reduce.assert_called_once() 138 | blocks_parser._blocks_range.limit.restore.assert_called_once() 139 | 140 | assert transfers == [{'blockNumber': 200, 'transfers': [{'value': 100}]}] 141 | -------------------------------------------------------------------------------- /tests/extra/generators/test_blocks_range.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | import pytest 4 | 5 | from aioetherscan.modules.extra.generators.blocks_range import Limit, BlocksRange 6 | 7 | INITIAL_LIMIT = 2**4 8 | BLOCKS_RANGE_DIVIDER = 2 9 | 10 | START_BLOCK = 1000 11 | END_BLOCK = 5000 12 | 13 | 14 | @pytest.fixture 15 | def limit() -> Limit: 16 | yield Limit(INITIAL_LIMIT, BLOCKS_RANGE_DIVIDER) 17 | 18 | 19 | @pytest.fixture 20 | def br(limit: Limit) -> BlocksRange: 21 | yield BlocksRange( 22 | START_BLOCK, 23 | END_BLOCK, 24 | INITIAL_LIMIT, 25 | BLOCKS_RANGE_DIVIDER, 26 | ) 27 | 28 | 29 | # ############################### limit ################################ 30 | 31 | 32 | def test_limit_init(limit: Limit): 33 | assert limit._limit == INITIAL_LIMIT 34 | assert limit._initial_limit == INITIAL_LIMIT 35 | assert limit._blocks_range_divider == BLOCKS_RANGE_DIVIDER 36 | 37 | assert isinstance(limit._logger, Logger) 38 | 39 | 40 | def test_limit_reduce(limit: Limit): 41 | limit.reduce() 42 | assert limit.get() == INITIAL_LIMIT // BLOCKS_RANGE_DIVIDER 43 | assert limit._initial_limit == INITIAL_LIMIT 44 | 45 | 46 | def test_limit_reduce_raise(limit: Limit): 47 | while limit.get() != 1: 48 | limit.reduce() 49 | 50 | with pytest.raises(Exception) as e: 51 | limit.reduce() 52 | assert e.value.args[0] == 'Limit is 0' 53 | 54 | 55 | def test_limit_restore(limit: Limit): 56 | limit.reduce() 57 | limit.restore() 58 | 59 | assert limit.get() == INITIAL_LIMIT 60 | 61 | 62 | def test_limit_get(limit: Limit): 63 | assert limit.get() == INITIAL_LIMIT 64 | 65 | 66 | # ############################### blocks range ################################ 67 | 68 | 69 | def test_br_init(br: BlocksRange): 70 | assert br.start_block == START_BLOCK 71 | assert br.end_block == END_BLOCK 72 | assert br.current_block == START_BLOCK 73 | 74 | assert isinstance(br.limit, Limit) 75 | assert isinstance(br._logger, Logger) 76 | -------------------------------------------------------------------------------- /tests/extra/generators/test_generator_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch, MagicMock, AsyncMock 2 | 3 | import pytest 4 | 5 | from aioetherscan.exceptions import EtherscanClientApiError 6 | from aioetherscan.modules.extra.generators.blocks_parser import BlocksParser 7 | from aioetherscan.modules.extra.generators.generator_utils import GeneratorUtils 8 | 9 | 10 | @pytest.fixture 11 | def generator_utils(): 12 | return GeneratorUtils(Mock()) 13 | 14 | 15 | async def parse_mock(*args, **kwargs): 16 | yield None 17 | 18 | 19 | def transfers_for_test() -> list[dict[str, str]]: 20 | return [{'result': 'transfer1'}, {'result': 'transfer2'}] 21 | 22 | 23 | async def transfers_mock(*args, **kwargs): 24 | for t in transfers_for_test(): 25 | yield t 26 | 27 | 28 | def test_init_with_default_values(): 29 | client = Mock() 30 | utils = GeneratorUtils(client) 31 | 32 | assert utils._client == client 33 | 34 | 35 | def test_get_blocks_parser(generator_utils): 36 | blocks_parser = generator_utils._get_blocks_parser( 37 | api_method=None, 38 | request_params={'param': 'value'}, 39 | start_block=100, 40 | end_block=200, 41 | blocks_limit=1000, 42 | blocks_limit_divider=2, 43 | ) 44 | assert isinstance(blocks_parser, BlocksParser) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_token_transfers(generator_utils): 49 | params_return_value = {'param': 'value'} 50 | generator_utils._get_parser_params = Mock(return_value=params_return_value) 51 | 52 | with patch( 53 | 'aioetherscan.modules.extra.generators.generator_utils.GeneratorUtils._parse_by_blocks', 54 | new=MagicMock(side_effect=parse_mock), 55 | ) as mock: 56 | async for _ in generator_utils.token_transfers( 57 | contract_address='c1', address='a1', start_block=0, end_block=20 58 | ): 59 | break 60 | 61 | generator_utils._get_parser_params.assert_called_once_with( 62 | generator_utils._client.account.token_transfers, 63 | { 64 | 'self': generator_utils, 65 | 'contract_address': 'c1', 66 | 'address': 'a1', 67 | 'start_block': 0, 68 | 'end_block': 20, 69 | 'blocks_limit': 2048, 70 | 'blocks_limit_divider': 2, 71 | }, 72 | ) 73 | 74 | mock.assert_called_once_with(param='value') 75 | 76 | 77 | @pytest.mark.asyncio 78 | async def test_normal_txs(generator_utils): 79 | params_return_value = {'param': 'value'} 80 | generator_utils._get_parser_params = Mock(return_value=params_return_value) 81 | 82 | with patch( 83 | 'aioetherscan.modules.extra.generators.generator_utils.GeneratorUtils._parse_by_blocks', 84 | new=MagicMock(side_effect=parse_mock), 85 | ) as mock: 86 | async for _ in generator_utils.normal_txs(address='a1', start_block=0, end_block=20): 87 | break 88 | 89 | generator_utils._get_parser_params.assert_called_once_with( 90 | generator_utils._client.account.normal_txs, 91 | { 92 | 'self': generator_utils, 93 | 'address': 'a1', 94 | 'start_block': 0, 95 | 'end_block': 20, 96 | 'blocks_limit': 2048, 97 | 'blocks_limit_divider': 2, 98 | }, 99 | ) 100 | 101 | mock.assert_called_once_with(param='value') 102 | 103 | 104 | @pytest.mark.asyncio 105 | async def test_internal_txs(generator_utils): 106 | params_return_value = {'param': 'value'} 107 | generator_utils._get_parser_params = Mock(return_value=params_return_value) 108 | 109 | with patch( 110 | 'aioetherscan.modules.extra.generators.generator_utils.GeneratorUtils._parse_by_blocks', 111 | new=MagicMock(side_effect=parse_mock), 112 | ) as mock: 113 | async for _ in generator_utils.internal_txs( 114 | address='a1', start_block=0, end_block=20, txhash='0x123' 115 | ): 116 | break 117 | 118 | generator_utils._get_parser_params.assert_called_once_with( 119 | generator_utils._client.account.internal_txs, 120 | { 121 | 'self': generator_utils, 122 | 'address': 'a1', 123 | 'txhash': '0x123', 124 | 'start_block': 0, 125 | 'end_block': 20, 126 | 'blocks_limit': 2048, 127 | 'blocks_limit_divider': 2, 128 | }, 129 | ) 130 | 131 | mock.assert_called_once_with(param='value') 132 | 133 | 134 | @pytest.mark.asyncio 135 | async def test_mined_blocks(generator_utils): 136 | params_return_value = {'param': 'value'} 137 | generator_utils._get_parser_params = Mock(return_value=params_return_value) 138 | 139 | with patch( 140 | 'aioetherscan.modules.extra.generators.generator_utils.GeneratorUtils._parse_by_pages', 141 | new=MagicMock(side_effect=parse_mock), 142 | ) as mock: 143 | async for _ in generator_utils.mined_blocks(address='a1', blocktype='blocks'): 144 | break 145 | 146 | generator_utils._get_parser_params.assert_called_once_with( 147 | generator_utils._client.account.mined_blocks, 148 | {'self': generator_utils, 'address': 'a1', 'blocktype': 'blocks', 'offset': 10000}, 149 | ) 150 | 151 | mock.assert_called_once_with(param='value') 152 | 153 | 154 | @pytest.mark.asyncio 155 | async def test_parse_by_blocks(generator_utils): 156 | blocks_parser_mock = Mock() 157 | blocks_parser_mock.return_value.txs_generator = MagicMock(side_effect=transfers_mock) 158 | generator_utils._get_blocks_parser = blocks_parser_mock 159 | 160 | transfers = [] 161 | async for transfer in generator_utils._parse_by_blocks( 162 | api_method=None, 163 | request_params={'param': 'value'}, 164 | start_block=100, 165 | end_block=200, 166 | blocks_limit=1000, 167 | blocks_limit_divider=2, 168 | ): 169 | transfers.append(transfer) 170 | assert transfers == transfers_for_test() 171 | 172 | blocks_parser_mock.assert_called_once_with(None, {'param': 'value'}, 100, 200, 1000, 2) 173 | 174 | 175 | async def test_parse_by_blocks_end_block_is_none(generator_utils): 176 | blocks_parser_mock = Mock() 177 | blocks_parser_mock.return_value.txs_generator = MagicMock(side_effect=transfers_mock) 178 | generator_utils._get_blocks_parser = blocks_parser_mock 179 | 180 | transfers = [] 181 | async for transfer in generator_utils._parse_by_blocks( 182 | api_method=None, 183 | request_params={'param': 'value'}, 184 | start_block=100, 185 | end_block=200, 186 | blocks_limit=1000, 187 | blocks_limit_divider=2, 188 | ): 189 | transfers.append(transfer) 190 | assert transfers == transfers_for_test() 191 | 192 | blocks_parser_mock.assert_called_once_with(None, {'param': 'value'}, 100, 200, 1000, 2) 193 | 194 | 195 | async def test_parse_by_pages_ok(generator_utils): 196 | api_method = AsyncMock(return_value=['row1', 'row2']) 197 | params = {'param': 'value'} 198 | 199 | result = [] 200 | async for row in generator_utils._parse_by_pages(api_method, params): 201 | result.append(row) 202 | 203 | if len(row) > 1: 204 | api_method.side_effect = EtherscanClientApiError('No transactions found', 'result') 205 | 206 | assert result == api_method.return_value 207 | 208 | 209 | async def test_parse_by_pages_error(generator_utils): 210 | api_method = AsyncMock(side_effect=EtherscanClientApiError('test error', 'result')) 211 | params = {'param': 'value'} 212 | 213 | with pytest.raises(EtherscanClientApiError) as e: 214 | async for _ in generator_utils._parse_by_pages(api_method, params): 215 | break 216 | 217 | assert e.value.args[0] == 'test error' 218 | 219 | 220 | @pytest.mark.parametrize( 221 | 'params, excluded, expected', 222 | [ 223 | ({'a': 1, 'b': 2, 'c': 3}, ('a', 'b'), {'c': 3}), 224 | ({'a': 1, 'b': 2, 'c': 3}, ('b', 'c'), {'a': 1}), 225 | ({'a': 1, 'b': 2, 'c': 3}, ('a', 'c'), {'b': 2}), 226 | ({'a': 1, 'b': 2, 'c': 3}, ('a', 'b', 'c'), {}), 227 | ({'a': 1, 'b': 2, 'c': 3}, ('d', 'e'), {'a': 1, 'b': 2, 'c': 3}), 228 | ], 229 | ) 230 | def test_without_keys(generator_utils, params, excluded, expected): 231 | assert generator_utils._without_keys(params, excluded) == expected 232 | 233 | 234 | def test_get_request_params(generator_utils): 235 | api_method = object() 236 | 237 | generator_utils._without_keys = Mock(return_value={'param1': 'value1', 'param2': 'value2'}) 238 | 239 | with patch('inspect.getfullargspec') as getfullargspec_mock: 240 | getfullargspec_mock.return_value.args = ['self', 'param1', 'param3'] 241 | 242 | result = generator_utils._get_request_params( 243 | api_method, {'param1': 'value1', 'param2': 'value2'} 244 | ) 245 | 246 | getfullargspec_mock.assert_called_once_with(api_method) 247 | generator_utils._without_keys.assert_called_once_with( 248 | {'param1': 'value1'}, ('self', 'start_block', 'end_block') 249 | ) 250 | assert result == generator_utils._without_keys.return_value 251 | 252 | 253 | def test_get_parser_params(generator_utils): 254 | api_method = object() 255 | params = {'param1': 'value1', 'param2': 'value2', 'param3': 'value3'} 256 | 257 | generator_utils._get_request_params = Mock(return_value={'param1': 'value1'}) 258 | result = generator_utils._get_parser_params(api_method, params) 259 | 260 | generator_utils._get_request_params.assert_called_once_with(api_method, params) 261 | 262 | assert result == dict( 263 | api_method=api_method, 264 | request_params=generator_utils._get_request_params.return_value, 265 | **{'param2': 'value2', 'param3': 'value3'}, 266 | ) 267 | -------------------------------------------------------------------------------- /tests/extra/generators/test_helpers.py: -------------------------------------------------------------------------------- 1 | from aioetherscan.modules.extra.generators.helpers import ( 2 | tx_block_number, 3 | drop_block, 4 | get_max_block_number, 5 | ) 6 | 7 | 8 | def test_tx_block_number(): 9 | block_number = 123 10 | tx = dict(blockNumber=block_number) 11 | result = tx_block_number(tx) 12 | assert block_number == result 13 | assert isinstance(result, int) 14 | 15 | 16 | def test_drop_block(): 17 | block_to_drop = 456 18 | transfers = [ 19 | dict(blockNumber=123), 20 | dict(blockNumber=456), 21 | dict(blockNumber=456), 22 | dict(blockNumber=789), 23 | ] 24 | 25 | result = list(drop_block(transfers, block_to_drop)) 26 | 27 | for tx in transfers: 28 | if tx['blockNumber'] == block_to_drop: 29 | assert tx not in result 30 | else: 31 | assert tx in result 32 | 33 | 34 | def test_get_max_block_number(): 35 | max_block_number = 2**16 36 | transfers = [ 37 | dict(blockNumber=123), 38 | dict(blockNumber=max_block_number), 39 | dict(blockNumber=456), 40 | ] 41 | 42 | assert get_max_block_number(transfers) == max_block_number 43 | -------------------------------------------------------------------------------- /tests/extra/test_contract_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | from aioetherscan.exceptions import EtherscanClientApiError 8 | 9 | 10 | @pytest_asyncio.fixture 11 | async def contract_utils(): 12 | c = Client('TestApiKey') 13 | yield c.extra.contract 14 | await c.close() 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_is_contract_ok(contract_utils): 19 | return_value = {'some': 'data'} 20 | with patch( 21 | 'aioetherscan.modules.contract.Contract.contract_abi', 22 | new=AsyncMock(return_value=return_value), 23 | ) as mock: 24 | result = await contract_utils.is_contract(address='addr') 25 | mock.assert_called_once_with(address='addr') 26 | assert result is True 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_is_contract_negative(contract_utils): 31 | exc = EtherscanClientApiError(message='NOTOK', result='contract source code not verified') 32 | with patch( 33 | 'aioetherscan.modules.contract.Contract.contract_abi', new=AsyncMock(side_effect=exc) 34 | ): 35 | result = await contract_utils.is_contract(address='addr') 36 | assert result is False 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_is_contract_exception(contract_utils): 41 | exc = EtherscanClientApiError(message='some_msg', result='some_result') 42 | with patch( 43 | 'aioetherscan.modules.contract.Contract.contract_abi', new=AsyncMock(side_effect=exc) 44 | ): 45 | with pytest.raises(EtherscanClientApiError): 46 | await contract_utils.is_contract(address='addr') 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_get_contract_creator_internal_ok(contract_utils): 51 | creator = '0x123' 52 | internal_txs_mock = AsyncMock( 53 | return_value=[ 54 | {'from': creator}, 55 | ] 56 | ) 57 | with patch('aioetherscan.modules.account.Account.internal_txs', new=internal_txs_mock) as mock: 58 | result = await contract_utils.get_contract_creator(contract_address='addr') 59 | mock.assert_called_once_with(address='addr', start_block=1, page=1, offset=1) 60 | assert result == creator 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_get_contract_creator_internal_raise(contract_utils): 65 | exc = EtherscanClientApiError(message='No transactions found', result='') 66 | internal_txs_mock = AsyncMock(side_effect=exc) 67 | with patch('aioetherscan.modules.account.Account.internal_txs', new=internal_txs_mock): 68 | with pytest.raises(EtherscanClientApiError): 69 | await contract_utils.get_contract_creator(contract_address='addr') 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_get_contract_creator_internal_raise_none(contract_utils): 74 | exc = EtherscanClientApiError(message='', result='') 75 | internal_txs_mock = AsyncMock(side_effect=exc) 76 | 77 | creator = '0x123' 78 | normal_txs_mock = AsyncMock( 79 | return_value=[ 80 | {'from': creator}, 81 | ] 82 | ) 83 | with patch( 84 | 'aioetherscan.modules.account.Account.internal_txs', new=internal_txs_mock 85 | ) as internal_mock: 86 | with patch( 87 | 'aioetherscan.modules.account.Account.normal_txs', new=normal_txs_mock 88 | ) as normal_mock: 89 | result = await contract_utils.get_contract_creator(contract_address='addr') 90 | internal_mock.assert_called_once_with(address='addr', start_block=1, page=1, offset=1) 91 | normal_mock.assert_called_once_with(address='addr', start_block=1, page=1, offset=1) 92 | 93 | assert result == creator 94 | 95 | 96 | @pytest.mark.asyncio 97 | async def test_get_contract_creator_internal_raise_normal(contract_utils): 98 | exc = EtherscanClientApiError(message='', result='') 99 | internal_txs_mock = AsyncMock(side_effect=exc) 100 | 101 | normal_exc = EtherscanClientApiError(message='No transactions found', result='') 102 | normal_txs_mock = AsyncMock(side_effect=normal_exc) 103 | with patch('aioetherscan.modules.account.Account.internal_txs', new=internal_txs_mock): 104 | with patch('aioetherscan.modules.account.Account.normal_txs', new=normal_txs_mock): 105 | with pytest.raises(EtherscanClientApiError): 106 | await contract_utils.get_contract_creator(contract_address='addr') 107 | 108 | 109 | @pytest.mark.asyncio 110 | async def test_get_contract_creator_internal_none(contract_utils): 111 | empty_result_mock_int = AsyncMock(return_value=[]) 112 | empty_result_mock_norm = AsyncMock(return_value=[]) 113 | 114 | with patch( 115 | 'aioetherscan.modules.account.Account.internal_txs', new=empty_result_mock_int 116 | ) as mock: 117 | with patch('aioetherscan.modules.account.Account.normal_txs', new=empty_result_mock_norm): 118 | result = await contract_utils.get_contract_creator(contract_address='addr') 119 | mock.assert_called_once_with(address='addr', start_block=1, page=1, offset=1) 120 | assert result is None 121 | -------------------------------------------------------------------------------- /tests/extra/test_link_utils.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | 5 | from aioetherscan.modules.extra.link import LinkUtils 6 | 7 | 8 | @pytest.fixture 9 | def ub(): 10 | url_builder_mock = MagicMock() 11 | yield url_builder_mock 12 | 13 | 14 | @pytest.fixture 15 | def lh(ub): 16 | lh = LinkUtils(ub) 17 | yield lh 18 | 19 | 20 | def test_get_address_link(lh, ub): 21 | address = 'some_address' 22 | lh.get_address_link(address) 23 | ub.get_link.assert_called_once_with(f'address/{address}') 24 | 25 | 26 | def test_get_tx_link(lh, ub): 27 | tx = '0x123' 28 | lh.get_tx_link(tx) 29 | ub.get_link.assert_called_once_with(f'tx/{tx}') 30 | 31 | 32 | def test_get_block_link(lh, ub): 33 | block = 123 34 | lh.get_block_link(block) 35 | ub.get_link.assert_called_once_with(f'block/{block}') 36 | 37 | 38 | def test_get_block_txs_link(lh, ub): 39 | block = 123 40 | lh.get_block_txs_link(block) 41 | ub.get_link.assert_called_once_with(f'txs?block={block}') 42 | -------------------------------------------------------------------------------- /tests/test_account.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def account(): 11 | c = Client('TestApiKey') 12 | yield c.account 13 | await c.close() 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_balance(account): 18 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 19 | await account.balance('addr') 20 | mock.assert_called_once_with( 21 | params=dict(module='account', action='balance', address='addr', tag='latest') 22 | ) 23 | 24 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 25 | await account.balance('addr', 123) 26 | mock.assert_called_once_with( 27 | params=dict(module='account', action='balance', address='addr', tag='0x7b') 28 | ) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_balances(account): 33 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 34 | await account.balances(['a1', 'a2']) 35 | mock.assert_called_once_with( 36 | params=dict(module='account', action='balancemulti', address='a1,a2', tag='latest') 37 | ) 38 | 39 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 40 | await account.balances(['a1', 'a2'], 123) 41 | mock.assert_called_once_with( 42 | params=dict(module='account', action='balancemulti', address='a1,a2', tag='0x7b') 43 | ) 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_normal_txs(account): 48 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 49 | await account.normal_txs('addr') 50 | mock.assert_called_once_with( 51 | params=dict( 52 | module='account', 53 | action='txlist', 54 | address='addr', 55 | startblock=None, 56 | endblock=None, 57 | sort=None, 58 | page=None, 59 | offset=None, 60 | ) 61 | ) 62 | 63 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 64 | await account.normal_txs( 65 | address='addr', start_block=1, end_block=2, sort='asc', page=3, offset=4 66 | ) 67 | mock.assert_called_once_with( 68 | params=dict( 69 | module='account', 70 | action='txlist', 71 | address='addr', 72 | startblock=1, 73 | endblock=2, 74 | sort='asc', 75 | page=3, 76 | offset=4, 77 | ) 78 | ) 79 | with pytest.raises(ValueError): 80 | await account.normal_txs( 81 | address='addr', 82 | sort='wrong', 83 | ) 84 | 85 | 86 | @pytest.mark.asyncio 87 | async def test_internal_txs(account): 88 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 89 | await account.internal_txs('addr') 90 | mock.assert_called_once_with( 91 | params=dict( 92 | module='account', 93 | action='txlistinternal', 94 | address='addr', 95 | startblock=None, 96 | endblock=None, 97 | sort=None, 98 | page=None, 99 | offset=None, 100 | txhash=None, 101 | ) 102 | ) 103 | 104 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 105 | await account.internal_txs( 106 | address='addr', start_block=1, end_block=2, sort='asc', page=3, offset=4, txhash='0x123' 107 | ) 108 | mock.assert_called_once_with( 109 | params=dict( 110 | module='account', 111 | action='txlistinternal', 112 | address='addr', 113 | startblock=1, 114 | endblock=2, 115 | sort='asc', 116 | page=3, 117 | offset=4, 118 | txhash='0x123', 119 | ) 120 | ) 121 | with pytest.raises(ValueError): 122 | await account.internal_txs( 123 | address='addr', 124 | sort='wrong', 125 | ) 126 | 127 | 128 | @pytest.mark.asyncio 129 | @pytest.mark.parametrize( 130 | 'token_standard,expected_action', 131 | [ 132 | ('erc20', 'tokentx'), 133 | ('erc721', 'tokennfttx'), 134 | ('erc1155', 'token1155tx'), 135 | ], 136 | ) 137 | async def test_token_transfers(account, token_standard, expected_action): 138 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 139 | await account.token_transfers('addr') 140 | mock.assert_called_once_with( 141 | params=dict( 142 | module='account', 143 | action='tokentx', 144 | address='addr', 145 | startblock=None, 146 | endblock=None, 147 | sort=None, 148 | page=None, 149 | offset=None, 150 | contractaddress=None, 151 | ) 152 | ) 153 | 154 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 155 | await account.token_transfers( 156 | address='addr', 157 | start_block=1, 158 | end_block=2, 159 | sort='asc', 160 | page=3, 161 | offset=4, 162 | contract_address='0x123', 163 | ) 164 | mock.assert_called_once_with( 165 | params=dict( 166 | module='account', 167 | action='tokentx', 168 | address='addr', 169 | startblock=1, 170 | endblock=2, 171 | sort='asc', 172 | page=3, 173 | offset=4, 174 | contractaddress='0x123', 175 | ) 176 | ) 177 | 178 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 179 | await account.token_transfers( 180 | address='addr', 181 | start_block=1, 182 | end_block=2, 183 | sort='asc', 184 | page=3, 185 | offset=4, 186 | contract_address='0x123', 187 | token_standard=token_standard, 188 | ) 189 | mock.assert_called_once_with( 190 | params=dict( 191 | module='account', 192 | action=expected_action, 193 | address='addr', 194 | startblock=1, 195 | endblock=2, 196 | sort='asc', 197 | page=3, 198 | offset=4, 199 | contractaddress='0x123', 200 | ) 201 | ) 202 | 203 | with pytest.raises(ValueError): 204 | await account.token_transfers( 205 | address='addr', 206 | sort='wrong', 207 | ) 208 | with pytest.raises(ValueError): 209 | await account.token_transfers(start_block=123) 210 | 211 | 212 | @pytest.mark.asyncio 213 | async def test_mined_blocks(account): 214 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 215 | await account.mined_blocks('addr') 216 | mock.assert_called_once_with( 217 | params=dict( 218 | module='account', 219 | action='getminedblocks', 220 | address='addr', 221 | blocktype='blocks', 222 | page=None, 223 | offset=None, 224 | ) 225 | ) 226 | 227 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 228 | await account.mined_blocks(address='addr', blocktype='uncles', page=1, offset=2) 229 | mock.assert_called_once_with( 230 | params=dict( 231 | module='account', 232 | action='getminedblocks', 233 | address='addr', 234 | blocktype='uncles', 235 | page=1, 236 | offset=2, 237 | ) 238 | ) 239 | 240 | with pytest.raises(ValueError): 241 | await account.mined_blocks( 242 | address='addr', 243 | blocktype='wrong', 244 | ) 245 | 246 | 247 | @pytest.mark.asyncio 248 | async def test_beacon_chain_withdrawals(account): 249 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 250 | await account.beacon_chain_withdrawals('addr') 251 | mock.assert_called_once_with( 252 | params=dict( 253 | module='account', 254 | action='txsBeaconWithdrawal', 255 | address='addr', 256 | startblock=None, 257 | endblock=None, 258 | sort=None, 259 | page=None, 260 | offset=None, 261 | ) 262 | ) 263 | 264 | with pytest.raises(ValueError): 265 | await account.beacon_chain_withdrawals( 266 | address='addr', 267 | sort='wrong', 268 | ) 269 | 270 | 271 | @pytest.mark.asyncio 272 | async def test_account_balance_by_blockno(account): 273 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 274 | await account.account_balance_by_blockno('a1', 123) 275 | mock.assert_called_once_with( 276 | params=dict(module='account', action='balancehistory', address='a1', blockno=123) 277 | ) 278 | -------------------------------------------------------------------------------- /tests/test_block.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from unittest.mock import patch, AsyncMock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from aioetherscan import Client 8 | 9 | 10 | @pytest_asyncio.fixture 11 | async def block(): 12 | c = Client('TestApiKey') 13 | yield c.block 14 | await c.close() 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_block_reward(block): 19 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 20 | await block.block_reward(123) 21 | mock.assert_called_once_with( 22 | params=dict(module='block', action='getblockreward', blockno=123) 23 | ) 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_est_block_countdown_time(block): 28 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 29 | await block.est_block_countdown_time(123) 30 | mock.assert_called_once_with( 31 | params=dict(module='block', action='getblockcountdown', blockno=123) 32 | ) 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_block_number_by_ts(block): 37 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 38 | await block.block_number_by_ts(123, 'before') 39 | mock.assert_called_once_with( 40 | params=dict(module='block', action='getblocknobytime', timestamp=123, closest='before') 41 | ) 42 | 43 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 44 | await block.block_number_by_ts(321, 'after') 45 | mock.assert_called_once_with( 46 | params=dict(module='block', action='getblocknobytime', timestamp=321, closest='after') 47 | ) 48 | 49 | with pytest.raises(ValueError): 50 | await block.block_number_by_ts( 51 | ts=111, 52 | closest='wrong', 53 | ) 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_daily_average_block_size(block): 58 | start_date = date(2023, 11, 12) 59 | end_date = date(2023, 11, 13) 60 | 61 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 62 | await block.daily_average_block_size(start_date, end_date, 'asc') 63 | mock.assert_called_once_with( 64 | params=dict( 65 | module='stats', 66 | action='dailyavgblocksize', 67 | startdate='2023-11-12', 68 | enddate='2023-11-13', 69 | sort='asc', 70 | ) 71 | ) 72 | 73 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 74 | await block.daily_average_block_size(start_date, end_date) 75 | mock.assert_called_once_with( 76 | params=dict( 77 | module='stats', 78 | action='dailyavgblocksize', 79 | startdate='2023-11-12', 80 | enddate='2023-11-13', 81 | sort=None, 82 | ) 83 | ) 84 | 85 | with pytest.raises(ValueError): 86 | await block.daily_average_block_size(start_date, end_date, 'wrong') 87 | 88 | 89 | @pytest.mark.asyncio 90 | async def test_daily_block_count(block): 91 | start_date = date(2023, 11, 12) 92 | end_date = date(2023, 11, 13) 93 | 94 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 95 | await block.daily_block_count(start_date, end_date, 'asc') 96 | mock.assert_called_once_with( 97 | params=dict( 98 | module='stats', 99 | action='dailyblkcount', 100 | startdate='2023-11-12', 101 | enddate='2023-11-13', 102 | sort='asc', 103 | ) 104 | ) 105 | 106 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 107 | await block.daily_block_count(start_date, end_date) 108 | mock.assert_called_once_with( 109 | params=dict( 110 | module='stats', 111 | action='dailyblkcount', 112 | startdate='2023-11-12', 113 | enddate='2023-11-13', 114 | sort=None, 115 | ) 116 | ) 117 | 118 | with pytest.raises(ValueError): 119 | await block.daily_block_count(start_date, end_date, 'wrong') 120 | 121 | 122 | @pytest.mark.asyncio 123 | async def test_daily_block_rewards(block): 124 | start_date = date(2023, 11, 12) 125 | end_date = date(2023, 11, 13) 126 | 127 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 128 | await block.daily_block_rewards(start_date, end_date, 'asc') 129 | mock.assert_called_once_with( 130 | params=dict( 131 | module='stats', 132 | action='dailyblockrewards', 133 | startdate='2023-11-12', 134 | enddate='2023-11-13', 135 | sort='asc', 136 | ) 137 | ) 138 | 139 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 140 | await block.daily_block_rewards(start_date, end_date) 141 | mock.assert_called_once_with( 142 | params=dict( 143 | module='stats', 144 | action='dailyblockrewards', 145 | startdate='2023-11-12', 146 | enddate='2023-11-13', 147 | sort=None, 148 | ) 149 | ) 150 | 151 | with pytest.raises(ValueError): 152 | await block.daily_block_rewards(start_date, end_date, 'wrong') 153 | 154 | 155 | @pytest.mark.asyncio 156 | async def test_daily_average_time_for_a_block(block): 157 | start_date = date(2023, 11, 12) 158 | end_date = date(2023, 11, 13) 159 | 160 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 161 | await block.daily_average_time_for_a_block(start_date, end_date, 'asc') 162 | mock.assert_called_once_with( 163 | params=dict( 164 | module='stats', 165 | action='dailyavgblocktime', 166 | startdate='2023-11-12', 167 | enddate='2023-11-13', 168 | sort='asc', 169 | ) 170 | ) 171 | 172 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 173 | await block.daily_average_time_for_a_block(start_date, end_date) 174 | mock.assert_called_once_with( 175 | params=dict( 176 | module='stats', 177 | action='dailyavgblocktime', 178 | startdate='2023-11-12', 179 | enddate='2023-11-13', 180 | sort=None, 181 | ) 182 | ) 183 | 184 | with pytest.raises(ValueError): 185 | await block.daily_average_time_for_a_block(start_date, end_date, 'wrong') 186 | 187 | 188 | @pytest.mark.asyncio 189 | async def test_daily_uncle_block_count(block): 190 | start_date = date(2023, 11, 12) 191 | end_date = date(2023, 11, 13) 192 | 193 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 194 | await block.daily_uncle_block_count(start_date, end_date, 'asc') 195 | mock.assert_called_once_with( 196 | params=dict( 197 | module='stats', 198 | action='dailyuncleblkcount', 199 | startdate='2023-11-12', 200 | enddate='2023-11-13', 201 | sort='asc', 202 | ) 203 | ) 204 | 205 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 206 | await block.daily_uncle_block_count(start_date, end_date) 207 | mock.assert_called_once_with( 208 | params=dict( 209 | module='stats', 210 | action='dailyuncleblkcount', 211 | startdate='2023-11-12', 212 | enddate='2023-11-13', 213 | sort=None, 214 | ) 215 | ) 216 | 217 | with pytest.raises(ValueError): 218 | await block.daily_uncle_block_count(start_date, end_date, 'wrong') 219 | -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock, PropertyMock, Mock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | from aioetherscan.modules.account import Account 8 | from aioetherscan.modules.block import Block 9 | from aioetherscan.modules.contract import Contract 10 | from aioetherscan.modules.extra import ExtraModules, ContractUtils 11 | from aioetherscan.modules.extra.generators.generator_utils import GeneratorUtils 12 | from aioetherscan.modules.extra.link import LinkUtils 13 | from aioetherscan.modules.logs import Logs 14 | from aioetherscan.modules.proxy import Proxy 15 | from aioetherscan.modules.stats import Stats 16 | from aioetherscan.modules.transaction import Transaction 17 | from aioetherscan.network import Network 18 | from aioetherscan.url_builder import UrlBuilder 19 | 20 | 21 | @pytest_asyncio.fixture 22 | async def client(): 23 | c = Client('TestApiKey') 24 | yield c 25 | await c.close() 26 | 27 | 28 | def test_api_key(): 29 | with pytest.raises(TypeError): 30 | # noinspection PyArgumentList,PyUnusedLocal 31 | Client() 32 | 33 | 34 | def test_init(client): 35 | assert isinstance(client._url_builder, UrlBuilder) 36 | assert isinstance(client._http, Network) 37 | 38 | assert isinstance(client.account, Account) 39 | assert isinstance(client.block, Block) 40 | assert isinstance(client.contract, Contract) 41 | assert isinstance(client.transaction, Transaction) 42 | assert isinstance(client.stats, Stats) 43 | assert isinstance(client.logs, Logs) 44 | assert isinstance(client.proxy, Proxy) 45 | 46 | assert isinstance(client.extra, ExtraModules) 47 | assert isinstance(client.extra.link, LinkUtils) 48 | assert isinstance(client.extra.contract, ContractUtils) 49 | assert isinstance(client.extra.generators, GeneratorUtils) 50 | 51 | assert isinstance(client.account._client, Client) 52 | assert isinstance(client.block._client, Client) 53 | assert isinstance(client.contract._client, Client) 54 | assert isinstance(client.transaction._client, Client) 55 | assert isinstance(client.stats._client, Client) 56 | assert isinstance(client.logs._client, Client) 57 | assert isinstance(client.proxy._client, Client) 58 | 59 | assert isinstance(client.extra._client, Client) 60 | assert isinstance(client.extra.contract._client, Client) 61 | assert isinstance(client.extra.generators._client, Client) 62 | assert isinstance(client.extra.link._url_builder, UrlBuilder) 63 | 64 | 65 | @pytest.mark.asyncio 66 | async def test_close_session(client): 67 | with patch('aioetherscan.network.Network.close', new_callable=AsyncMock) as m: 68 | await client.close() 69 | m.assert_called_once_with() 70 | 71 | 72 | def test_currency(client): 73 | with patch('aioetherscan.url_builder.UrlBuilder.currency', new_callable=PropertyMock) as m: 74 | currency = 'ETH' 75 | m.return_value = currency 76 | 77 | assert client.currency == currency 78 | m.assert_called_once() 79 | 80 | 81 | def test_api_kind(client): 82 | client._url_builder.api_kind = Mock() 83 | client._url_builder.api_kind.title = Mock() 84 | 85 | client.api_kind 86 | 87 | client._url_builder.api_kind.assert_not_called() 88 | client._url_builder.api_kind.title.assert_called_once() 89 | 90 | 91 | def test_scaner_url(client): 92 | url = 'some_url' 93 | client._url_builder.BASE_URL = url 94 | assert client.scaner_url == url 95 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | 5 | from aioetherscan.common import ( 6 | check_hex, 7 | check_tag, 8 | check_value, 9 | check_sort_direction, 10 | check_blocktype, 11 | check_client_type, 12 | check_sync_mode, 13 | ) 14 | 15 | 16 | def test_check_hex(): 17 | assert check_hex(123) == '0x7b' 18 | assert check_hex(0x7B) == '0x7b' 19 | assert check_hex('0x7b') == '0x7b' 20 | with pytest.raises(ValueError): 21 | check_hex('wrong') 22 | 23 | 24 | def test_check_tag(): 25 | assert check_tag('latest') == 'latest' 26 | assert check_tag('earliest') == 'earliest' 27 | assert check_tag('pending') == 'pending' 28 | 29 | with patch('aioetherscan.common.check_hex', new=Mock()) as mock: 30 | check_tag(123) 31 | mock.assert_called_once_with(123) 32 | 33 | 34 | def test_check(): 35 | assert check_value('a', ('a', 'b')) == 'a' 36 | assert check_value('A', ('a', 'b')) == 'A' 37 | with pytest.raises(ValueError): 38 | check_value('c', ('a', 'b')) 39 | with pytest.raises(ValueError): 40 | check_value('C', ('a', 'b')) 41 | 42 | 43 | def test_check_sort_direction(): 44 | assert check_sort_direction('asc') == 'asc' 45 | assert check_sort_direction('desc') == 'desc' 46 | with pytest.raises(ValueError): 47 | check_sort_direction('wrong') 48 | 49 | 50 | def test_check_blocktype(): 51 | assert check_blocktype('blocks') == 'blocks' 52 | assert check_blocktype('uncles') == 'uncles' 53 | with pytest.raises(ValueError): 54 | check_blocktype('wrong') 55 | 56 | 57 | def test_check_client_type(): 58 | assert check_client_type('geth') == 'geth' 59 | assert check_client_type('parity') == 'parity' 60 | with pytest.raises(ValueError): 61 | check_client_type('wrong') 62 | 63 | 64 | def test_check_sync_mode(): 65 | assert check_sync_mode('default') == 'default' 66 | assert check_sync_mode('archive') == 'archive' 67 | with pytest.raises(ValueError): 68 | check_sync_mode('wrong') 69 | -------------------------------------------------------------------------------- /tests/test_contract.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def contract(): 11 | c = Client('TestApiKey') 12 | yield c.contract 13 | await c.close() 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_contract_abi(contract): 18 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 19 | await contract.contract_abi('0x012345') 20 | mock.assert_called_once_with( 21 | params=dict(module='contract', action='getabi', address='0x012345') 22 | ) 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_contract_source_code(contract): 27 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 28 | await contract.contract_source_code('0x012345') 29 | mock.assert_called_once_with( 30 | params=dict(module='contract', action='getsourcecode', address='0x012345') 31 | ) 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_contract_creation(contract): 36 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 37 | await contract.contract_creation(['0x012345', '0x678901']) 38 | mock.assert_called_once_with( 39 | params=dict( 40 | module='contract', 41 | action='getcontractcreation', 42 | contractaddresses='0x012345,0x678901', 43 | ) 44 | ) 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_verify_contract_source_code(contract): 49 | with patch('aioetherscan.network.Network.post', new=AsyncMock()) as mock: 50 | await contract.verify_contract_source_code( 51 | contract_address='0x012345', 52 | source_code='some source code\ntest', 53 | contract_name='some contract name', 54 | compiler_version='1.0.0', 55 | optimization_used=False, 56 | runs=123, 57 | constructor_arguements='some args', 58 | ) 59 | mock.assert_called_once_with( 60 | data=dict( 61 | module='contract', 62 | action='verifysourcecode', 63 | contractaddress='0x012345', 64 | sourceCode='some source code\ntest', 65 | contractname='some contract name', 66 | compilerversion='1.0.0', 67 | optimizationUsed=0, 68 | runs=123, 69 | constructorArguements='some args', 70 | ) 71 | ) 72 | 73 | with patch('aioetherscan.network.Network.post', new=AsyncMock()) as mock: 74 | await contract.verify_contract_source_code( 75 | contract_address='0x012345', 76 | source_code='some source code\ntest', 77 | contract_name='some contract name', 78 | compiler_version='1.0.0', 79 | optimization_used=False, 80 | runs=123, 81 | constructor_arguements='some args', 82 | libraries={'one_name': 'one_addr', 'two_name': 'two_addr'}, 83 | ) 84 | mock.assert_called_once_with( 85 | data=dict( 86 | module='contract', 87 | action='verifysourcecode', 88 | contractaddress='0x012345', 89 | sourceCode='some source code\ntest', 90 | contractname='some contract name', 91 | compilerversion='1.0.0', 92 | optimizationUsed=0, 93 | runs=123, 94 | constructorArguements='some args', 95 | libraryname1='one_name', 96 | libraryaddress1='one_addr', 97 | libraryname2='two_name', 98 | libraryaddress2='two_addr', 99 | ) 100 | ) 101 | 102 | 103 | @pytest.mark.asyncio 104 | async def test_check_verification_status(contract): 105 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 106 | await contract.check_verification_status('some_guid') 107 | mock.assert_called_once_with( 108 | params=dict(module='contract', action='checkverifystatus', guid='some_guid') 109 | ) 110 | 111 | 112 | async def test_verify_proxy_contract(contract): 113 | with patch('aioetherscan.network.Network.post', new=AsyncMock()) as mock: 114 | await contract.verify_proxy_contract( 115 | address='0x012345', 116 | ) 117 | mock.assert_called_once_with( 118 | data=dict( 119 | module='contract', 120 | action='verifyproxycontract', 121 | address='0x012345', 122 | expectedimplementation=None, 123 | ) 124 | ) 125 | 126 | with patch('aioetherscan.network.Network.post', new=AsyncMock()) as mock: 127 | await contract.verify_proxy_contract( 128 | address='0x012345', 129 | expected_implementation='0x54321', 130 | ) 131 | mock.assert_called_once_with( 132 | data=dict( 133 | module='contract', 134 | action='verifyproxycontract', 135 | address='0x012345', 136 | expectedimplementation='0x54321', 137 | ) 138 | ) 139 | 140 | 141 | @pytest.mark.asyncio 142 | async def test_check_proxy_contract_verification(contract): 143 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 144 | await contract.check_proxy_contract_verification('some_guid') 145 | mock.assert_called_once_with( 146 | params=dict(module='contract', action='checkproxyverification', guid='some_guid') 147 | ) 148 | 149 | 150 | def test_parse_libraries(contract): 151 | mydict = { 152 | 'lib1': 'addr1', 153 | 'lib2': 'addr2', 154 | } 155 | expected = { 156 | 'libraryname1': 'lib1', 157 | 'libraryaddress1': 'addr1', 158 | 'libraryname2': 'lib2', 159 | 'libraryaddress2': 'addr2', 160 | } 161 | assert contract._parse_libraries(mydict) == expected 162 | 163 | mydict = { 164 | 'lib1': 'addr1', 165 | } 166 | expected = { 167 | 'libraryname1': 'lib1', 168 | 'libraryaddress1': 'addr1', 169 | } 170 | assert contract._parse_libraries(mydict) == expected 171 | 172 | mydict = {} 173 | expected = {} 174 | assert contract._parse_libraries(mydict) == expected 175 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from aioetherscan.exceptions import ( 2 | EtherscanClientContentTypeError, 3 | EtherscanClientApiError, 4 | EtherscanClientProxyError, 5 | ) 6 | 7 | 8 | def test_content_type_error(): 9 | e = EtherscanClientContentTypeError(1, 2) 10 | assert e.status == 1 11 | assert e.content == 2 12 | assert str(e) == '[1] 2' 13 | 14 | 15 | def test_api_error(): 16 | e = EtherscanClientApiError(1, 2) 17 | assert e.message == 1 18 | assert e.result == 2 19 | assert str(e) == '[1] 2' 20 | 21 | 22 | def test_proxy_error(): 23 | e = EtherscanClientProxyError(1, 2) 24 | assert e.code == 1 25 | assert e.message == 2 26 | assert str(e) == '[1] 2' 27 | -------------------------------------------------------------------------------- /tests/test_gas_tracker.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from unittest.mock import patch, AsyncMock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from aioetherscan import Client 8 | 9 | 10 | @pytest_asyncio.fixture 11 | async def gas_tracker(): 12 | c = Client('TestApiKey') 13 | yield c.gas_tracker 14 | await c.close() 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_estimation_of_confirmation_time(gas_tracker): 19 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 20 | await gas_tracker.estimation_of_confirmation_time(123) 21 | mock.assert_called_once_with( 22 | params=dict(module='gastracker', action='gasestimate', gasprice=123) 23 | ) 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_gas_oracle(gas_tracker): 28 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 29 | await gas_tracker.gas_oracle() 30 | mock.assert_called_once_with(params=dict(module='gastracker', action='gasoracle')) 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_daily_average_gas_limit(gas_tracker): 35 | start_date = date(2023, 11, 12) 36 | end_date = date(2023, 11, 13) 37 | 38 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 39 | await gas_tracker.daily_average_gas_limit(start_date, end_date, 'asc') 40 | mock.assert_called_once_with( 41 | params=dict( 42 | module='stats', 43 | action='dailyavggaslimit', 44 | startdate='2023-11-12', 45 | enddate='2023-11-13', 46 | sort='asc', 47 | ) 48 | ) 49 | 50 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 51 | await gas_tracker.daily_average_gas_limit(start_date, end_date) 52 | mock.assert_called_once_with( 53 | params=dict( 54 | module='stats', 55 | action='dailyavggaslimit', 56 | startdate='2023-11-12', 57 | enddate='2023-11-13', 58 | sort=None, 59 | ) 60 | ) 61 | 62 | with pytest.raises(ValueError): 63 | await gas_tracker.daily_average_gas_limit(start_date, end_date, 'wrong') 64 | 65 | 66 | @pytest.mark.asyncio 67 | async def test_daily_total_gas_used(gas_tracker): 68 | start_date = date(2023, 11, 12) 69 | end_date = date(2023, 11, 13) 70 | 71 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 72 | await gas_tracker.daily_total_gas_used(start_date, end_date, 'asc') 73 | mock.assert_called_once_with( 74 | params=dict( 75 | module='stats', 76 | action='dailygasused', 77 | startdate='2023-11-12', 78 | enddate='2023-11-13', 79 | sort='asc', 80 | ) 81 | ) 82 | 83 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 84 | await gas_tracker.daily_total_gas_used(start_date, end_date) 85 | mock.assert_called_once_with( 86 | params=dict( 87 | module='stats', 88 | action='dailygasused', 89 | startdate='2023-11-12', 90 | enddate='2023-11-13', 91 | sort=None, 92 | ) 93 | ) 94 | 95 | with pytest.raises(ValueError): 96 | await gas_tracker.daily_total_gas_used(start_date, end_date, 'wrong') 97 | 98 | 99 | @pytest.mark.asyncio 100 | async def test_daily_average_gas_price(gas_tracker): 101 | start_date = date(2023, 11, 12) 102 | end_date = date(2023, 11, 13) 103 | 104 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 105 | await gas_tracker.daily_average_gas_price(start_date, end_date, 'asc') 106 | mock.assert_called_once_with( 107 | params=dict( 108 | module='stats', 109 | action='dailyavggasprice', 110 | startdate='2023-11-12', 111 | enddate='2023-11-13', 112 | sort='asc', 113 | ) 114 | ) 115 | 116 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 117 | await gas_tracker.daily_average_gas_price(start_date, end_date) 118 | mock.assert_called_once_with( 119 | params=dict( 120 | module='stats', 121 | action='dailyavggasprice', 122 | startdate='2023-11-12', 123 | enddate='2023-11-13', 124 | sort=None, 125 | ) 126 | ) 127 | 128 | with pytest.raises(ValueError): 129 | await gas_tracker.daily_average_gas_price(start_date, end_date, 'wrong') 130 | -------------------------------------------------------------------------------- /tests/test_logs.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock, Mock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def logs(): 11 | c = Client('TestApiKey') 12 | yield c.logs 13 | await c.close() 14 | 15 | 16 | def test_module(logs): 17 | assert logs._module == 'logs' 18 | 19 | 20 | @pytest.mark.asyncio 21 | async def test_get_logs_raises_value_error_when_no_address_or_topics_provided(logs): 22 | with pytest.raises(ValueError): 23 | await logs.get_logs() 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_get_logs_calls_get_with_correct_parameters(logs): 28 | logs._get = AsyncMock(return_value=[]) 29 | await logs.get_logs(address='0x123', from_block=1, to_block=2) 30 | logs._get.assert_called_once_with( 31 | action='getLogs', 32 | address='0x123', 33 | fromBlock=1, 34 | toBlock=2, 35 | page=None, 36 | offset=None, 37 | ) 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_get_logs_calls_fill_topics_with_correct_parameters(logs): 42 | logs._get = AsyncMock(return_value=[]) 43 | logs._fill_topics = Mock(return_value={}) 44 | await logs.get_logs(topics={0: '0x123'}, operators=[(0, 1, 'and')]) 45 | logs._fill_topics.assert_called_once_with( 46 | {0: '0x123'}, 47 | [(0, 1, 'and')], 48 | ) 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_get_logs_returns_result_of_get(logs): 53 | logs._get = AsyncMock(return_value=[{'log': 'entry'}]) 54 | result = await logs.get_logs(address='0x123', from_block=1, to_block=2) 55 | assert result == [{'log': 'entry'}] 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_get_logs_calls_get_with_page_and_offset(logs): 60 | logs._get = AsyncMock(return_value=[]) 61 | await logs.get_logs(address='0x123', from_block=1, to_block=2, page=3, offset=10) 62 | logs._get.assert_called_once_with( 63 | action='getLogs', 64 | address='0x123', 65 | fromBlock=1, 66 | toBlock=2, 67 | page=3, 68 | offset=10, 69 | ) 70 | 71 | 72 | @pytest.mark.asyncio 73 | async def test_get_logs_raises_value_error_when_both_address_and_topics_are_empty(logs): 74 | with pytest.raises(ValueError): 75 | await logs.get_logs() 76 | 77 | 78 | def test_fill_topics_none(logs): 79 | topics = None 80 | topic_operators = None 81 | assert logs._fill_topics(topics, topic_operators) == {} 82 | 83 | 84 | def test_fill_topics_one_element(logs): 85 | topics = {0: '0x123'} 86 | topic_operators = None 87 | assert logs._fill_topics(topics, topic_operators) == {'topic0': '0x123'} 88 | 89 | 90 | def test_fill_topics_multiple_elements_none(logs): 91 | topics = {0: '0x123', 1: '0x456'} 92 | topic_operators = None 93 | with pytest.raises(ValueError): 94 | logs._fill_topics(topics, topic_operators) 95 | 96 | 97 | def test_fill_topics_multiple_elements_operators(logs): 98 | topics = {0: '0x123', 1: '0x456', 3: '0x789'} 99 | topic_operators = {(0, 1, 'and'), (1, 2, 'or'), (0, 3, 'or')} 100 | assert logs._fill_topics(topics, topic_operators) == { 101 | 'topic0': '0x123', 102 | 'topic1': '0x456', 103 | 'topic3': '0x789', 104 | 'topic0_1_opr': 'and', 105 | 'topic1_2_opr': 'or', 106 | 'topic0_3_opr': 'or', 107 | } 108 | 109 | 110 | def test_fill_topic_operators_ok(logs): 111 | topic_operators = {(0, 1, 'and'), (1, 2, 'or'), (0, 3, 'or'), (0, 2, 'and')} 112 | assert logs._fill_topic_operators(topic_operators) == { 113 | 'topic0_1_opr': 'and', 114 | 'topic1_2_opr': 'or', 115 | 'topic0_3_opr': 'or', 116 | 'topic0_2_opr': 'and', 117 | } 118 | 119 | 120 | @pytest.mark.parametrize( 121 | 'topic_operators', 122 | [ 123 | {(0, 1, 'and'), (1, 0, 'or'), (0, 3, 'or')}, # duplicate 124 | {(0, 1, 'and'), (1, 2, 'or'), (3, 3, 'or')}, # same topic twice 125 | {(0, 1, 'and'), (1, 0, 'or'), (3, 3, 'or')}, # both 126 | ], 127 | ) 128 | def test_fill_topic_operators_exception(logs, topic_operators): 129 | with pytest.raises(ValueError) as exc_info: 130 | logs._fill_topic_operators(topic_operators) 131 | assert ( 132 | str(exc_info.value) 133 | == 'Topic operators must be used with 2 different topics without duplicates.' 134 | ) 135 | -------------------------------------------------------------------------------- /tests/test_network.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | from unittest.mock import patch, AsyncMock, MagicMock, Mock 5 | 6 | import aiohttp 7 | import aiohttp_retry 8 | import pytest 9 | import pytest_asyncio 10 | from aiohttp import ClientTimeout 11 | from aiohttp.hdrs import METH_GET, METH_POST 12 | from aiohttp_retry import ExponentialRetry 13 | from asyncio_throttle import Throttler 14 | 15 | from aioetherscan.exceptions import ( 16 | EtherscanClientContentTypeError, 17 | EtherscanClientError, 18 | EtherscanClientApiError, 19 | EtherscanClientProxyError, 20 | ) 21 | from aioetherscan.network import Network 22 | from aioetherscan.url_builder import UrlBuilder 23 | 24 | 25 | class SessionMock(AsyncMock): 26 | # noinspection PyUnusedLocal 27 | @pytest.mark.asyncio 28 | async def get(self, url, params, data): 29 | return AsyncCtxMgrMock() 30 | 31 | 32 | class AsyncCtxMgrMock(MagicMock): 33 | @pytest.mark.asyncio 34 | async def __aenter__(self): 35 | return self.aenter 36 | 37 | @pytest.mark.asyncio 38 | async def __aexit__(self, *args): 39 | pass 40 | 41 | 42 | def get_loop(): 43 | return asyncio.get_event_loop() 44 | 45 | 46 | @pytest_asyncio.fixture 47 | async def ub(): 48 | ub = UrlBuilder('test_api_key', 'eth', 'main') 49 | yield ub 50 | 51 | 52 | @pytest_asyncio.fixture 53 | async def nw(ub): 54 | nw = Network(ub, get_loop(), None, None, None, None) 55 | yield nw 56 | await nw.close() 57 | 58 | 59 | def test_init(ub): 60 | myloop = get_loop() 61 | proxy = 'qwe' 62 | timeout = ClientTimeout(5) 63 | throttler = Throttler(1) 64 | retry_options = ExponentialRetry() 65 | n = Network(ub, myloop, timeout, proxy, throttler, retry_options) 66 | 67 | assert n._url_builder is ub 68 | assert n._loop == myloop 69 | assert n._timeout is timeout 70 | assert n._proxy is proxy 71 | assert n._throttler is throttler 72 | 73 | assert n._retry_options is retry_options 74 | assert n._retry_client is None 75 | 76 | assert isinstance(n._logger, logging.Logger) 77 | 78 | 79 | def test_no_loop(ub): 80 | with pytest.raises(RuntimeError) as e: 81 | Network(ub, None, None, None, None, None) 82 | assert str(e.value) == 'no running event loop' 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_get(nw): 87 | with patch('aioetherscan.network.Network._request', new=AsyncMock()) as mock: 88 | await nw.get() 89 | mock.assert_called_once_with(METH_GET, params={'apikey': nw._url_builder._API_KEY}) 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_post(nw): 94 | with patch('aioetherscan.network.Network._request', new=AsyncMock()) as mock: 95 | await nw.post() 96 | mock.assert_called_once_with(METH_POST, data={'apikey': nw._url_builder._API_KEY}) 97 | 98 | with patch('aioetherscan.network.Network._request', new=AsyncMock()) as mock: 99 | await nw.post({'some': 'data'}) 100 | mock.assert_called_once_with( 101 | METH_POST, data={'apikey': nw._url_builder._API_KEY, 'some': 'data'} 102 | ) 103 | 104 | with patch('aioetherscan.network.Network._request', new=AsyncMock()) as mock: 105 | await nw.post({'some': 'data', 'null': None}) 106 | mock.assert_called_once_with( 107 | METH_POST, data={'apikey': nw._url_builder._API_KEY, 'some': 'data'} 108 | ) 109 | 110 | 111 | @pytest.mark.asyncio 112 | async def test_request(nw): 113 | class MagicMockContext(MagicMock): 114 | def __init__(self, *args, **kwargs): 115 | super().__init__(*args, **kwargs) 116 | type(self).__aenter__ = AsyncMock(return_value=MagicMock()) 117 | type(self).__aexit__ = AsyncMock(return_value=MagicMock()) 118 | 119 | throttler_mock = AsyncMock() 120 | nw._throttler = AsyncMock() 121 | nw._throttler.__aenter__ = throttler_mock 122 | 123 | retry_client_mock = Mock() 124 | retry_client_mock.get = MagicMockContext() 125 | retry_client_mock.close = AsyncMock() 126 | nw._get_retry_client = Mock(return_value=retry_client_mock) 127 | 128 | with patch('aioetherscan.network.Network._handle_response', new=AsyncMock()) as h: 129 | await nw._request(METH_GET) 130 | throttler_mock.assert_awaited_once() 131 | retry_client_mock.get.assert_called_once_with( 132 | 'https://api.etherscan.io/api', params=None, data=None, proxy=None 133 | ) 134 | h.assert_called_once() 135 | 136 | post_mock = MagicMockContext() 137 | nw._retry_client.post = post_mock 138 | with patch('aioetherscan.network.Network._handle_response', new=AsyncMock()) as h: 139 | await nw._request(METH_POST) 140 | nw._get_retry_client.assert_called_once() 141 | throttler_mock.assert_awaited() 142 | post_mock.assert_called_once_with( 143 | 'https://api.etherscan.io/api', params=None, data=None, proxy=None 144 | ) 145 | h.assert_called_once() 146 | 147 | assert throttler_mock.call_count == 2 148 | 149 | 150 | # noinspection PyTypeChecker 151 | @pytest.mark.asyncio 152 | async def test_handle_response(nw): 153 | class MockResponse: 154 | def __init__(self, data, raise_exc=None): 155 | self.data = data 156 | self.raise_exc = raise_exc 157 | 158 | @property 159 | def status(self): 160 | return 200 161 | 162 | # noinspection PyMethodMayBeStatic 163 | async def text(self): 164 | return 'some text' 165 | 166 | async def json(self): 167 | if self.raise_exc: 168 | raise self.raise_exc 169 | return json.loads(self.data) 170 | 171 | with pytest.raises(EtherscanClientContentTypeError) as e: 172 | await nw._handle_response(MockResponse('some', aiohttp.ContentTypeError('info', 'hist'))) 173 | assert e.value.status == 200 174 | assert e.value.content == 'some text' 175 | 176 | with pytest.raises(EtherscanClientError, match='some exc'): 177 | await nw._handle_response(MockResponse('some', Exception('some exception'))) 178 | 179 | with pytest.raises(EtherscanClientApiError) as e: 180 | await nw._handle_response( 181 | MockResponse('{"status": "0", "message": "NOTOK", "result": "res"}') 182 | ) 183 | assert e.value.message == 'NOTOK' 184 | assert e.value.result == 'res' 185 | 186 | with pytest.raises(EtherscanClientProxyError) as e: 187 | await nw._handle_response(MockResponse('{"error": {"code": "100", "message": "msg"}}')) 188 | assert e.value.code == '100' 189 | assert e.value.message == 'msg' 190 | 191 | assert await nw._handle_response(MockResponse('{"result": "some_result"}')) == 'some_result' 192 | 193 | 194 | @pytest.mark.asyncio 195 | async def test_close_session(nw): 196 | with patch('aiohttp.ClientSession.close', new_callable=AsyncMock) as m: 197 | await nw.close() 198 | m: AsyncMock 199 | m.assert_not_called() 200 | 201 | nw._retry_client = MagicMock() 202 | nw._retry_client.close = AsyncMock() 203 | await nw.close() 204 | nw._retry_client.close.assert_called_once() 205 | 206 | 207 | def test_get_session_timeout_is_none(nw): 208 | with patch('aiohttp.ClientSession.__new__', new=Mock()) as m: 209 | session = nw._get_session() 210 | 211 | m.assert_called_once_with( 212 | aiohttp.ClientSession, 213 | loop=nw._loop, 214 | ) 215 | 216 | assert session is m.return_value 217 | 218 | 219 | def test_get_session_timeout_is_not_none(nw): 220 | nw._timeout = 1 221 | 222 | with patch('aiohttp.ClientSession.__new__', new=Mock()) as m: 223 | session = nw._get_session() 224 | 225 | m.assert_called_once_with(aiohttp.ClientSession, loop=nw._loop, timeout=nw._timeout) 226 | 227 | assert session is m.return_value 228 | 229 | 230 | def test_get_retry_client(nw): 231 | nw._get_session = Mock() 232 | 233 | with patch('aiohttp_retry.RetryClient.__new__', new=Mock()) as m: 234 | result = nw._get_retry_client() 235 | m.assert_called_once_with( 236 | aiohttp_retry.RetryClient, 237 | client_session=nw._get_session.return_value, 238 | retry_options=nw._retry_options, 239 | ) 240 | assert result is m.return_value 241 | -------------------------------------------------------------------------------- /tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock, call, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def proxy(): 11 | c = Client('TestApiKey') 12 | yield c.proxy 13 | await c.close() 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_block_number(proxy): 18 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 19 | await proxy.block_number() 20 | mock.assert_called_once_with( 21 | params=dict( 22 | module='proxy', 23 | action='eth_blockNumber', 24 | ) 25 | ) 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_block_by_number(proxy): 30 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 31 | await proxy.block_by_number(True) 32 | mock.assert_called_once_with( 33 | params=dict(module='proxy', action='eth_getBlockByNumber', boolean=True, tag='latest') 34 | ) 35 | 36 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 37 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 38 | await proxy.block_by_number(True) 39 | tag_mock.assert_called_once_with('latest') 40 | mock.assert_called_once() 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_uncle_block_by_number_and_index(proxy): 45 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 46 | await proxy.uncle_block_by_number_and_index(123) 47 | mock.assert_called_once_with( 48 | params=dict( 49 | module='proxy', 50 | action='eth_getUncleByBlockNumberAndIndex', 51 | index='0x7b', 52 | tag='latest', 53 | ) 54 | ) 55 | 56 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 57 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 58 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 59 | await proxy.uncle_block_by_number_and_index(123) 60 | hex_mock.assert_called_once_with(123) 61 | tag_mock.assert_called_once_with('latest') 62 | mock.assert_called_once() 63 | 64 | 65 | @pytest.mark.asyncio 66 | async def test_block_tx_count_by_number(proxy): 67 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 68 | await proxy.block_tx_count_by_number() 69 | mock.assert_called_once_with( 70 | params=dict(module='proxy', action='eth_getBlockTransactionCountByNumber', tag='latest') 71 | ) 72 | 73 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 74 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 75 | await proxy.block_tx_count_by_number(123) 76 | tag_mock.assert_called_once_with(123) 77 | mock.assert_called_once() 78 | 79 | 80 | @pytest.mark.asyncio 81 | async def test_tx_by_hash(proxy): 82 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 83 | await proxy.tx_by_hash('0x123') 84 | mock.assert_called_once_with( 85 | params=dict(module='proxy', action='eth_getTransactionByHash', txhash='0x123') 86 | ) 87 | 88 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 89 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 90 | await proxy.tx_by_hash(123) 91 | hex_mock.assert_called_once_with(123) 92 | mock.assert_called_once() 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_tx_by_number_and_index(proxy): 97 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 98 | await proxy.tx_by_number_and_index(123) 99 | mock.assert_called_once_with( 100 | params=dict( 101 | module='proxy', 102 | action='eth_getTransactionByBlockNumberAndIndex', 103 | index='0x7b', 104 | tag='latest', 105 | ) 106 | ) 107 | 108 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 109 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 110 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 111 | await proxy.tx_by_number_and_index(123, 456) 112 | hex_mock.assert_called_once_with(123) 113 | tag_mock.assert_called_once_with(456) 114 | mock.assert_called_once() 115 | 116 | 117 | @pytest.mark.asyncio 118 | async def test_tx_count(proxy): 119 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 120 | await proxy.tx_count('addr') 121 | mock.assert_called_once_with( 122 | params=dict( 123 | module='proxy', action='eth_getTransactionCount', address='addr', tag='latest' 124 | ) 125 | ) 126 | 127 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 128 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 129 | await proxy.tx_count('addr', 123) 130 | tag_mock.assert_called_once_with(123) 131 | mock.assert_called_once() 132 | 133 | 134 | @pytest.mark.asyncio 135 | async def test_send_raw_tx(proxy): 136 | with patch('aioetherscan.network.Network.post', new=AsyncMock()) as mock: 137 | await proxy.send_raw_tx('somehex') 138 | mock.assert_called_once_with( 139 | data=dict( 140 | module='proxy', 141 | action='eth_sendRawTransaction', 142 | hex='somehex', 143 | ) 144 | ) 145 | 146 | 147 | @pytest.mark.asyncio 148 | async def test_tx_receipt(proxy): 149 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 150 | await proxy.tx_receipt('0x123') 151 | mock.assert_called_once_with( 152 | params=dict( 153 | module='proxy', 154 | action='eth_getTransactionReceipt', 155 | txhash='0x123', 156 | ) 157 | ) 158 | 159 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 160 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 161 | await proxy.tx_receipt('0x123') 162 | hex_mock.assert_called_once_with('0x123') 163 | mock.assert_called_once() 164 | 165 | 166 | @pytest.mark.asyncio 167 | async def test_call(proxy): 168 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 169 | await proxy.call('0x123', '0x456') 170 | mock.assert_called_once_with( 171 | params=dict(module='proxy', action='eth_call', to='0x123', data='0x456', tag='latest') 172 | ) 173 | 174 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 175 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 176 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 177 | await proxy.call('0x123', '0x456', '0x789') 178 | hex_mock.assert_has_calls([call('0x123'), call('0x456')]) 179 | tag_mock.assert_called_once_with('0x789') 180 | mock.assert_called_once() 181 | 182 | 183 | @pytest.mark.asyncio 184 | async def test_code(proxy): 185 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 186 | await proxy.code('addr') 187 | mock.assert_called_once_with( 188 | params=dict(module='proxy', action='eth_getCode', address='addr', tag='latest') 189 | ) 190 | 191 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 192 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 193 | await proxy.code('addr', 123) 194 | tag_mock.assert_called_once_with(123) 195 | mock.assert_called_once() 196 | 197 | 198 | @pytest.mark.asyncio 199 | async def test_storage_at(proxy): 200 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 201 | await proxy.storage_at('addr', 'pos') 202 | mock.assert_called_once_with( 203 | params=dict( 204 | module='proxy', 205 | action='eth_getStorageAt', 206 | address='addr', 207 | position='pos', 208 | tag='latest', 209 | ) 210 | ) 211 | 212 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 213 | with patch('aioetherscan.modules.proxy.check_tag', new=Mock()) as tag_mock: 214 | await proxy.storage_at('addr', 'pos', 123) 215 | tag_mock.assert_called_once_with(123) 216 | mock.assert_called_once() 217 | 218 | 219 | @pytest.mark.asyncio 220 | async def test_gas_price(proxy): 221 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 222 | await proxy.gas_price() 223 | mock.assert_called_once_with( 224 | params=dict( 225 | module='proxy', 226 | action='eth_gasPrice', 227 | ) 228 | ) 229 | 230 | 231 | @pytest.mark.asyncio 232 | async def test_estimate_gas(proxy): 233 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 234 | await proxy.estimate_gas(to='0x123', value='val', gas_price='123', gas='456') 235 | mock.assert_called_once_with( 236 | params=dict( 237 | module='proxy', 238 | action='eth_estimateGas', 239 | to='0x123', 240 | value='val', 241 | gasPrice='123', 242 | gas='456', 243 | ) 244 | ) 245 | 246 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 247 | with patch('aioetherscan.modules.proxy.check_hex', new=Mock()) as hex_mock: 248 | await proxy.estimate_gas(to='0x123', value='val', gas_price='123', gas='456') 249 | hex_mock.assert_called_once_with('0x123') 250 | mock.assert_called_once() 251 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from unittest.mock import patch, AsyncMock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from aioetherscan import Client 8 | 9 | 10 | @pytest_asyncio.fixture 11 | async def stats(): 12 | c = Client('TestApiKey') 13 | yield c.stats 14 | await c.close() 15 | 16 | 17 | @pytest.mark.asyncio 18 | async def test_eth_supply(stats): 19 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 20 | await stats.eth_supply() 21 | mock.assert_called_once_with(params=dict(module='stats', action='ethsupply')) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_eth2_supply(stats): 26 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 27 | await stats.eth2_supply() 28 | mock.assert_called_once_with(params=dict(module='stats', action='ethsupply2')) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_eth_price(stats): 33 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 34 | await stats.eth_price() 35 | mock.assert_called_once_with(params=dict(module='stats', action='ethprice')) 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_eth_nodes_size(stats): 40 | start_date = date(2023, 11, 12) 41 | end_date = date(2023, 11, 13) 42 | 43 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 44 | await stats.eth_nodes_size(start_date, end_date, 'geth', 'default', 'asc') 45 | mock.assert_called_once_with( 46 | params=dict( 47 | module='stats', 48 | action='chainsize', 49 | startdate='2023-11-12', 50 | enddate='2023-11-13', 51 | clienttype='geth', 52 | syncmode='default', 53 | sort='asc', 54 | ) 55 | ) 56 | 57 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 58 | await stats.eth_nodes_size( 59 | start_date, 60 | end_date, 61 | 'geth', 62 | 'default', 63 | ) 64 | mock.assert_called_once_with( 65 | params=dict( 66 | module='stats', 67 | action='chainsize', 68 | startdate='2023-11-12', 69 | enddate='2023-11-13', 70 | clienttype='geth', 71 | syncmode='default', 72 | sort=None, 73 | ) 74 | ) 75 | 76 | with pytest.raises(ValueError): 77 | await stats.eth_nodes_size(start_date, end_date, 'wrong', 'default', 'asc') 78 | 79 | with pytest.raises(ValueError): 80 | await stats.eth_nodes_size(start_date, end_date, 'geth', 'wrong', 'asc') 81 | 82 | with pytest.raises(ValueError): 83 | await stats.eth_nodes_size(start_date, end_date, 'geth', 'default', 'wrong') 84 | 85 | 86 | @pytest.mark.asyncio 87 | async def test_total_nodes_count(stats): 88 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 89 | await stats.total_nodes_count() 90 | mock.assert_called_once_with(params=dict(module='stats', action='nodecount')) 91 | 92 | 93 | @pytest.mark.asyncio 94 | async def test_daily_network_tx_fee(stats): 95 | start_date = date(2023, 11, 12) 96 | end_date = date(2023, 11, 13) 97 | 98 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 99 | await stats.daily_network_tx_fee(start_date, end_date, 'asc') 100 | mock.assert_called_once_with( 101 | params=dict( 102 | module='stats', 103 | action='dailytxnfee', 104 | startdate='2023-11-12', 105 | enddate='2023-11-13', 106 | sort='asc', 107 | ) 108 | ) 109 | 110 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 111 | await stats.daily_network_tx_fee(start_date, end_date) 112 | mock.assert_called_once_with( 113 | params=dict( 114 | module='stats', 115 | action='dailytxnfee', 116 | startdate='2023-11-12', 117 | enddate='2023-11-13', 118 | sort=None, 119 | ) 120 | ) 121 | 122 | with pytest.raises(ValueError): 123 | await stats.daily_network_tx_fee(start_date, end_date, 'wrong') 124 | 125 | 126 | @pytest.mark.asyncio 127 | async def test_daily_new_address_count(stats): 128 | start_date = date(2023, 11, 12) 129 | end_date = date(2023, 11, 13) 130 | 131 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 132 | await stats.daily_new_address_count(start_date, end_date, 'asc') 133 | mock.assert_called_once_with( 134 | params=dict( 135 | module='stats', 136 | action='dailynewaddress', 137 | startdate='2023-11-12', 138 | enddate='2023-11-13', 139 | sort='asc', 140 | ) 141 | ) 142 | 143 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 144 | await stats.daily_new_address_count(start_date, end_date) 145 | mock.assert_called_once_with( 146 | params=dict( 147 | module='stats', 148 | action='dailynewaddress', 149 | startdate='2023-11-12', 150 | enddate='2023-11-13', 151 | sort=None, 152 | ) 153 | ) 154 | 155 | with pytest.raises(ValueError): 156 | await stats.daily_new_address_count(start_date, end_date, 'wrong') 157 | 158 | 159 | @pytest.mark.asyncio 160 | async def test_daily_network_utilization(stats): 161 | start_date = date(2023, 11, 12) 162 | end_date = date(2023, 11, 13) 163 | 164 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 165 | await stats.daily_network_utilization(start_date, end_date, 'asc') 166 | mock.assert_called_once_with( 167 | params=dict( 168 | module='stats', 169 | action='dailynetutilization', 170 | startdate='2023-11-12', 171 | enddate='2023-11-13', 172 | sort='asc', 173 | ) 174 | ) 175 | 176 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 177 | await stats.daily_network_utilization(start_date, end_date) 178 | mock.assert_called_once_with( 179 | params=dict( 180 | module='stats', 181 | action='dailynetutilization', 182 | startdate='2023-11-12', 183 | enddate='2023-11-13', 184 | sort=None, 185 | ) 186 | ) 187 | 188 | with pytest.raises(ValueError): 189 | await stats.daily_network_utilization(start_date, end_date, 'wrong') 190 | 191 | 192 | @pytest.mark.asyncio 193 | async def test_daily_average_network_hash_rate(stats): 194 | start_date = date(2023, 11, 12) 195 | end_date = date(2023, 11, 13) 196 | 197 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 198 | await stats.daily_average_network_hash_rate(start_date, end_date, 'asc') 199 | mock.assert_called_once_with( 200 | params=dict( 201 | module='stats', 202 | action='dailyavghashrate', 203 | startdate='2023-11-12', 204 | enddate='2023-11-13', 205 | sort='asc', 206 | ) 207 | ) 208 | 209 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 210 | await stats.daily_average_network_hash_rate(start_date, end_date) 211 | mock.assert_called_once_with( 212 | params=dict( 213 | module='stats', 214 | action='dailyavghashrate', 215 | startdate='2023-11-12', 216 | enddate='2023-11-13', 217 | sort=None, 218 | ) 219 | ) 220 | 221 | with pytest.raises(ValueError): 222 | await stats.daily_average_network_hash_rate(start_date, end_date, 'wrong') 223 | 224 | 225 | @pytest.mark.asyncio 226 | async def test_daily_transaction_count(stats): 227 | start_date = date(2023, 11, 12) 228 | end_date = date(2023, 11, 13) 229 | 230 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 231 | await stats.daily_transaction_count(start_date, end_date, 'asc') 232 | mock.assert_called_once_with( 233 | params=dict( 234 | module='stats', 235 | action='dailytx', 236 | startdate='2023-11-12', 237 | enddate='2023-11-13', 238 | sort='asc', 239 | ) 240 | ) 241 | 242 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 243 | await stats.daily_transaction_count(start_date, end_date) 244 | mock.assert_called_once_with( 245 | params=dict( 246 | module='stats', 247 | action='dailytx', 248 | startdate='2023-11-12', 249 | enddate='2023-11-13', 250 | sort=None, 251 | ) 252 | ) 253 | 254 | with pytest.raises(ValueError): 255 | await stats.daily_transaction_count(start_date, end_date, 'wrong') 256 | 257 | 258 | @pytest.mark.asyncio 259 | async def test_daily_average_network_difficulty(stats): 260 | start_date = date(2023, 11, 12) 261 | end_date = date(2023, 11, 13) 262 | 263 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 264 | await stats.daily_average_network_difficulty(start_date, end_date, 'asc') 265 | mock.assert_called_once_with( 266 | params=dict( 267 | module='stats', 268 | action='dailyavgnetdifficulty', 269 | startdate='2023-11-12', 270 | enddate='2023-11-13', 271 | sort='asc', 272 | ) 273 | ) 274 | 275 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 276 | await stats.daily_average_network_difficulty(start_date, end_date) 277 | mock.assert_called_once_with( 278 | params=dict( 279 | module='stats', 280 | action='dailyavgnetdifficulty', 281 | startdate='2023-11-12', 282 | enddate='2023-11-13', 283 | sort=None, 284 | ) 285 | ) 286 | 287 | with pytest.raises(ValueError): 288 | await stats.daily_average_network_difficulty(start_date, end_date, 'wrong') 289 | 290 | 291 | @pytest.mark.asyncio 292 | async def test_ether_historical_daily_market_cap(stats): 293 | start_date = date(2023, 11, 12) 294 | end_date = date(2023, 11, 13) 295 | 296 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 297 | await stats.ether_historical_daily_market_cap(start_date, end_date, 'asc') 298 | mock.assert_called_once_with( 299 | params=dict( 300 | module='stats', 301 | action='ethdailymarketcap', 302 | startdate='2023-11-12', 303 | enddate='2023-11-13', 304 | sort='asc', 305 | ) 306 | ) 307 | 308 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 309 | await stats.ether_historical_daily_market_cap(start_date, end_date) 310 | mock.assert_called_once_with( 311 | params=dict( 312 | module='stats', 313 | action='ethdailymarketcap', 314 | startdate='2023-11-12', 315 | enddate='2023-11-13', 316 | sort=None, 317 | ) 318 | ) 319 | 320 | with pytest.raises(ValueError): 321 | await stats.ether_historical_daily_market_cap(start_date, end_date, 'wrong') 322 | 323 | 324 | @pytest.mark.asyncio 325 | async def test_ether_historical_price(stats): 326 | start_date = date(2023, 11, 12) 327 | end_date = date(2023, 11, 13) 328 | 329 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 330 | await stats.ether_historical_price(start_date, end_date, 'asc') 331 | mock.assert_called_once_with( 332 | params=dict( 333 | module='stats', 334 | action='ethdailyprice', 335 | startdate='2023-11-12', 336 | enddate='2023-11-13', 337 | sort='asc', 338 | ) 339 | ) 340 | 341 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 342 | await stats.ether_historical_price(start_date, end_date) 343 | mock.assert_called_once_with( 344 | params=dict( 345 | module='stats', 346 | action='ethdailyprice', 347 | startdate='2023-11-12', 348 | enddate='2023-11-13', 349 | sort=None, 350 | ) 351 | ) 352 | 353 | with pytest.raises(ValueError): 354 | await stats.ether_historical_price(start_date, end_date, 'wrong') 355 | -------------------------------------------------------------------------------- /tests/test_token.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def token(): 11 | c = Client('TestApiKey') 12 | yield c.token 13 | await c.close() 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_total_supply(token): 18 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 19 | await token.total_supply('addr') 20 | mock.assert_called_once_with( 21 | params=dict(module='stats', action='tokensupply', contractaddress='addr') 22 | ) 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_account_balance(token): 27 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 28 | await token.account_balance('a1', 'c1') 29 | mock.assert_called_once_with( 30 | params=dict( 31 | module='account', 32 | action='tokenbalance', 33 | address='a1', 34 | contractaddress='c1', 35 | tag='latest', 36 | ) 37 | ) 38 | 39 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 40 | await token.account_balance('a1', 'c1', 123) 41 | mock.assert_called_once_with( 42 | params=dict( 43 | module='account', 44 | action='tokenbalance', 45 | address='a1', 46 | contractaddress='c1', 47 | tag='0x7b', 48 | ) 49 | ) 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_total_supply_by_blockno(token): 54 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 55 | await token.total_supply_by_blockno('c1', 123) 56 | mock.assert_called_once_with( 57 | params=dict( 58 | module='stats', action='tokensupplyhistory', contractaddress='c1', blockno=123 59 | ) 60 | ) 61 | 62 | 63 | @pytest.mark.asyncio 64 | async def test_account_balance_by_blockno(token): 65 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 66 | await token.account_balance_by_blockno('a1', 'c1', 123) 67 | mock.assert_called_once_with( 68 | params=dict( 69 | module='account', 70 | action='tokenbalancehistory', 71 | address='a1', 72 | contractaddress='c1', 73 | blockno=123, 74 | ) 75 | ) 76 | 77 | 78 | @pytest.mark.asyncio 79 | async def test_token_holder_list(token): 80 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 81 | await token.token_holder_list('c1') 82 | mock.assert_called_once_with( 83 | params=dict( 84 | module='token', 85 | action='tokenholderlist', 86 | contractaddress='c1', 87 | page=None, 88 | offset=None, 89 | ) 90 | ) 91 | 92 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 93 | await token.token_holder_list('c1', 1, 10) 94 | mock.assert_called_once_with( 95 | params=dict( 96 | module='token', action='tokenholderlist', contractaddress='c1', page=1, offset=10 97 | ) 98 | ) 99 | 100 | 101 | @pytest.mark.asyncio 102 | async def test_token_info(token): 103 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 104 | await token.token_info('c1') 105 | mock.assert_called_once_with( 106 | params=dict(module='token', action='tokeninfo', contractaddress='c1') 107 | ) 108 | 109 | 110 | @pytest.mark.asyncio 111 | async def test_token_holding_erc20(token): 112 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 113 | await token.token_holding_erc20('a1') 114 | mock.assert_called_once_with( 115 | params=dict( 116 | module='account', action='addresstokenbalance', address='a1', page=None, offset=None 117 | ) 118 | ) 119 | 120 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 121 | await token.token_holding_erc20('a1', 1, 10) 122 | mock.assert_called_once_with( 123 | params=dict( 124 | module='account', action='addresstokenbalance', address='a1', page=1, offset=10 125 | ) 126 | ) 127 | 128 | 129 | @pytest.mark.asyncio 130 | async def test_token_holding_erc721(token): 131 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 132 | await token.token_holding_erc721('a1') 133 | mock.assert_called_once_with( 134 | params=dict( 135 | module='account', 136 | action='addresstokennftbalance', 137 | address='a1', 138 | page=None, 139 | offset=None, 140 | ) 141 | ) 142 | 143 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 144 | await token.token_holding_erc721('a1', 1, 10) 145 | mock.assert_called_once_with( 146 | params=dict( 147 | module='account', action='addresstokennftbalance', address='a1', page=1, offset=10 148 | ) 149 | ) 150 | 151 | 152 | @pytest.mark.asyncio 153 | async def test_token_inventory(token): 154 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 155 | await token.token_inventory('a1', 'c1') 156 | mock.assert_called_once_with( 157 | params=dict( 158 | module='account', 159 | action='addresstokennftinventory', 160 | address='a1', 161 | contractaddress='c1', 162 | page=None, 163 | offset=None, 164 | ) 165 | ) 166 | 167 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 168 | await token.token_inventory('a1', 'c1', 1, 10) 169 | mock.assert_called_once_with( 170 | params=dict( 171 | module='account', 172 | action='addresstokennftinventory', 173 | address='a1', 174 | contractaddress='c1', 175 | page=1, 176 | offset=10, 177 | ) 178 | ) 179 | -------------------------------------------------------------------------------- /tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, AsyncMock 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan import Client 7 | 8 | 9 | @pytest_asyncio.fixture 10 | async def transaction(): 11 | c = Client('TestApiKey') 12 | yield c.transaction 13 | await c.close() 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_contract_execution_status(transaction): 18 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 19 | await transaction.contract_execution_status('0x123') 20 | mock.assert_called_once_with( 21 | params=dict(module='transaction', action='getstatus', txhash='0x123') 22 | ) 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_tx_receipt_status(transaction): 27 | with patch('aioetherscan.network.Network.get', new=AsyncMock()) as mock: 28 | await transaction.tx_receipt_status('0x123') 29 | mock.assert_called_once_with( 30 | params=dict(module='transaction', action='gettxreceiptstatus', txhash='0x123') 31 | ) 32 | -------------------------------------------------------------------------------- /tests/test_url_builder.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | import pytest_asyncio 5 | 6 | from aioetherscan.url_builder import UrlBuilder 7 | 8 | 9 | def apikey(): 10 | return 'test_api_key' 11 | 12 | 13 | @pytest_asyncio.fixture 14 | async def ub(): 15 | ub = UrlBuilder(apikey(), 'eth', 'main') 16 | yield ub 17 | 18 | 19 | def test_sign(ub): 20 | assert ub._sign({}) == {'apikey': ub._API_KEY} 21 | assert ub._sign({'something': 'something'}) == {'something': 'something', 'apikey': ub._API_KEY} 22 | 23 | 24 | def test_filter_params(ub): 25 | assert ub._filter_params({}) == {} 26 | assert ub._filter_params({1: 2, 3: None}) == {1: 2} 27 | assert ub._filter_params({1: 2, 3: 0}) == {1: 2, 3: 0} 28 | assert ub._filter_params({1: 2, 3: False}) == {1: 2, 3: False} 29 | 30 | 31 | @pytest.mark.parametrize( 32 | 'api_kind,network_name,expected', 33 | [ 34 | ('eth', 'main', 'https://api.etherscan.io/api'), 35 | ('eth', 'ropsten', 'https://api-ropsten.etherscan.io/api'), 36 | ('eth', 'kovan', 'https://api-kovan.etherscan.io/api'), 37 | ('eth', 'rinkeby', 'https://api-rinkeby.etherscan.io/api'), 38 | ('eth', 'goerli', 'https://api-goerli.etherscan.io/api'), 39 | ('eth', 'sepolia', 'https://api-sepolia.etherscan.io/api'), 40 | ('bsc', 'main', 'https://api.bscscan.com/api'), 41 | ('bsc', 'testnet', 'https://api-testnet.bscscan.com/api'), 42 | ('avax', 'main', 'https://api.snowtrace.io/api'), 43 | ('avax', 'testnet', 'https://api-testnet.snowtrace.io/api'), 44 | ('polygon', 'main', 'https://api.polygonscan.com/api'), 45 | ('polygon', 'testnet', 'https://api-testnet.polygonscan.com/api'), 46 | ('optimism', 'main', 'https://api-optimistic.etherscan.io/api'), 47 | ('optimism', 'goerli', 'https://api-goerli-optimistic.etherscan.io/api'), 48 | ('base', 'main', 'https://api.basescan.org/api'), 49 | ('base', 'sepolia', 'https://api-sepolia.basescan.org/api'), 50 | ('base', 'goerli', 'https://api-goerli.basescan.org/api'), 51 | ('arbitrum', 'main', 'https://api.arbiscan.io/api'), 52 | ('arbitrum', 'nova', 'https://api-nova.arbiscan.io/api'), 53 | ('arbitrum', 'goerli', 'https://api-goerli.arbiscan.io/api'), 54 | ('fantom', 'main', 'https://api.ftmscan.com/api'), 55 | ('fantom', 'testnet', 'https://api-testnet.ftmscan.com/api'), 56 | ('taiko', 'main', 'https://api.taikoscan.io/api'), 57 | ('taiko', 'hekla', 'https://api-hekla.taikoscan.io/api'), 58 | ('snowscan', 'main', 'https://api.snowscan.xyz/api'), 59 | ('snowscan', 'testnet', 'https://api-testnet.snowscan.xyz/api'), 60 | ], 61 | ) 62 | def test_api_url(api_kind, network_name, expected): 63 | ub = UrlBuilder(apikey(), api_kind, network_name) 64 | assert ub.API_URL == expected 65 | 66 | 67 | @pytest.mark.parametrize( 68 | 'api_kind,network_name,expected', 69 | [ 70 | ('eth', 'main', 'https://etherscan.io'), 71 | ('eth', 'ropsten', 'https://ropsten.etherscan.io'), 72 | ('eth', 'kovan', 'https://kovan.etherscan.io'), 73 | ('eth', 'rinkeby', 'https://rinkeby.etherscan.io'), 74 | ('eth', 'goerli', 'https://goerli.etherscan.io'), 75 | ('eth', 'sepolia', 'https://sepolia.etherscan.io'), 76 | ('bsc', 'main', 'https://bscscan.com'), 77 | ('bsc', 'testnet', 'https://testnet.bscscan.com'), 78 | ('avax', 'main', 'https://snowtrace.io'), 79 | ('avax', 'testnet', 'https://testnet.snowtrace.io'), 80 | ('polygon', 'main', 'https://polygonscan.com'), 81 | ('polygon', 'testnet', 'https://mumbai.polygonscan.com'), 82 | ('optimism', 'main', 'https://optimistic.etherscan.io'), 83 | ('optimism', 'goerli', 'https://goerli-optimism.etherscan.io'), 84 | ('base', 'main', 'https://basescan.org'), 85 | ('base', 'sepolia', 'https://sepolia.basescan.org'), 86 | ('base', 'goerli', 'https://goerli.basescan.org'), 87 | ('arbitrum', 'main', 'https://arbiscan.io'), 88 | ('arbitrum', 'nova', 'https://nova.arbiscan.io'), 89 | ('arbitrum', 'goerli', 'https://goerli.arbiscan.io'), 90 | ('fantom', 'main', 'https://ftmscan.com'), 91 | ('fantom', 'testnet', 'https://testnet.ftmscan.com'), 92 | ('taiko', 'main', 'https://taikoscan.io'), 93 | ('taiko', 'hekla', 'https://hekla.taikoscan.io'), 94 | ('snowscan', 'main', 'https://snowscan.xyz'), 95 | ('snowscan', 'testnet', 'https://testnet.snowscan.xyz'), 96 | ], 97 | ) 98 | def test_base_url(api_kind, network_name, expected): 99 | ub = UrlBuilder(apikey(), api_kind, network_name) 100 | assert ub.BASE_URL == expected 101 | 102 | 103 | def test_invalid_api_kind(): 104 | with pytest.raises(ValueError) as exception: 105 | UrlBuilder(apikey(), 'wrong', 'main') 106 | assert 'Incorrect api_kind' in str(exception.value) 107 | 108 | 109 | @pytest.mark.parametrize( 110 | 'api_kind,expected', 111 | [ 112 | ('eth', 'ETH'), 113 | ('bsc', 'BNB'), 114 | ('avax', 'AVAX'), 115 | ('polygon', 'MATIC'), 116 | ('optimism', 'ETH'), 117 | ('base', 'ETH'), 118 | ('arbitrum', 'ETH'), 119 | ('fantom', 'FTM'), 120 | ('taiko', 'ETH'), 121 | ], 122 | ) 123 | def test_currency(api_kind, expected): 124 | ub = UrlBuilder(apikey(), api_kind, 'main') 125 | assert ub.currency == expected 126 | 127 | 128 | def test_get_link(ub): 129 | with patch('aioetherscan.url_builder.urljoin') as join_mock: 130 | path = 'some_path' 131 | ub.get_link(path) 132 | join_mock.assert_called_once_with(ub.BASE_URL, path) 133 | --------------------------------------------------------------------------------