├── CHANGELOG.md ├── tests ├── __init__.py ├── conftest.py ├── integration │ ├── test_alchemy_setup.py │ ├── test_evm_node.py │ └── test_alchemy_core.py └── test_data.py ├── requirements-dev.txt ├── sample-alchemy.env ├── alchemy_sdk_py ├── __init__.py ├── __version__.py ├── errors.py ├── networks.py ├── utils.py ├── evm_node.py └── alchemy.py ├── img └── logo.png ├── setup.cfg ├── pyproject.toml ├── requirements.txt ├── .github └── workflows │ └── pypi-release.yml ├── LICENSE ├── CONTRIBUTING.md ├── setup.py ├── .gitignore └── README.md /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | coverage -------------------------------------------------------------------------------- /sample-alchemy.env: -------------------------------------------------------------------------------- 1 | ALCHEMY_API_KEY="demo" -------------------------------------------------------------------------------- /alchemy_sdk_py/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .alchemy import Alchemy 3 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyfrin/alchemy_sdk_py/HEAD/img/logo.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | description-file = README.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi>=2022.12.7 2 | charset-normalizer~=2.1.1 3 | idna~=3.4 4 | requests~=2.28.1 5 | urllib3~=1.26.13 6 | python-dotenv~=0.21.0 -------------------------------------------------------------------------------- /alchemy_sdk_py/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = "alchemy_sdk_py" 2 | __description__ = "Python SDK for working with the Alchemy API." 3 | __version__ = "0.2.2" 4 | __author__ = "Cyfrin" 5 | __license__ = "MIT" 6 | __copyright__ = "Copyright 2023 Cyfrin" 7 | -------------------------------------------------------------------------------- /alchemy_sdk_py/errors.py: -------------------------------------------------------------------------------- 1 | # Don't import anything into this file otherwise you'll do a circule import 2 | 3 | NO_API_KEY_ERROR: str = ( 4 | "A valid Alchemy API key must be provided " 5 | "either through the key parameter or " 6 | "through the environment variable " 7 | '"ALCHEMY_API_KEY". Get a free key ' 8 | "from the alchemy website: " 9 | "https://alchemy.com/?a=673c802981" 10 | ) 11 | 12 | 13 | def NETWORK_INITIALIZATION_ERROR(network_id_map): 14 | str = ( 15 | "Network has been given a poor name or chain ID. " 16 | f"Please use one of the following options: {network_id_map.keys()}" 17 | ) 18 | return str 19 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from alchemy_sdk_py import Alchemy 3 | from _pytest.monkeypatch import MonkeyPatch 4 | 5 | 6 | @pytest.fixture 7 | def dummy_api_key() -> str: 8 | return "Hello" 9 | 10 | 11 | @pytest.fixture 12 | def alchemy(dummy_api_key: str) -> Alchemy: 13 | return Alchemy(dummy_api_key) 14 | 15 | 16 | @pytest.fixture 17 | def alchemy_with_key() -> Alchemy: 18 | # Be sure to use an environment variable called ALCHEMY_API_KEY 19 | return Alchemy() 20 | 21 | 22 | @pytest.fixture 23 | def mock_env_missing(monkeypatch: MonkeyPatch): 24 | """A plugin from pytest to help safely mock and delete environment variables. 25 | 26 | Args: 27 | monkeypatch (_pytest.monkeypatch.MonkeyPatch): _description_ 28 | """ 29 | monkeypatch.delenv("ALCHEMY_API_KEY", raising=False) 30 | -------------------------------------------------------------------------------- /.github/workflows/pypi-release.yml: -------------------------------------------------------------------------------- 1 | name: pypi-release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: "3.x" 16 | - name: Install dependencies 17 | run: | 18 | python3 -m pip install --upgrade build 19 | python3 -m pip install --upgrade pip 20 | python3 -m pip install --upgrade twine 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | python3 -m build 27 | python3 -m twine upload dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alpha Chain 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 | -------------------------------------------------------------------------------- /tests/integration/test_alchemy_setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from alchemy_sdk_py import Alchemy 4 | from _pytest.monkeypatch import MonkeyPatch 5 | 6 | 7 | def test_initialize_empty_network(dummy_api_key, mock_env_missing): 8 | alchemy = Alchemy(api_key=dummy_api_key) 9 | assert alchemy.network == "eth_mainnet" 10 | 11 | 12 | def test_initialize_network_by_name(dummy_api_key): 13 | alchemy = Alchemy(api_key=dummy_api_key, network="eth_ropsten") 14 | assert alchemy.network == "eth_ropsten" 15 | 16 | 17 | def test_initialize_network_by_str_chain_id(dummy_api_key): 18 | alchemy = Alchemy(api_key=dummy_api_key, network="5") 19 | assert alchemy.network == "eth_goerli" 20 | 21 | 22 | def test_initialize_network_by_int_chain_id(dummy_api_key): 23 | alchemy = Alchemy(api_key=dummy_api_key, network=5) 24 | assert alchemy.network == "eth_goerli" 25 | 26 | 27 | def test_initialize_network_by_bad_network(dummy_api_key): 28 | with pytest.raises(ValueError): 29 | _ = Alchemy(api_key=dummy_api_key, network=2) 30 | 31 | 32 | def test_initialize_alchemy_using_key_instead_of_api_key(dummy_api_key): 33 | alchemy = Alchemy(key=dummy_api_key, network=5) 34 | assert alchemy.api_key == dummy_api_key 35 | 36 | 37 | def test_api_key_property(dummy_api_key): 38 | alchemy = Alchemy(api_key=dummy_api_key, network=5) 39 | assert alchemy.key == dummy_api_key 40 | 41 | 42 | def test_key_with_environment_variable(monkeypatch: MonkeyPatch): 43 | test_key = "test_key" 44 | monkeypatch.setenv("ALCHEMY_API_KEY", test_key) 45 | alchemy = Alchemy() 46 | assert alchemy.key == test_key 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | To get started contributing to this project, you'll first need to set up your development environment. 2 | 3 | ``` 4 | git clone https://github.com/alphachainio/alchemy_sdk_py 5 | cd alchemy_sdk_py 6 | 7 | python3 -m venv venv 8 | source venv/bin/activate 9 | ``` 10 | 11 | We set up a virtual environment so that packages you install get installed to an isolated location (the `venv` folder we just created). If you want to exit this virtual environment, you can run `deactivate`. 12 | 13 | Then, install the development dependencies. 14 | 15 | ``` 16 | pip install -r requirements.txt 17 | pip install -r requirements-dev.txt 18 | ``` 19 | 20 | ### Optional 21 | 22 | You can also install the package in editable mode. 23 | 24 | ``` 25 | pip install -e . 26 | ``` 27 | 28 | The `pip install -e .` command installs our package in "editable" mode. This means that any changes you make to the code will be reflected in the package you import in your own code. 29 | 30 | This would be if you want to run make changes and test them out on your own code in another project. 31 | 32 | 33 | # Testing 34 | 35 | Then, run the tests to make sure everything is working: 36 | 37 | ``` 38 | pytest 39 | ``` 40 | 41 | ## Coverage 42 | 43 | To run coverage, run: 44 | 45 | ``` 46 | coverage run -m pytest 47 | ``` 48 | 49 | # Uploading to PyPI 50 | 51 | _For maintainers only. You can view the [docs](https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives) to learn more._ 52 | 53 | _Note: `setup.py sdist` is deprecated. Use `python3 -m build` instead._ 54 | 55 | ``` 56 | python3 -m build 57 | python3 -m twine upload dist/* 58 | ``` 59 | 60 | Right now, we have our GitHub actions setup so that every release we push we automatically upload to PyPI. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | import sys 4 | 5 | CURRENT_PYTHON = sys.version_info[:2] 6 | REQUIRED_PYTHON = (3, 7) 7 | 8 | if CURRENT_PYTHON < REQUIRED_PYTHON: 9 | sys.stderr.write( 10 | """ 11 | ========================== 12 | Unsupported Python version 13 | ========================== 14 | This version of Requests requires at least Python {}.{}, but 15 | you're trying to install it on Python {}.{}. To resolve this, 16 | consider upgrading to a supported Python version. 17 | """.format( 18 | *(REQUIRED_PYTHON + CURRENT_PYTHON) 19 | ) 20 | ) 21 | sys.exit(1) 22 | 23 | 24 | here = os.path.abspath(os.path.dirname(__file__)) 25 | with open(os.path.join(here, "README.md"), "r") as f: 26 | readme = f.read() 27 | 28 | about = {} 29 | with open(os.path.join(here, "alchemy_sdk_py", "__version__.py"), "r") as f: 30 | exec(f.read(), about) 31 | 32 | setup( 33 | name=about["__title__"], 34 | version=about["__version__"], 35 | author=about["__author__"], 36 | license=about["__license__"], 37 | install_requires=[ 38 | "certifi", 39 | "charset-normalizer", 40 | "idna", 41 | "requests", 42 | "urllib3", 43 | ], 44 | packages=[about["__title__"]], 45 | python_requires=">=3.7, <4", 46 | url="https://github.com/alphachainio/alchemy_sdk_py", 47 | long_description=readme, 48 | long_description_content_type="text/markdown", 49 | classifiers=[ 50 | "Development Status :: 2 - Pre-Alpha", 51 | "Intended Audience :: Developers", 52 | "License :: OSI Approved :: MIT License", 53 | "Programming Language :: Python :: 3.7", 54 | "Programming Language :: Python :: 3.8", 55 | "Programming Language :: Python :: 3.9", 56 | "Programming Language :: Python :: 3.10", 57 | ], 58 | ) 59 | -------------------------------------------------------------------------------- /alchemy_sdk_py/networks.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from .errors import NETWORK_INITIALIZATION_ERROR 3 | 4 | network_id_map = { 5 | "eth_mainnet": "1", 6 | "eth_ropsten": "3", 7 | "eth_rinkeby": "4", 8 | "eth_goerli": "5", 9 | "eth_kovan": "42", 10 | "opt_mainnet": "10", 11 | "opt_goerli": "420", 12 | "arb_mainnet": "42161", 13 | "arb_rinkeby": "421611", 14 | "matic_mainnet": "137", 15 | "matic_mumbai": "80001", 16 | "astar_mainnet": "592", 17 | "1": "eth_mainnet", 18 | "3": "eth_ropsten", 19 | "4": "eth_rinkeby", 20 | "5": "eth_goerli", 21 | "42": "eth_kovan", 22 | "10": "opt_mainnet", 23 | "420": "opt_goerli", 24 | "42161": "arb_mainnet", 25 | "421611": "arb_rinkeby", 26 | "137": "matic_mainnet", 27 | "80001": "matic_mumbai", 28 | "592": "astar_mainnet", 29 | } 30 | 31 | 32 | class Network: 33 | def __init__(self, name_or_chain_id: Union[str, int, None] = "eth_mainnet"): 34 | """Creates an instance of a Network class, which is an easy way to access the chain ID and name of a network. 35 | 36 | Args: 37 | name_or_chain_id (Union[str, int, None], optional): This can be one of the following: 38 | - A chain name, ie: "eth_mainnet" 39 | - A chain ID, ie: 1 40 | - A hex chain ID, ie: "0x1" 41 | - A chain ID as a string, ie: "1" 42 | - None, which goes to the default 43 | 44 | Defaults to "eth_mainnet". 45 | 46 | Raises: 47 | ValueError: If the network name or chain ID is not valid. 48 | """ 49 | name_or_chain_id = str(name_or_chain_id) 50 | if name_or_chain_id.startswith("0x"): 51 | name_or_chain_id = str(int(name_or_chain_id, 16)) 52 | if name_or_chain_id not in network_id_map: 53 | raise ValueError(NETWORK_INITIALIZATION_ERROR(network_id_map)) 54 | else: 55 | if name_or_chain_id.isdigit(): 56 | self.chain_id = name_or_chain_id 57 | self.name = network_id_map[name_or_chain_id] 58 | else: 59 | self.name = name_or_chain_id 60 | self.chain_id = network_id_map[name_or_chain_id] 61 | 62 | def __eq__(self, other: Union[str, int]): 63 | other = str(other) 64 | return self.name == other or self.chain_id == other 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /alchemy_sdk_py/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | ETH_NULL_VALUE: str = "0x" 4 | 5 | 6 | def is_hash(string: str) -> bool: 7 | """ 8 | params: 9 | string: String to check 10 | returns: 11 | True if string is a hash, False otherwise 12 | """ 13 | if not type(string) == str: 14 | return False 15 | if not string.startswith("0x"): 16 | return False 17 | if not len(string) == 66: 18 | return False 19 | return True 20 | 21 | 22 | def is_hex_int(string: str) -> bool: 23 | """ 24 | params: 25 | string: String to check 26 | returns: 27 | True if string is a hex int, False otherwise 28 | """ 29 | if not type(string) == str: 30 | return False 31 | try: 32 | int(string, 16) 33 | return True 34 | except ValueError: 35 | return False 36 | 37 | 38 | def bytes32_to_text(bytes_to_convert: str) -> str: 39 | """ 40 | params: 41 | string: String to convert 42 | returns: 43 | String converted to text 44 | """ 45 | if not isinstance(bytes_to_convert, str): 46 | raise TypeError("string must be a string") 47 | bytes_object = bytes.fromhex(bytes_to_convert[2:]) # Strip the "0x" prefix 48 | null_byte_index = bytes_object.index(b"\x00") # Find the null byte 49 | bytes_object = bytes_object[:null_byte_index] # Strip the null byte 50 | decoded_string = bytes_object.decode() # Decode the bytes 51 | return decoded_string 52 | 53 | 54 | class HexIntStringNumber: 55 | def __init__(self, stringIntNumber: Union[str, int, None]): 56 | self.hex_string = ( 57 | hex(stringIntNumber) if not is_hex_int(stringIntNumber) else stringIntNumber 58 | ) 59 | self.int: int = int(self.hex_string, 16) 60 | self.int_string: str = str(self.int) 61 | 62 | def __str__(self) -> str: 63 | return self.int_string 64 | 65 | def __eq__(self, other: Union[str, int, any, None]) -> bool: 66 | if isinstance(other, HexIntStringNumber): 67 | return self.int == other.int 68 | else: 69 | HexIntStringNumber(other).int == self.int 70 | 71 | @property 72 | def hex(self) -> str: 73 | return self.hex_string 74 | 75 | @property 76 | def int_number(self) -> int: 77 | return self.int 78 | 79 | @property 80 | def str(self) -> str: 81 | return self.int_string 82 | 83 | @property 84 | def hexString(self) -> str: 85 | return self.hex_string 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alchemy_sdk_py (Beta) 2 | An SDK to use the [Alchemy API](https://www.alchemy.com/) 3 | 4 | 5 |
6 |

7 | 8 | Alchemy logo 9 | 10 |

11 |
12 | 13 | - [alchemy\_sdk\_py (Pre-Alpha)](#alchemy_sdk_py-pre-alpha) 14 | - [Getting Started](#getting-started) 15 | - [Requirements](#requirements) 16 | - [Installation](#installation) 17 | - [Quickstart](#quickstart) 18 | - [Get an API Key](#get-an-api-key) 19 | - [Useage](#useage) 20 | - [Get all ERC20, value, and NFT transfers for an address](#get-all-erc20-value-and-nft-transfers-for-an-address) 21 | - [Get contract metadata for any NFT](#get-contract-metadata-for-any-nft) 22 | - [What's here and what's not](#whats-here-and-whats-not) 23 | - [What this currently has](#what-this-currently-has) 24 | - [Currently not implemented](#currently-not-implemented) 25 | 26 | 27 | # Getting Started 28 | 29 | ## Requirements 30 | 31 | - [Python](https://www.python.org/downloads/) 3.7 or higher 32 | - You'll know you've done it right if you can run `python3 --version` in your terminal and see something like `Python 3.10.6` 33 | 34 | ## Installation 35 | 36 | ```bash 37 | pip3 install alchemy_sdk_py 38 | ``` 39 | 40 | ## Quickstart 41 | 42 | ### Get an API Key 43 | After [installing](#installation), you'll need to sign up for an API key and set it as an `ALCHEMY_API_KEY` environment variable. You can place them in a `.env` file if you like *just please don't push the `.env` file to GitHub*. 44 | 45 | `.env` 46 | ```bash 47 | ALCHEMY_API_KEY="asdfasfsfasf 48 | ``` 49 | 50 | If you're unfamiliar with environment variables, you can use the API to set the key directly using the SDK - please don't do this in production code. 51 | 52 | ```python 53 | from alchemy_sdk_py import Alchemy 54 | 55 | alchemy = Alchemy(api_key="asdfasfsfasf", network="eth_mainnet") 56 | ``` 57 | If you have your environment variable set, and you want to use eth mainnet, you can just do this: 58 | 59 | ```python 60 | from alchemy_sdk_py import Alchemy 61 | alchemy = Alchemy() 62 | ``` 63 | 64 | You can also set the network ID using the chainId, or hex, and even update it later. 65 | ```python 66 | # For Goerli ETH 67 | alchemy = Alchemy(network=5) 68 | # For Polygon ("0x89" is hex for 137) 69 | alchemy.set_network("0x89") 70 | ``` 71 | 72 | # Useage 73 | 74 | ```python 75 | from alchemy_sdk_py import Alchemy 76 | 77 | alchemy = Alchemy() 78 | 79 | current_block_number = alchemy.get_current_block_number() 80 | print(current_block_number) 81 | # prints the current block number 82 | ``` 83 | 84 | With web3.py 85 | 86 | ```python 87 | from alchemy_sdk_py import Alchemy 88 | from web3 import Web3 89 | 90 | alchemy = Alchemy() 91 | 92 | w3 = Web3(Web3.HTTPProvider(alchemy.base_url)) 93 | ``` 94 | 95 | ## Get all ERC20, value, and NFT transfers for an address 96 | 97 | The following code will get you every transfer in and out of a single wallet address. 98 | 99 | ```python 100 | from alchemy_sdk_py import Alchemy 101 | alchemy = Alchemy() 102 | 103 | address = "YOUR_ADDRESS_HERE" 104 | 105 | transfers, page_key = alchemy.get_asset_transfers(from_address=address) 106 | print(transfers) 107 | # prints every transfer in or out that's ever happened on the address 108 | ``` 109 | 110 | ## Get contract metadata for any NFT 111 | 112 | ```python 113 | ENS = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" 114 | contract_metadata = alchemy.get_contract_metadata(ENS) 115 | 116 | print(contract_metadata["contractMetadata"]["openSea"]["collectionName"]) 117 | # prints "ENS: Ethereum Name Service" 118 | ``` 119 | 120 | # What's here and what's not 121 | 122 | ## What this currently has 123 | 124 | Just about everything in the [Alchemy SDK](https://docs.alchemy.com/reference/alchemy-sdk-quickstart) section of the docs. 125 | 126 | ## Currently not implemented 127 | 128 | - [ ] `batchRequests` 129 | - [ ] `web sockets` 130 | - [ ] `Notify API` & `filters` ie `eth_newFilter` 131 | - [ ] `Async support` 132 | - [ ] ENS Support for addresses 133 | - [ ] Double check the NFT, Transact, and Token docs for function 134 | - [ ] Trace API 135 | - [ ] Debug API 136 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | CHAINLINK_CODE = "0x606060405236156100b75763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166306fdde0381146100bc578063095ea7b31461014757806318160ddd1461017d57806323b872dd146101a2578063313ce567146101de5780634000aea014610207578063661884631461028057806370a08231146102b657806395d89b41146102e7578063a9059cbb14610372578063d73dd623146103a8578063dd62ed3e146103de575b600080fd5b34156100c757600080fd5b6100cf610415565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561015257600080fd5b610169600160a060020a036004351660243561044c565b604051901515815260200160405180910390f35b341561018857600080fd5b610190610499565b60405190815260200160405180910390f35b34156101ad57600080fd5b610169600160a060020a03600435811690602435166044356104a9565b604051901515815260200160405180910390f35b34156101e957600080fd5b6101f16104f8565b60405160ff909116815260200160405180910390f35b341561021257600080fd5b61016960048035600160a060020a03169060248035919060649060443590810190830135806020601f820181900481020160405190810160405281815292919060208401838380828437509496506104fd95505050505050565b604051901515815260200160405180910390f35b341561028b57600080fd5b610169600160a060020a036004351660243561054c565b604051901515815260200160405180910390f35b34156102c157600080fd5b610190600160a060020a0360043516610648565b60405190815260200160405180910390f35b34156102f257600080fd5b6100cf610667565b60405160208082528190810183818151815260200191508051906020019080838360005b8381101561010c5780820151818401525b6020016100f3565b50505050905090810190601f1680156101395780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561037d57600080fd5b610169600160a060020a036004351660243561069e565b604051901515815260200160405180910390f35b34156103b357600080fd5b610169600160a060020a03600435166024356106eb565b604051901515815260200160405180910390f35b34156103e957600080fd5b610190600160a060020a0360043581169060243516610790565b60405190815260200160405180910390f35b60408051908101604052600f81527f436861696e4c696e6b20546f6b656e0000000000000000000000000000000000602082015281565b600082600160a060020a03811615801590610479575030600160a060020a031681600160a060020a031614155b151561048457600080fd5b61048e84846107bd565b91505b5b5092915050565b6b033b2e3c9fd0803ce800000081565b600082600160a060020a038116158015906104d6575030600160a060020a031681600160a060020a031614155b15156104e157600080fd5b6104ec85858561082a565b91505b5b509392505050565b601281565b600083600160a060020a0381161580159061052a575030600160a060020a031681600160a060020a031614155b151561053557600080fd5b6104ec85858561093c565b91505b5b509392505050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054808311156105a957600160a060020a0333811660009081526002602090815260408083209388168352929052908120556105e0565b6105b9818463ffffffff610a2316565b600160a060020a033381166000908152600260209081526040808320938916835292905220555b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020547f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925915190815260200160405180910390a3600191505b5092915050565b600160a060020a0381166000908152600160205260409020545b919050565b60408051908101604052600481527f4c494e4b00000000000000000000000000000000000000000000000000000000602082015281565b600082600160a060020a038116158015906106cb575030600160a060020a031681600160a060020a031614155b15156106d657600080fd5b61048e8484610a3a565b91505b5b5092915050565b600160a060020a033381166000908152600260209081526040808320938616835292905290812054610723908363ffffffff610afa16565b600160a060020a0333811660008181526002602090815260408083209489168084529490915290819020849055919290917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591905190815260200160405180910390a35060015b92915050565b600160a060020a038083166000908152600260209081526040808320938516835292905220545b92915050565b600160a060020a03338116600081815260026020908152604080832094871680845294909152808220859055909291907f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259085905190815260200160405180910390a35060015b92915050565b600160a060020a03808416600081815260026020908152604080832033909516835293815283822054928252600190529182205461086e908463ffffffff610a2316565b600160a060020a0380871660009081526001602052604080822093909355908616815220546108a3908463ffffffff610afa16565b600160a060020a0385166000908152600160205260409020556108cc818463ffffffff610a2316565b600160a060020a03808716600081815260026020908152604080832033861684529091529081902093909355908616917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9086905190815260200160405180910390a3600191505b509392505050565b60006109488484610a3a565b5083600160a060020a031633600160a060020a03167fe19260aff97b920c7df27010903aeb9c8d2be5d310a2c67824cf3f15396e4c16858560405182815260406020820181815290820183818151815260200191508051906020019080838360005b838110156109c35780820151818401525b6020016109aa565b50505050905090810190601f1680156109f05780820380516001836020036101000a031916815260200191505b50935050505060405180910390a3610a0784610b14565b15610a1757610a17848484610b23565b5b5060015b9392505050565b600082821115610a2f57fe5b508082035b92915050565b600160a060020a033316600090815260016020526040812054610a63908363ffffffff610a2316565b600160a060020a033381166000908152600160205260408082209390935590851681522054610a98908363ffffffff610afa16565b600160a060020a0380851660008181526001602052604090819020939093559133909116907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9085905190815260200160405180910390a35060015b92915050565b600082820183811015610b0957fe5b8091505b5092915050565b6000813b908111905b50919050565b82600160a060020a03811663a4c0ed363385856040518463ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018084600160a060020a0316600160a060020a0316815260200183815260200180602001828103825283818151815260200191508051906020019080838360005b83811015610bbd5780820151818401525b602001610ba4565b50505050905090810190601f168015610bea5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b1515610c0a57600080fd5b6102c65a03f11515610c1b57600080fd5b5050505b505050505600a165627a7a72305820c5f438ff94e5ddaf2058efa0019e246c636c37a622e04bb67827c7374acad8d60029" 2 | CHAINLINK_ADDRESS = "0x514910771AF9Ca656af840dff83E8264EcF986CA" 3 | CHAINLINK_CREATOR = "0xf55037738604FDDFC4043D12F25124E94D7D1780" 4 | # Just some testing data... 5 | PATRICK_ALPHA_C = "0x874437B5a42aA6E6419eC2447C9e36c278c46532" 6 | VITALIK = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" 7 | GAS = "0x76c0" 8 | GAS_PRICE = "0x9184e72a000" 9 | VALUE = 0 10 | DATA = "0x3b3b57debf074faa138b72c65adbdcfb329847e4f2c04bde7f7dd7fcad5a52d2f395a558" 11 | TAG = "latest" 12 | WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 13 | TX_HASH = "0x1ad11558e5bdfb59aae212849ceebcc90670e28e75143c4b5726ce9f3b619358" 14 | ENS = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85" 15 | PATRICK_ALPHA_C_TOKEN_ID_ENS = ( 16 | 106007717588688686207498772197209154629895902187952590615369907092501012786736 17 | ) 18 | ETH_BLOCKS = "0x01234567bac6ff94d7e4f0ee23119cf848f93245" 19 | -------------------------------------------------------------------------------- /tests/integration/test_evm_node.py: -------------------------------------------------------------------------------- 1 | from tests.test_data import ( 2 | CHAINLINK_CODE, 3 | CHAINLINK_ADDRESS, 4 | PATRICK_ALPHA_C, 5 | VITALIK, 6 | GAS, 7 | GAS_PRICE, 8 | VALUE, 9 | DATA, 10 | TAG, 11 | TX_HASH, 12 | WETH_ADDRESS, 13 | ) 14 | from alchemy_sdk_py.utils import bytes32_to_text, HexIntStringNumber 15 | 16 | 17 | def test_call(alchemy_with_key): 18 | response = alchemy_with_key.call( 19 | PATRICK_ALPHA_C, 20 | VITALIK, 21 | GAS, 22 | GAS_PRICE, 23 | VALUE, 24 | DATA, 25 | ) 26 | assert response == "0x" 27 | 28 | 29 | def test_estimate_gas(alchemy_with_key): 30 | response = alchemy_with_key.estimate_gas( 31 | PATRICK_ALPHA_C, 32 | VITALIK, 33 | GAS, 34 | GAS_PRICE, 35 | VALUE, 36 | DATA, 37 | ) 38 | assert response == "0x5448" 39 | 40 | 41 | # def test_find_contract_deployer(alchemy_with_key): 42 | # response = alchemy_with_key.find_contract_deployer(CHAINLINK) 43 | # assert response == CHAINLINK_CREATOR 44 | 45 | 46 | def test_get_balance(alchemy_with_key): 47 | response = alchemy_with_key.get_balance(PATRICK_ALPHA_C, TAG) 48 | # Only true if Patrick's ass ain't broke lol 49 | assert response > 0 50 | 51 | 52 | def test_get_code(alchemy_with_key): 53 | response = alchemy_with_key.get_code(CHAINLINK_ADDRESS, TAG) 54 | assert response == CHAINLINK_CODE 55 | 56 | 57 | def test_get_transaction_count(alchemy_with_key): 58 | response = alchemy_with_key.get_transaction_count(PATRICK_ALPHA_C, TAG) 59 | assert response > 0 60 | 61 | 62 | def test_get_storage_at(alchemy_with_key): 63 | # Arrange 64 | expected_response_string = "Wrapped Ether" 65 | 66 | # Act 67 | response = alchemy_with_key.get_storage_at(WETH_ADDRESS, 0, TAG) 68 | decoded_string = bytes32_to_text(response) 69 | assert decoded_string == expected_response_string 70 | 71 | 72 | def test_get_block_transaction_count_by_hash(alchemy_with_key): 73 | # Arrange 74 | expected = 305 75 | # Act 76 | response = alchemy_with_key.get_block_transaction_count_by_hash(TX_HASH) 77 | # Assert 78 | assert response == expected 79 | 80 | 81 | def test_get_block_transaction_count_by_number(alchemy_with_key): 82 | # Arrange 83 | number = 16235426 84 | expected = 305 85 | # Act 86 | response = alchemy_with_key.get_block_transaction_count_by_number(number) 87 | # Assert 88 | assert response == expected 89 | 90 | 91 | def test_get_uncle_count_by_blockhash(alchemy_with_key): 92 | # Arrange 93 | expected = 0 94 | response = alchemy_with_key.get_uncle_count_by_blockhash(TX_HASH) 95 | assert expected == response 96 | 97 | 98 | def test_get_uncle_count_by_block_number(alchemy_with_key): 99 | # Arrange 100 | expected = 0 101 | response = alchemy_with_key.get_uncle_count_by_block_number(16235426) 102 | assert expected == response 103 | 104 | 105 | def test_get_block_by_hash(alchemy_with_key): 106 | hash = "0x50f4aaf5aa0e7f2be6766c406e542a42bc980b14f85500ee14f4873cb20d411c" 107 | full_tx = False 108 | expected_miner = "0x199d5ed7f45f4ee35960cf22eade2076e95b253f" 109 | response = alchemy_with_key.get_block_by_hash(hash, full_tx) 110 | assert response["miner"] == expected_miner 111 | 112 | 113 | def test_get_block_by_number(alchemy_with_key): 114 | number = 16292589 115 | full_tx = False 116 | expected_miner = "0x199d5ed7f45f4ee35960cf22eade2076e95b253f" 117 | response = alchemy_with_key.get_block_by_number(number, full_tx) 118 | assert response["miner"] == expected_miner 119 | 120 | 121 | def test_get_transaction_by_hash(alchemy_with_key): 122 | hash = "0x7ac79af930a26f05ef3ae4b3f9e38cb7323696232aea00e3d3e04394ab1c7234" 123 | response = alchemy_with_key.get_transaction_by_hash(hash) 124 | assert response["from"].lower() == PATRICK_ALPHA_C.lower() 125 | 126 | 127 | def test_get_transaction_by_block_hash_and_index(alchemy_with_key): 128 | hash = "0x50f4aaf5aa0e7f2be6766c406e542a42bc980b14f85500ee14f4873cb20d411c" 129 | index = 0 130 | expected_from = "0x6b2d93fc921a14928069f7f013addec1f61e329c" 131 | expected_to = "0x45511c17e28395d445b2992efff08ee65fe25146" 132 | response = alchemy_with_key.get_transaction_by_block_hash_and_index(hash, index) 133 | assert response["from"] == expected_from 134 | assert response["to"] == expected_to 135 | 136 | 137 | def test_get_transaction_by_block_number_and_index(alchemy_with_key): 138 | number = 16292589 139 | index = 0 140 | expected_from = "0x6b2d93fc921a14928069f7f013addec1f61e329c" 141 | expected_to = "0x45511c17e28395d445b2992efff08ee65fe25146" 142 | response = alchemy_with_key.get_transaction_by_block_number_and_index(number, index) 143 | assert response["from"] == expected_from 144 | assert response["to"] == expected_to 145 | 146 | 147 | def test_get_uncle_by_block_hash_and_index(alchemy_with_key): 148 | hash = "0xd6940190d24aa1c2e8aa70fb2847aba6c4461679753a7546daf79e6295a9e1e2" 149 | expected_miner = "0xea674fdde714fd979de3edf0f56aa9716b898ec8" 150 | expected_number = "0x1506f2" 151 | index = 0 152 | response = alchemy_with_key.get_uncle_by_block_hash_and_index(hash, index) 153 | assert response["miner"] == expected_miner 154 | assert response["number"] == expected_number 155 | 156 | 157 | def test_get_uncle_by_block_number_and_index(alchemy_with_key): 158 | number = 1378035 159 | expected_miner = "0xea674fdde714fd979de3edf0f56aa9716b898ec8" 160 | expected_number = "0x1506f2" 161 | index = 0 162 | response = alchemy_with_key.get_uncle_by_block_number_and_index(number, index) 163 | assert response["miner"] == expected_miner 164 | assert response["number"] == expected_number 165 | 166 | 167 | def test_client_version(alchemy_with_key): 168 | response = alchemy_with_key.client_version() 169 | assert response is not "" 170 | assert response is not None 171 | 172 | 173 | def test_sha(alchemy_with_key): 174 | response = alchemy_with_key.sha("hi") 175 | expected_hash = "0x7624778dedc75f8b322b9fa1632a610d40b85e106c7d9bf0e743a9ce291b9c6f" 176 | assert response == expected_hash 177 | 178 | 179 | def test_net_version(alchemy_with_key): 180 | response = alchemy_with_key.net_version() 181 | assert response == "1" 182 | 183 | 184 | def test_net_listening(alchemy_with_key): 185 | response = alchemy_with_key.net_listening() 186 | assert response is True 187 | 188 | 189 | # Not implemented in Alchemy 190 | # def test_net_peer_count(alchemy_with_key): 191 | # response = alchemy_with_key.net_peer_count() 192 | 193 | 194 | def test_protocol_version(alchemy_with_key): 195 | response = alchemy_with_key.protocol_version() 196 | assert HexIntStringNumber(response).int > 0 197 | 198 | 199 | def test_syncing(alchemy_with_key): 200 | response = alchemy_with_key.syncing() 201 | assert response is False 202 | 203 | 204 | # Not supported by Alchemy 205 | # def test_coinbase(alchemy_with_key): 206 | # response = alchemy_with_key.coinbase() 207 | # assert response == "0x0000000000000000000000000000000000000000" 208 | 209 | # Not supported by Alchemy 210 | # def test_hashrate(alchemy_with_key): 211 | # response = alchemy_with_key.hashrate() 212 | # assert response == 0 213 | 214 | 215 | def test_gas_price(alchemy_with_key): 216 | response = alchemy_with_key.gas_price() 217 | assert response > 0 218 | 219 | 220 | # Unsupported by Alchemy 221 | # def test_get_compilers(alchemy_with_key): 222 | # response = alchemy_with_key.get_compilers() 223 | # assert response == [] 224 | 225 | 226 | # Unsupported by Alchemy 227 | # def test_get_work(alchemy_with_key): 228 | # response = alchemy_with_key.get_work() 229 | # assert response == [] 230 | 231 | 232 | def test_get_logs(alchemy_with_key): 233 | topics = ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"] 234 | response = alchemy_with_key.get_logs(CHAINLINK_ADDRESS, topics, 16293070, 16293080) 235 | assert len(response) == 7 236 | -------------------------------------------------------------------------------- /tests/integration/test_alchemy_core.py: -------------------------------------------------------------------------------- 1 | from tests.test_data import ( 2 | CHAINLINK_ADDRESS, 3 | CHAINLINK_CREATOR, 4 | VITALIK, 5 | PATRICK_ALPHA_C, 6 | ENS, 7 | ETH_BLOCKS, 8 | PATRICK_ALPHA_C_TOKEN_ID_ENS, 9 | ) 10 | 11 | 12 | def test_set_api_key(alchemy, mock_env_missing): 13 | # Arrange / Act 14 | alchemy.set_api_key("Hello") 15 | # Assert 16 | assert alchemy.api_key == "Hello" 17 | 18 | 19 | def test_get_current_block_number(alchemy_with_key): 20 | # Arrange / Act 21 | current_block = alchemy_with_key.get_current_block_number() 22 | # Assert 23 | assert current_block > 0 24 | 25 | 26 | def test_get_asset_transfers_page_key(alchemy_with_key): 27 | # Arrange 28 | start_block = 0 29 | end_block = 16271807 30 | address = "0x165Ff6730D449Af03B4eE1E48122227a3328A1fc" 31 | 32 | # Act 33 | _, page_key = alchemy_with_key.get_asset_transfers( 34 | from_address=address, from_block=start_block, to_block=end_block 35 | ) 36 | 37 | # Assert 38 | assert page_key is not None 39 | 40 | 41 | def test_get_asset_transfers_page_key_is_none(alchemy_with_key): 42 | # Arrange 43 | start_block = 16271807 44 | end_block = 16271807 45 | address = "0x165Ff6730D449Af03B4eE1E48122227a3328A1fc" 46 | 47 | # Act 48 | _, page_key = alchemy_with_key.get_asset_transfers( 49 | from_address=address, from_block=start_block, to_block=end_block 50 | ) 51 | 52 | # Assert 53 | assert page_key is None 54 | 55 | 56 | def test_get_asset_transfers_all(alchemy_with_key): 57 | # Arrange 58 | start_block = 0 59 | end_block = 16291530 60 | to_address = "0xa5D0084A766203b463b3164DFc49D91509C12daB" 61 | category = ["erc20"] 62 | expected_transfers_number = 14185 63 | 64 | # Act 65 | transfers, _ = alchemy_with_key.get_asset_transfers( 66 | to_address=to_address, 67 | from_block=start_block, 68 | to_block=end_block, 69 | get_all_flag=True, 70 | category=category, 71 | ) 72 | 73 | # Assert 74 | assert len(transfers) == expected_transfers_number 75 | 76 | 77 | def test_get_transaction_receipts(alchemy_with_key): 78 | # Arrange 79 | block_number = 16292979 80 | expected_from = "0x7944e84d18803f926743fa56fb7a9bb9ba5f5f24" 81 | # Act 82 | transaction_receipts = alchemy_with_key.get_transaction_receipts(block_number) 83 | # Assert 84 | assert transaction_receipts[0]["from"] == expected_from 85 | assert len(transaction_receipts) == 132 86 | 87 | 88 | def test_find_contract_deployer(alchemy_with_key): 89 | deployer_address, block_number = alchemy_with_key.find_contract_deployer( 90 | CHAINLINK_ADDRESS 91 | ) 92 | expected_block_number = 4281611 93 | assert deployer_address.lower() == CHAINLINK_CREATOR.lower() 94 | assert block_number == expected_block_number 95 | 96 | 97 | def test_get_max_priority_fee_per_gas(alchemy_with_key): 98 | response = alchemy_with_key.get_max_priority_fee_per_gas() 99 | assert response > 0 100 | 101 | 102 | def test_get_fee_history(alchemy_with_key): 103 | current_block = int(alchemy_with_key.get_current_block_number()) 104 | response = alchemy_with_key.get_fee_history(1, current_block) 105 | assert len(response["baseFeePerGas"]) > 0 106 | 107 | 108 | def test_fee_data(alchemy_with_key): 109 | fee_data = alchemy_with_key.get_fee_data() 110 | assert fee_data["max_fee_per_gas"] > 0 111 | assert fee_data["max_priority_fee_per_gas"] > 0 112 | assert fee_data["gas_price"] > 0 113 | 114 | 115 | def test_get_token_balances_with_address_and_contract_list(alchemy_with_key): 116 | contract = "0xdAC17F958D2ee523a2206206994597C13D831ec7" 117 | response = alchemy_with_key.get_token_balances(VITALIK, [contract]) 118 | assert len(response["tokenBalances"]) == 1 119 | 120 | 121 | def test_get_token_balances_with_type(alchemy_with_key): 122 | response = alchemy_with_key.get_token_balances(VITALIK, "erc20") 123 | assert len(response["tokenBalances"]) > 0 124 | 125 | 126 | def test_get_token_balances_with_page_key(alchemy_with_key): 127 | setup_response = alchemy_with_key.get_token_balances(VITALIK, "erc20") 128 | page_key = setup_response["pageKey"] 129 | response = alchemy_with_key.get_token_balances(VITALIK, "erc20", page_key) 130 | assert len(response["tokenBalances"]) > 0 131 | 132 | 133 | def test_get_token_metadata(alchemy_with_key): 134 | response = alchemy_with_key.get_token_metadata(CHAINLINK_ADDRESS) 135 | assert response["name"] == "Chainlink" 136 | assert response["symbol"] == "LINK" 137 | 138 | 139 | def test_send(alchemy_with_key): 140 | hash = "0x7ac79af930a26f05ef3ae4b3f9e38cb7323696232aea00e3d3e04394ab1c7234" 141 | response = alchemy_with_key.send("eth_getTransactionByHash", [hash]) 142 | assert response["from"].lower() == PATRICK_ALPHA_C.lower() 143 | 144 | 145 | def test_get_nfts(alchemy_with_key): 146 | response = alchemy_with_key.get_nfts_for_owner(PATRICK_ALPHA_C) 147 | assert response["totalCount"] > 0 148 | 149 | 150 | def test_get_owners_for_token(alchemy_with_key): 151 | response = alchemy_with_key.get_owners_for_token( 152 | ENS, 153 | PATRICK_ALPHA_C_TOKEN_ID_ENS, 154 | ) 155 | assert response["owners"][0].lower() == PATRICK_ALPHA_C.lower() 156 | 157 | 158 | def test_get_owners_for_nft(alchemy_with_key): 159 | response = alchemy_with_key.get_owners_for_nft( 160 | ENS, 161 | PATRICK_ALPHA_C_TOKEN_ID_ENS, 162 | ) 163 | assert response["owners"][0].lower() == PATRICK_ALPHA_C.lower() 164 | 165 | 166 | # I don't think spam filter is working 167 | # def test_get_nfts_spam_check(alchemy_with_key): 168 | # with_spam = alchemy_with_key.get_nfts(PATRICK_ALPHA_C) 169 | # no_spam = alchemy_with_key.get_nfts( 170 | # PATRICK_ALPHA_C, exclude_filters=["SPAM", "AIRDROPS"] 171 | # ) 172 | # assert with_spam["totalCount"] != no_spam["totalCount"] 173 | 174 | 175 | def test_get_owners_for_collection(alchemy_with_key): 176 | current_block_number = alchemy_with_key.get_current_block_number() 177 | response = alchemy_with_key.get_owners_for_collection( 178 | ETH_BLOCKS, False, current_block_number 179 | ) 180 | assert len(response["ownerAddresses"]) > 0 181 | 182 | 183 | def test_get_owners_for_contract(alchemy_with_key): 184 | current_block_number = alchemy_with_key.get_current_block_number() 185 | response = alchemy_with_key.get_owners_for_contract( 186 | ETH_BLOCKS, False, current_block_number 187 | ) 188 | assert len(response["ownerAddresses"]) > 0 189 | 190 | 191 | def test_is_holder_of_collection(alchemy_with_key): 192 | response = alchemy_with_key.is_holder_of_collection(PATRICK_ALPHA_C, ETH_BLOCKS) 193 | # Unless I buy one... 194 | assert response["isHolderOfCollection"] is False 195 | 196 | 197 | def test_get_contracts_for_owner(alchemy_with_key): 198 | response = alchemy_with_key.get_contracts_for_owner(PATRICK_ALPHA_C) 199 | assert len(response["contracts"]) > 0 200 | 201 | 202 | def test_get_nft_metadata(alchemy_with_key): 203 | response = alchemy_with_key.get_nft_metadata(ENS, PATRICK_ALPHA_C_TOKEN_ID_ENS) 204 | assert response["title"].lower() == "patrickalphac.eth" 205 | 206 | 207 | def test_get_contract_metadata(alchemy_with_key): 208 | response = alchemy_with_key.get_contract_metadata(ENS) 209 | assert ( 210 | response["contractMetadata"]["openSea"]["collectionName"] 211 | == "ENS: Ethereum Name Service" 212 | ) 213 | 214 | 215 | def test_get_nfts_for_contract(alchemy_with_key): 216 | response = alchemy_with_key.get_nfts_for_contract(ENS, limit=5) 217 | assert len(response["nfts"]) == 5 218 | 219 | 220 | def test_get_nfts_for_collection(alchemy_with_key): 221 | response = alchemy_with_key.get_nfts_for_collection(ENS, limit=5) 222 | assert len(response["nfts"]) == 5 223 | 224 | 225 | def test_get_spam_contracts(alchemy_with_key): 226 | response = alchemy_with_key.get_spam_contracts() 227 | assert len(response) > 0 228 | 229 | 230 | def test_is_spam_contract(alchemy_with_key): 231 | response = alchemy_with_key.is_spam_contract(ENS) 232 | assert response is False 233 | 234 | 235 | def test_refresh_contract(alchemy_with_key): 236 | response = alchemy_with_key.refresh_contract(ENS) 237 | assert "reingestionState" in response 238 | 239 | 240 | def test_get_floor_price(alchemy_with_key): 241 | response = alchemy_with_key.get_floor_price(ENS) 242 | assert "openSea" in response 243 | assert "floorPrice" in response["openSea"] 244 | 245 | 246 | # Something going wrong here, idk 247 | # def test_compute_rarity(alchemy_with_key): 248 | # response = alchemy_with_key.compute_rarity(ENS, PATRICK_ALPHA_C_TOKEN_ID_ENS) 249 | # assert "rarity" in response 250 | 251 | 252 | def test_verify_nft_ownership(alchemy_with_key): 253 | response = alchemy_with_key.verify_nft_ownership(PATRICK_ALPHA_C, ENS) 254 | # find ENS key ignoring caps and case 255 | assert response[ENS.lower()] == True 256 | 257 | 258 | def test_alchemy_get_transaction(alchemy_with_key): 259 | response = alchemy_with_key.get_transaction( 260 | "0x7ac79af930a26f05ef3ae4b3f9e38cb7323696232aea00e3d3e04394ab1c7234" 261 | ) 262 | assert response["from"].lower() == PATRICK_ALPHA_C.lower() 263 | -------------------------------------------------------------------------------- /alchemy_sdk_py/evm_node.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Optional, Union 3 | 4 | import requests 5 | from dotenv import load_dotenv 6 | 7 | from .errors import NO_API_KEY_ERROR 8 | from .networks import Network 9 | from .utils import HexIntStringNumber 10 | 11 | load_dotenv() 12 | 13 | HEADERS = {"accept": "application/json", "content-type": "application/json"} 14 | POSSIBLE_BLOCK_TAGS = ["latest", "earliest", "pending", "safe", "finalized"] 15 | 16 | 17 | class EVM_Node: 18 | def __init__( 19 | self, 20 | api_key: Optional[str] = None, 21 | key: Optional[str] = None, 22 | network: Optional[Network] = "eth_mainnet", 23 | retries: Optional[int] = 0, 24 | proxy: Optional[dict] = None, 25 | url: Optional[str] = None, 26 | ): 27 | """A python class to interact with the Alchemy API. This class is used to interact with the EVM JSON-RPC API. 28 | We see most of the typical EVM JSON-RPC endpoints here. For more information on the EVM JSON-RPC API, see 29 | https://ethereum.org/en/developers/docs/apis/json-rpc/ 30 | 31 | Args: 32 | api_key (Optional[str], optional): The API key of your alchemy instance. Defaults to None. 33 | key (Optional[str], optional): Another way to pass an api key. 34 | network (Optional[str], optional): The network you want to work on. Defaults to None. 35 | retries (Optional[int], optional): The number of times to retry a request. Defaults to 0. 36 | proxy (Optional[dict], optional): A proxy to use for requests. Defaults to None. 37 | url (Optional[str], optional): A custom url to use for requests. Defaults to None. 38 | 39 | Raises: 40 | ValueError: If you give it a bad network or API key it'll error 41 | """ 42 | if key: 43 | api_key = key 44 | if api_key is None: 45 | api_key = os.getenv("ALCHEMY_API_KEY") 46 | if not api_key or not isinstance(api_key, str): 47 | raise ValueError(NO_API_KEY_ERROR) 48 | self.api_key = api_key 49 | self.network = Network(network) 50 | self.url_network_name = self.network.name.replace("_", "-") 51 | self.base_url_without_key = f"https://{self.url_network_name}.g.alchemy.com/v2/" 52 | self.base_url = ( 53 | f"{self.base_url_without_key}{self.api_key}" if url is None else url 54 | ) 55 | self.retries = retries 56 | self.proxy = proxy or {} 57 | self.call_id = 0 58 | 59 | @property 60 | def key(self) -> str: 61 | """ 62 | returns: 63 | API key 64 | """ 65 | return self.api_key 66 | 67 | ############################################################ 68 | ################ ETH JSON-RPC Methods ###################### 69 | ############################################################ 70 | 71 | def call( 72 | self, 73 | from_address: str, 74 | to_address: str, 75 | gas: Union[str, int], 76 | gas_price: Union[str, int], 77 | value: Union[int, str, None] = "0", 78 | data: Optional[str] = "0x0", 79 | tag: Union[str, dict, None] = "latest", 80 | ) -> str: 81 | """Call a smart contract function 82 | 83 | Args: 84 | from_address (str): The address to call from 85 | to_address (str): The address to call to 86 | gas (int): The gas to use 87 | gas_price (int): The gas price to use 88 | value (int): The value to send 89 | data (str): The data to send 90 | tag (Union[str, dict, None]): The tag to use. "latest", "earlist", "pending", or a block number like: 91 | {"blockHash": "0x"} 92 | 93 | Returns: 94 | str: The result of the call 95 | """ 96 | tag = tag.lower() if isinstance(tag, str) else tag 97 | payload = { 98 | "id": self.call_id, 99 | "jsonrpc": "2.0", 100 | "method": "eth_call", 101 | "params": [ 102 | { 103 | "from": from_address, 104 | "to": to_address, 105 | "gas": HexIntStringNumber(gas).hex, 106 | "gasPrice": HexIntStringNumber(gas_price).hex, 107 | "value": HexIntStringNumber(value).hex, 108 | "data": data, 109 | }, 110 | tag, 111 | ], 112 | } 113 | json_response = self._handle_api_call(payload) 114 | return json_response.get("result") 115 | 116 | def estimate_gas( 117 | self, 118 | from_address: str, 119 | to_address: str, 120 | gas: Union[str, int], 121 | gas_price: Union[str, int], 122 | value: Union[int, str, None] = "0", 123 | data: Optional[str] = "0x0", 124 | tag: Union[str, dict, None] = "latest", 125 | ) -> str: 126 | payload = { 127 | "id": self.call_id, 128 | "jsonrpc": "2.0", 129 | "method": "eth_estimateGas", 130 | "params": [ 131 | { 132 | "from": from_address, 133 | "to": to_address, 134 | "gas": HexIntStringNumber(gas).hex, 135 | "gasPrice": HexIntStringNumber(gas_price).hex, 136 | "value": HexIntStringNumber(value).hex, 137 | "data": data, 138 | }, 139 | tag.lower(), 140 | ], 141 | } 142 | json_response = self._handle_api_call(payload) 143 | return json_response.get("result") 144 | 145 | def get_current_block_number(self) -> int: 146 | """Returns the current block number 147 | params: 148 | None 149 | returns: 150 | the current max block (INT) 151 | """ 152 | payload = {"id": self.call_id, "jsonrpc": "2.0", "method": "eth_blockNumber"} 153 | json_response = self._handle_api_call(payload) 154 | result = int(json_response.get("result"), 16) 155 | return result 156 | 157 | def block_number(self) -> int: 158 | return self.get_current_block_number() 159 | 160 | def get_balance( 161 | self, 162 | address: str, 163 | tag: Union[str, dict, None] = "latest", 164 | ) -> int: 165 | """ 166 | params: 167 | address: address to get balance of 168 | tag: "latest", "earliest", "pending", or an dict with a block number 169 | ie: {"blockNumber": "0x1"} 170 | 171 | returns: 172 | balance of address (int) 173 | """ 174 | tag = tag.lower() if isinstance(tag, str) else tag 175 | payload = { 176 | "id": self.call_id, 177 | "jsonrpc": "2.0", 178 | "method": "eth_getBalance", 179 | "params": [address, tag], 180 | } 181 | json_response = self._handle_api_call(payload) 182 | return int(json_response.get("result"), 16) 183 | 184 | def get_code(self, address: str, tag: Union[str, dict, None] = "latest") -> str: 185 | """Returns code at a given address. 186 | 187 | Args: 188 | address (str): DATA, 20 Bytes - address 189 | tag (Union[str, dict, None], optional): tag: "latest", "earliest", "pending", or an dict with a block number 190 | ie: {"blockNumber": "0x1"}. Defaults to "latest". 191 | 192 | Returns: 193 | str: Code at given address 194 | """ 195 | tag = tag.lower() if isinstance(tag, str) else tag 196 | payload = { 197 | "id": self.call_id, 198 | "jsonrpc": "2.0", 199 | "method": "eth_getCode", 200 | "params": [address, tag], 201 | } 202 | json_response = self._handle_api_call(payload) 203 | return json_response.get("result") 204 | 205 | def get_transaction_count( 206 | self, address: str, tag: Union[str, dict, None] = "latest" 207 | ) -> int: 208 | """Returns the number of transactions sent from an address. 209 | 210 | Args: 211 | address (str): DATA, 20 Bytes - address 212 | tag (Union[str, dict, None], optional): tag: "latest", "earliest", "pending", or an dict with a block number 213 | ie: {"blockNumber": "0x1"}. Defaults to "latest". 214 | 215 | Returns: 216 | int: Number of transactions sent from an address 217 | """ 218 | tag = tag.lower() if isinstance(tag, str) else tag 219 | payload = { 220 | "id": self.call_id, 221 | "jsonrpc": "2.0", 222 | "method": "eth_getTransactionCount", 223 | "params": [address, tag], 224 | } 225 | json_response = self._handle_api_call(payload) 226 | return int(json_response.get("result"), 16) 227 | 228 | def get_storage_at( 229 | self, 230 | address: str, 231 | storage_position: Union[int, str], 232 | tag: Union[str, dict, None] = "latest", 233 | ) -> str: 234 | """Returns the value from a storage position at a given address. 235 | 236 | Args: 237 | address (str): DATA, 20 Bytes - address 238 | storage_position (Union[int, str]): QUANTITY - integer of the position in the storage. 239 | tag (Union[str, dict, None], optional): tag: "latest", "earliest", "pending", or an dict with a block number 240 | ie: {"blockNumber": "0x1"}. Defaults to "latest". 241 | 242 | Returns: 243 | str: The value at this storage position. 244 | """ 245 | tag = tag.lower() if isinstance(tag, str) else tag 246 | payload = { 247 | "id": self.call_id, 248 | "jsonrpc": "2.0", 249 | "method": "eth_getStorageAt", 250 | "params": [address, HexIntStringNumber(storage_position).hex, tag], 251 | } 252 | json_response = self._handle_api_call(payload) 253 | return json_response.get("result") 254 | 255 | def get_block_transaction_count_by_hash(self, block_hash: str) -> int: 256 | """Returns the number of transactions in a block from a block matching the given block hash. 257 | 258 | Args: 259 | block_hash (str): DATA, 32 Bytes - hash of a block 260 | 261 | Returns: 262 | int: Number of transactions in a block from a block matching the given block hash. 263 | """ 264 | payload = { 265 | "id": self.call_id, 266 | "jsonrpc": "2.0", 267 | "method": "eth_getBlockTransactionCountByHash", 268 | "params": [block_hash], 269 | } 270 | json_response = self._handle_api_call(payload) 271 | return int(json_response.get("result"), 16) 272 | 273 | def get_block_transaction_count_by_number(self, tag: Union[int, str]) -> int: 274 | """Returns the number of transactions in a block from a block matching the given block number. 275 | 276 | Args: 277 | tag (Union[int, str]): QUANTITY|TAG - integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. 278 | ie: "latest" or "0xe8" 279 | 280 | Returns: 281 | int: Number of transactions in a block from a block matching the given block number. 282 | """ 283 | tag_hex = HexIntStringNumber(tag).hex if tag not in POSSIBLE_BLOCK_TAGS else tag 284 | payload = { 285 | "id": self.call_id, 286 | "jsonrpc": "2.0", 287 | "method": "eth_getBlockTransactionCountByNumber", 288 | "params": [tag_hex], 289 | } 290 | json_response = self._handle_api_call(payload) 291 | return int(json_response.get("result"), 16) 292 | 293 | def get_uncle_count_by_blockhash(self, block_hash: str) -> int: 294 | """Returns the number of uncles in a block from a block matching the given block hash. 295 | 296 | Args: 297 | block_hash (str): DATA, 32 Bytes - hash of a block 298 | 299 | Returns: 300 | int: Number of uncles in a block from a block matching the given block hash. 301 | """ 302 | payload = { 303 | "id": self.call_id, 304 | "jsonrpc": "2.0", 305 | "method": "eth_getUncleCountByBlockHash", 306 | "params": [block_hash], 307 | } 308 | json_response = self._handle_api_call(payload) 309 | return int(json_response.get("result"), 16) 310 | 311 | def get_uncle_count_by_block_number(self, tag: Union[int, str]) -> int: 312 | """Returns the number of uncles in a block from a block matching the given block number. 313 | 314 | Args: 315 | tag (Union[int, str]): QUANTITY|TAG - integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. 316 | ie: "latest" or "0xe8" 317 | 318 | Returns: 319 | int: Number of uncles in a block from a block matching the given block number. 320 | """ 321 | tag_hex = HexIntStringNumber(tag).hex if tag not in POSSIBLE_BLOCK_TAGS else tag 322 | payload = { 323 | "id": self.call_id, 324 | "jsonrpc": "2.0", 325 | "method": "eth_getUncleCountByBlockNumber", 326 | "params": [tag_hex], 327 | } 328 | json_response = self._handle_api_call(payload) 329 | return int(json_response.get("result"), 16) 330 | 331 | def get_block_by_hash( 332 | self, block_hash: str, full_transaction_objects: bool = False 333 | ) -> dict: 334 | """Returns information about a block by hash. 335 | 336 | Args: 337 | block_hash (str): DATA, 32 Bytes - hash of a block 338 | full_transaction_objects (bool, optional): If true it returns the full transaction objects, if false only the hashes of the transactions. Defaults to False. 339 | 340 | Returns: 341 | dict: Block data 342 | """ 343 | payload = { 344 | "id": self.call_id, 345 | "jsonrpc": "2.0", 346 | "method": "eth_getBlockByHash", 347 | "params": [block_hash, full_transaction_objects], 348 | } 349 | json_response = self._handle_api_call(payload) 350 | return json_response.get("result", {}) 351 | 352 | def get_block_by_number( 353 | self, tag: Union[int, str], full_transaction_objects: Optional[bool] = False 354 | ) -> dict: 355 | """Returns information about a block by block number. 356 | 357 | Args: 358 | tag (Union[int, str]): QUANTITY|TAG - integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. 359 | ie: "latest" or "0xe8" 360 | full_transaction_objects (bool, optional): If true it returns the full transaction objects, if false only the hashes of the transactions. Defaults to False. 361 | 362 | Returns: 363 | dict: Block data 364 | """ 365 | tag_hex = HexIntStringNumber(tag).hex if tag not in POSSIBLE_BLOCK_TAGS else tag 366 | payload = { 367 | "id": self.call_id, 368 | "jsonrpc": "2.0", 369 | "method": "eth_getBlockByNumber", 370 | "params": [tag_hex, full_transaction_objects], 371 | } 372 | json_response = self._handle_api_call(payload) 373 | return json_response.get("result", {}) 374 | 375 | def get_current_block(self) -> dict: 376 | """ 377 | returns: 378 | current block data 379 | """ 380 | current_block: int = self.get_current_block_number() 381 | return self.get_block_by_number(current_block) 382 | 383 | def get_transaction_by_hash(self, transaction_hash: str) -> dict: 384 | """Returns the information about a transaction requested by transaction hash. 385 | 386 | Args: 387 | transaction_hash (str): DATA, 32 Bytes - hash of a transaction 388 | 389 | Returns: 390 | dict: Transaction data 391 | """ 392 | if not isinstance(transaction_hash, str): 393 | raise TypeError("transaction_hash must be a string") 394 | payload = { 395 | "id": self.call_id, 396 | "jsonrpc": "2.0", 397 | "method": "eth_getTransactionByHash", 398 | "params": [transaction_hash], 399 | } 400 | json_response = self._handle_api_call(payload) 401 | return json_response.get("result", {}) 402 | 403 | def get_transaction_by_block_hash_and_index( 404 | self, block_hash: str, index: int 405 | ) -> dict: 406 | """Returns information about a transaction by block hash and transaction index position. 407 | 408 | Args: 409 | block_hash (str): DATA, 32 Bytes - hash of a block 410 | index (int): QUANTITY - integer of the transaction index position 411 | 412 | Returns: 413 | dict: Transaction data 414 | """ 415 | if not isinstance(block_hash, str): 416 | raise TypeError("block_hash must be a string") 417 | payload = { 418 | "id": self.call_id, 419 | "jsonrpc": "2.0", 420 | "method": "eth_getTransactionByBlockHashAndIndex", 421 | "params": [block_hash, HexIntStringNumber(index).hex], 422 | } 423 | json_response = self._handle_api_call(payload) 424 | return json_response.get("result", {}) 425 | 426 | def get_transaction_by_block_number_and_index( 427 | self, tag: Union[int, str], index: int 428 | ) -> dict: 429 | """Returns information about a transaction by block number and transaction index position. 430 | 431 | Args: 432 | tag (Union[int, str]): QUANTITY|TAG - integer of a block number, or the string "earliest", "latest" or "pending", as in the default block parameter. 433 | ie: "latest" or "0xe8" 434 | index (int): QUANTITY - integer of the transaction index position 435 | 436 | Returns: 437 | dict: Transaction data 438 | """ 439 | tag_hex = HexIntStringNumber(tag).hex if tag not in POSSIBLE_BLOCK_TAGS else tag 440 | payload = { 441 | "id": self.call_id, 442 | "jsonrpc": "2.0", 443 | "method": "eth_getTransactionByBlockNumberAndIndex", 444 | "params": [tag_hex, HexIntStringNumber(index).hex], 445 | } 446 | json_response = self._handle_api_call(payload) 447 | return json_response.get("result", {}) 448 | 449 | def get_transaction_receipt(self, transaction_hash: str) -> dict: 450 | """ 451 | params: 452 | transaction_hash: transaction hash to search for 453 | returns: 454 | transaction receipt data 455 | """ 456 | if not isinstance(transaction_hash, str): 457 | raise TypeError("transaction_hash must be a string") 458 | payload = { 459 | "id": self.call_id, 460 | "jsonrpc": "2.0", 461 | "method": "eth_getTransactionReceipt", 462 | "params": [transaction_hash], 463 | } 464 | json_response = self._handle_api_call(payload) 465 | result = json_response.get("result", {}) 466 | return result 467 | 468 | def get_uncle_by_block_hash_and_index(self, block_hash: str, index: int) -> dict: 469 | """ 470 | params: 471 | block_hash: block hash to search for 472 | index: index of the uncle to search for 473 | returns: 474 | uncle data 475 | """ 476 | if not isinstance(block_hash, str): 477 | raise TypeError("block_hash must be a string") 478 | payload = { 479 | "id": self.call_id, 480 | "jsonrpc": "2.0", 481 | "method": "eth_getUncleByBlockHashAndIndex", 482 | "params": [block_hash, HexIntStringNumber(index).hex], 483 | } 484 | json_response = self._handle_api_call(payload) 485 | result = json_response.get("result", {}) 486 | return result 487 | 488 | # make a function for eth_getUncleByBlockNumberAndIndex 489 | 490 | def get_uncle_by_block_number_and_index( 491 | self, tag: Union[int, str], index: int 492 | ) -> dict: 493 | """ 494 | params: 495 | tag: block number to search for 496 | index: index of the uncle to search for 497 | returns: 498 | uncle data 499 | """ 500 | tag_hex = HexIntStringNumber(tag).hex if tag not in POSSIBLE_BLOCK_TAGS else tag 501 | payload = { 502 | "id": self.call_id, 503 | "jsonrpc": "2.0", 504 | "method": "eth_getUncleByBlockNumberAndIndex", 505 | "params": [tag_hex, HexIntStringNumber(index).hex], 506 | } 507 | json_response = self._handle_api_call(payload) 508 | result = json_response.get("result", {}) 509 | return result 510 | 511 | def client_version(self) -> str: 512 | """ 513 | params: 514 | None 515 | returns: 516 | client version string 517 | """ 518 | payload = { 519 | "id": self.call_id, 520 | "jsonrpc": "2.0", 521 | "method": "web3_clientVersion", 522 | "params": [], 523 | } 524 | json_response = self._handle_api_call(payload) 525 | result = json_response.get("result", "") 526 | return result 527 | 528 | def sha(self, data: str) -> str: 529 | """Convert data to sha3 hash 530 | Args: 531 | data (str): data to convert 532 | Returns: 533 | str: sha3 hash 534 | """ 535 | if not isinstance(data, str): 536 | raise TypeError("data must be a string") 537 | if not data.startswith("0x"): 538 | data = hex(int.from_bytes(data.encode(), "big")) 539 | payload = { 540 | "id": self.call_id, 541 | "jsonrpc": "2.0", 542 | "method": "web3_sha3", 543 | "params": [data], 544 | } 545 | json_response = self._handle_api_call(payload) 546 | result = json_response.get("result", "") 547 | return result 548 | 549 | def net_version(self) -> str: 550 | """ 551 | params: 552 | None 553 | returns: 554 | network version string 555 | """ 556 | payload = { 557 | "id": self.call_id, 558 | "jsonrpc": "2.0", 559 | "method": "net_version", 560 | "params": [], 561 | } 562 | json_response = self._handle_api_call(payload) 563 | result = json_response.get("result", "") 564 | return result 565 | 566 | def net_listening(self) -> bool: 567 | """ 568 | params: 569 | None 570 | returns: 571 | True if client is actively listening for network connections 572 | """ 573 | payload = { 574 | "id": self.call_id, 575 | "jsonrpc": "2.0", 576 | "method": "net_listening", 577 | "params": [], 578 | } 579 | json_response = self._handle_api_call(payload) 580 | result = json_response.get("result", False) 581 | return result 582 | 583 | # Currently not implemented by Alchemy 584 | # def net_peer_count(self) -> int: 585 | # """ 586 | # params: 587 | # None 588 | # returns: 589 | # number of peers currently connected to the client 590 | # """ 591 | # payload = { 592 | # "id": self.call_id, 593 | # "jsonrpc": "2.0", 594 | # "method": "net_peerCount", 595 | # "params": [], 596 | # } 597 | # json_response = self._handle_api_call(payload) 598 | # result = json_response.get("result", "") 599 | # return int(result, 16) 600 | 601 | def protocol_version(self) -> str: 602 | """ 603 | params: 604 | None 605 | returns: 606 | ethereum protocol version string 607 | """ 608 | payload = { 609 | "id": self.call_id, 610 | "jsonrpc": "2.0", 611 | "method": "eth_protocolVersion", 612 | "params": [], 613 | } 614 | json_response = self._handle_api_call(payload) 615 | result = json_response.get("result", "") 616 | return result 617 | 618 | def syncing(self) -> Union[bool, dict]: 619 | """ 620 | params: 621 | None 622 | returns: 623 | False if not syncing, otherwise a dictionary with sync status info 624 | """ 625 | payload = { 626 | "id": self.call_id, 627 | "jsonrpc": "2.0", 628 | "method": "eth_syncing", 629 | "params": [], 630 | } 631 | json_response = self._handle_api_call(payload) 632 | result = json_response.get("result", False) 633 | return result 634 | 635 | # Not supported by Alchemy 636 | # def coinbase(self) -> str: 637 | # """ 638 | # params: 639 | # None 640 | # returns: 641 | # coinbase address 642 | # """ 643 | # payload = { 644 | # "id": self.call_id, 645 | # "jsonrpc": "2.0", 646 | # "method": "eth_coinbase", 647 | # "params": [], 648 | # } 649 | # json_response = self._handle_api_call(payload) 650 | # result = json_response.get("result", "") 651 | # return result 652 | 653 | # Not supported by Alchemy 654 | # def mining(self) -> bool: 655 | # """ 656 | # params: 657 | # None 658 | # returns: 659 | # True if client is actively mining new blocks 660 | # """ 661 | # payload = { 662 | # "id": self.call_id, 663 | # "jsonrpc": "2.0", 664 | # "method": "eth_mining", 665 | # "params": [], 666 | # } 667 | # json_response = self._handle_api_call(payload) 668 | # result = json_response.get("result", False) 669 | # return result 670 | 671 | # Not supported by Alchemy 672 | # def hashrate(self) -> str: 673 | # """ 674 | # params: 675 | # None 676 | # returns: 677 | # number of hashes per second that the node is mining with 678 | # """ 679 | # payload = { 680 | # "id": self.call_id, 681 | # "jsonrpc": "2.0", 682 | # "method": "eth_hashrate", 683 | # "params": [], 684 | # } 685 | # json_response = self._handle_api_call(payload) 686 | # result = json_response.get("result", "") 687 | # return result 688 | 689 | def gas_price(self) -> int: 690 | """ 691 | params: 692 | None 693 | returns: 694 | current gas price in wei 695 | """ 696 | payload = { 697 | "id": self.call_id, 698 | "jsonrpc": "2.0", 699 | "method": "eth_gasPrice", 700 | "params": [], 701 | } 702 | json_response = self._handle_api_call(payload) 703 | result = json_response.get("result", "0") 704 | return HexIntStringNumber(result).int 705 | 706 | def get_gas_price(self) -> int: 707 | """ 708 | params: 709 | None 710 | returns: 711 | current gas price in wei 712 | """ 713 | return self.gas_price() 714 | 715 | # Unsupported by Alchemy 716 | # def get_compilers(self) -> List[str]: 717 | # """ 718 | # params: 719 | # None 720 | # returns: 721 | # a list of available compilers in the client 722 | # """ 723 | # payload = { 724 | # "id": self.call_id, 725 | # "jsonrpc": "2.0", 726 | # "method": "eth_getCompilers", 727 | # "params": [], 728 | # } 729 | # json_response = self._handle_api_call(payload) 730 | # result = json_response.get("result", []) 731 | # return result 732 | 733 | # Unsupported by Alchemy 734 | # def get_work(self) -> List[str]: 735 | # """ 736 | # params: 737 | # None 738 | # returns: 739 | # a list of values required by the proof-of-work consensus algorithm 740 | # """ 741 | # payload = { 742 | # "id": self.call_id, 743 | # "jsonrpc": "2.0", 744 | # "method": "eth_getWork", 745 | # "params": [], 746 | # } 747 | # json_response = self._handle_api_call(payload) 748 | # result = json_response.get("result", []) 749 | # return result 750 | 751 | def get_logs( 752 | self, 753 | contract_address: str, 754 | topics: Union[List[str], str], 755 | from_block: Union[str, int, None] = 0, 756 | to_block: Union[str, int, None] = "latest", 757 | ) -> list: 758 | return self.get_events(contract_address, topics, from_block, to_block) 759 | 760 | def get_events( 761 | self, 762 | contract_address: str, 763 | topics: Union[List[str], str], 764 | from_block: Union[str, int, None] = 0, 765 | to_block: Union[str, int, None] = "latest", 766 | ) -> list: 767 | """ 768 | params: 769 | contract_address: address of the contract 770 | topics: list of topics to filter by (event signatures) 771 | from_block: block number, or one of "earliest", "latest", "pending" 772 | to_block: block number, or one of "earliest", "latest", "pending" 773 | 774 | returns: A dictionary, result[block] = block_date 775 | """ 776 | topics = topics if isinstance(topics, list) else [topics] 777 | from_block_hex = from_block 778 | to_block_hex = to_block 779 | if from_block not in POSSIBLE_BLOCK_TAGS: 780 | from_block_hex = HexIntStringNumber(from_block).hex 781 | if to_block not in POSSIBLE_BLOCK_TAGS: 782 | to_block_hex = HexIntStringNumber(to_block).hex 783 | payload = { 784 | "id": self.call_id, 785 | "jsonrpc": "2.0", 786 | "method": "eth_getLogs", 787 | "params": [ 788 | { 789 | "address": contract_address, 790 | "fromBlock": from_block_hex, 791 | "toBlock": to_block_hex, 792 | "topics": topics, 793 | } 794 | ], 795 | } 796 | json_response = self._handle_api_call(payload) 797 | result = json_response.get("result", {}) 798 | return result 799 | 800 | def send_raw_transactions(self, data: str) -> str: 801 | """ 802 | params: 803 | data: raw transaction data 804 | returns: transaction hash 805 | 806 | Note: I ain't bothering to test this. 807 | """ 808 | payload = { 809 | "id": self.call_id, 810 | "jsonrpc": "2.0", 811 | "method": "eth_sendRawTransaction", 812 | "params": [data], 813 | } 814 | json_response = self._handle_api_call(payload) 815 | result = json_response.get("result", "") 816 | return result 817 | 818 | ############################################################ 819 | ################ Internal/Raw Methods ###################### 820 | ############################################################ 821 | 822 | def _handle_api_call( 823 | self, 824 | payload: dict, 825 | endpoint: Optional[str] = None, 826 | url: Optional[str] = None, 827 | ) -> dict: 828 | """Handles making the API calls to Alchemy... It should be refactored, it's gross 829 | 830 | params: 831 | payload: the payload to send to the API 832 | endpoint: the endpoint to send the payload to 833 | url: the url to send the payload to 834 | http_method: the http method to use 835 | returns: a dictionary of the response 836 | """ 837 | url = self.base_url if url is None else url 838 | headers = HEADERS 839 | if endpoint is not None: 840 | headers["Alchemy-Python-Sdk-Method"] = endpoint 841 | response = requests.post(url, json=payload, headers=headers, proxies=self.proxy) 842 | if response.status_code != 200: 843 | retries_here = 0 844 | while retries_here < self.retries and response.status_code != 200: 845 | retries_here = retries_here + 1 846 | response = requests.post( 847 | url, json=payload, headers=headers, proxies=self.proxy 848 | ) 849 | if response.status_code != 200: 850 | raise ConnectionError( 851 | f'Status {response.status_code} when querying "{self.base_url_without_key}/" with payload {payload}:\n >>> Response with Error: {response.text}' 852 | ) 853 | json_response = response.json() 854 | if ( 855 | json_response.get("result", None) is None 856 | or json_response.get("error", None) is not None 857 | ): 858 | raise ConnectionError( 859 | f'Status {response.status_code} when querying "{self.base_url_without_key}/" with payload {payload}:\n >>> Response with Error: {response.text}' 860 | ) 861 | self.call_id = self.call_id + 1 862 | return json_response 863 | -------------------------------------------------------------------------------- /alchemy_sdk_py/alchemy.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | from datetime import datetime 4 | from typing import List, Optional, Tuple, Union 5 | from .errors import NO_API_KEY_ERROR 6 | from .evm_node import POSSIBLE_BLOCK_TAGS, EVM_Node, HEADERS 7 | from .networks import Network 8 | from .utils import HexIntStringNumber, ETH_NULL_VALUE, is_hash 9 | 10 | NFT_FILTERS = ["SPAM", "AIRDROPS"] 11 | 12 | 13 | class Alchemy(EVM_Node): 14 | def __init__( 15 | self, 16 | api_key: Optional[str] = None, 17 | key: Optional[str] = None, 18 | network: Optional[Network] = "eth_mainnet", 19 | retries: Optional[int] = 0, 20 | proxy: Optional[dict] = None, 21 | url: Optional[str] = None, 22 | ): 23 | """A python class to interact with the Alchemy API 24 | 25 | Args: 26 | api_key (Optional[str], optional): The API key of your alchemy instance. Defaults to None. 27 | key (Optional[str], optional): Another way to pass an api key. 28 | network (Optional[str], optional): The network you want to work on. Defaults to None. 29 | retries (Optional[int], optional): The number of times to retry a request. Defaults to 0. 30 | proxy (Optional[dict], optional): A proxy to use for requests. Defaults to None. 31 | url (Optional[str], optional): A custom url to use for requests. Defaults to None. 32 | 33 | Check the evm_node.py file for more details on these arguments and initialization. 34 | 35 | Raises: 36 | ValueError: If you give it a bad network or API key it'll error 37 | """ 38 | super().__init__( 39 | api_key=api_key, 40 | key=key, 41 | network=network, 42 | retries=retries, 43 | proxy=proxy, 44 | url=url, 45 | ) 46 | 47 | @property 48 | def nft_url(self) -> str: 49 | """The url for the NFT API""" 50 | return f"https://{self.url_network_name}.g.alchemy.com/nft/v2/{self.api_key}" 51 | 52 | @property 53 | def ws_url(self) -> str: 54 | """The url for the websocket""" 55 | return f"wss://{self.url_network_name}.g.alchemy.com/v2/{self.api_key}" 56 | 57 | @property 58 | def webhook_url(self) -> str: 59 | """The url for the webhook""" 60 | return f"https://dashboard.alchemy.com/api" 61 | 62 | ############################################################ 63 | ################ Alchemy SDK Methods ###################### 64 | ############################################################ 65 | 66 | def get_transaction_receipts( 67 | self, 68 | block_number_or_hash: Union[str, int], 69 | ) -> list: 70 | """An enhanced api that gets all transaction receipts for a given block by number or block hash. 71 | Supported on all networks for Ethereum, Polygon, and Arbitrum. 72 | 73 | params: 74 | block_number_or_hash: The block number or hash 75 | returns: 76 | The transaction receipts for the block 77 | """ 78 | input = {} 79 | if is_hash(block_number_or_hash): 80 | input = {"blockHash": block_number_or_hash} 81 | else: 82 | input = {"blockNumber": HexIntStringNumber(block_number_or_hash).hex} 83 | payload = { 84 | "id": self.call_id, 85 | "jsonrpc": "2.0", 86 | "method": "alchemy_getTransactionReceipts", 87 | "params": [input], 88 | } 89 | json_response = self._handle_api_call( 90 | payload, endpoint="getTransactionReceipts" 91 | ) 92 | result = json_response.get("result", {"receipts": []}) 93 | return result["receipts"] 94 | 95 | def binary_search_first_block( 96 | self, 97 | from_block: Union[str, int], 98 | to_block: Union[str, int], 99 | contract_address: str, 100 | ) -> int: 101 | """ 102 | params: 103 | from_block: int (1), hex ("0x1"), or str "1" 104 | to_block: int (1), hex ("0x1"), or str "1" 105 | contract_address: The address of the contract 106 | returns: 107 | The first block where the contract was deployed 108 | """ 109 | if not isinstance(contract_address, str): 110 | raise TypeError("contract_address must be a string") 111 | from_block = HexIntStringNumber(from_block) 112 | to_block = HexIntStringNumber(to_block) 113 | if from_block.int >= to_block.int: 114 | return to_block.int 115 | mid_block = HexIntStringNumber((from_block.int + to_block.int) // 2) 116 | code = self.get_code(contract_address, mid_block.hex) 117 | if code == ETH_NULL_VALUE: 118 | return self.binary_search_first_block( 119 | mid_block.int + 1, to_block.int, contract_address 120 | ) 121 | return self.binary_search_first_block( 122 | from_block.int, mid_block.int, contract_address 123 | ) 124 | 125 | def find_contract_deployer(self, contract_address: str) -> Tuple[str, int]: 126 | """ 127 | params: 128 | contract_address: The address of the contract 129 | returns: 130 | The address of the contract deployer 131 | """ 132 | if not isinstance(contract_address, str): 133 | raise TypeError("contract_address must be a string") 134 | current_block_number = self.get_block("latest")["number"] 135 | code = self.get_code(contract_address, current_block_number) 136 | if code == ETH_NULL_VALUE: 137 | raise ValueError("Contract not found") 138 | first_block = self.binary_search_first_block( 139 | 0, current_block_number, contract_address 140 | ) 141 | tx_receipts: list = self.get_transaction_receipts(first_block) 142 | matching_receipt = [ 143 | receipt 144 | for receipt in tx_receipts 145 | if str(receipt["contractAddress"]).lower() == contract_address.lower() 146 | ] 147 | if len(matching_receipt) == 0: 148 | raise ValueError("Contract not found") 149 | 150 | return matching_receipt[0]["from"], first_block 151 | 152 | def _get_all_asset_transfers( 153 | self, 154 | from_address: Optional[str] = None, 155 | to_address: Optional[str] = None, 156 | from_block: Union[int, str, None] = 0, 157 | to_block: Union[int, str, None] = None, 158 | contract_addresses: Optional[list] = None, 159 | category: Optional[List[str]] = [ 160 | "external", 161 | "internal", 162 | "erc20", 163 | "erc721", 164 | "specialnft", 165 | ], 166 | ) -> list: 167 | """ 168 | NOTE: This will make a LOT of API calls if you're not careful! 169 | 170 | params: 171 | from_address: Address to look for transactions from 172 | from_block: int (1), hex ("0x1"), or str "1" 173 | to_block: int (1), hex ("0x1"), or str "1" 174 | contract_addresses: List of contract addresses to filter by 175 | category: List of categories to filter by 176 | returns: 177 | a list of asset transfers 178 | """ 179 | total_transfers = [] 180 | page_key = None 181 | first_run = True 182 | while page_key is not None or first_run: 183 | first_run = False 184 | transfers, page_key = self.get_asset_transfers( 185 | from_address=from_address, 186 | to_address=to_address, 187 | from_block=from_block, 188 | to_block=to_block, 189 | page_key=page_key, 190 | contract_addresses=contract_addresses, 191 | category=category, 192 | ) 193 | total_transfers.extend(transfers) 194 | return total_transfers, None 195 | 196 | def get_asset_transfers( 197 | self, 198 | from_address: Optional[str] = None, 199 | to_address: Optional[str] = None, 200 | from_block: Union[int, str, None] = 0, 201 | to_block: Union[int, str, None] = None, 202 | max_count: Union[int, str, None] = 1000, 203 | page_key: Optional[str] = None, 204 | contract_addresses: Optional[list] = None, 205 | category: Optional[List[str]] = [ 206 | "external", 207 | "internal", 208 | "erc20", 209 | "erc721", 210 | "specialnft", 211 | ], 212 | get_all_flag: Optional[bool] = False, 213 | ) -> Tuple[list, str]: 214 | """ 215 | params: 216 | from_address: Address to look for transactions from 217 | from_block: int (1), hex ("0x1"), or str "1" 218 | to_block: int (1), hex ("0x1"), or str "1" 219 | max_count: Max number of transactions to return 220 | page_key: A unique key to get the next page of results 221 | contract_addresses: A list of contract addresses to filter by (for erc20, erc721, specialnft) 222 | category: A list of categories to filter by (external, internal, erc20, erc721, specialnft) 223 | get_all_flag: If True, will make multiple API calls to get all results 224 | 225 | NOTE: If get_all_flag is true, you risk making a LOT of API calls! 226 | 227 | returns: [list, str] 228 | A Tuple, index 0 is the list of transfers, index 1 is the page key or None 229 | """ 230 | if to_block is None: 231 | to_block = self.get_current_block_number() 232 | from_block_hex = HexIntStringNumber(from_block).hex 233 | to_block_hex = HexIntStringNumber(to_block).hex 234 | from_address = from_address.lower() if from_address else None 235 | to_address = to_address.lower() if to_address else None 236 | if get_all_flag: 237 | return self._get_all_asset_transfers( 238 | from_address=from_address, 239 | to_address=to_address, 240 | from_block=from_block_hex, 241 | to_block=to_block_hex, 242 | contract_addresses=contract_addresses, 243 | category=category, 244 | ) 245 | payload = { 246 | "id": self.call_id, 247 | "jsonrpc": "2.0", 248 | "method": "alchemy_getAssetTransfers", 249 | "params": [ 250 | { 251 | "fromBlock": from_block_hex, 252 | "toBlock": to_block_hex, 253 | "category": category, 254 | "excludeZeroValue": False, 255 | "maxCount": HexIntStringNumber(max_count).hex, 256 | } 257 | ], 258 | } 259 | if page_key: 260 | payload["params"][0]["pageKey"] = page_key 261 | if contract_addresses: 262 | payload["params"][0]["contractAddresses"] = contract_addresses 263 | if from_address: 264 | payload["params"][0]["fromAddress"] = from_address 265 | if to_address: 266 | payload["params"][0]["toAddress"] = to_address 267 | 268 | json_response = self._handle_api_call(payload, endpoint="getAssetTransfers") 269 | result = json_response.get("result") 270 | transfers = result.get("transfers", -1) 271 | if transfers == -1: 272 | raise ValueError(f"No transfers found. API response: {json_response}") 273 | if "pageKey" in result: 274 | return transfers, result["pageKey"] 275 | return transfers, None 276 | 277 | def get_block(self, block_number_or_hash_or_tag: Union[str, int]) -> dict: 278 | """ 279 | params: 280 | block_number: block number to get (can be int, string, or hash, or tag like "latest", "earliest", "pending") 281 | returns: 282 | block data 283 | """ 284 | if block_number_or_hash_or_tag in POSSIBLE_BLOCK_TAGS: 285 | return self.get_block_by_number(block_number_or_hash_or_tag) 286 | if is_hash(block_number_or_hash_or_tag): 287 | return self.get_block_by_hash(block_number_or_hash_or_tag) 288 | return self.get_block_by_number(block_number_or_hash_or_tag) 289 | 290 | def get_block_number(self) -> int: 291 | """ 292 | returns: 293 | current block number 294 | """ 295 | current_block: int = self.get_current_block_number() 296 | return current_block 297 | 298 | def get_block_with_transactions( 299 | self, block_hash_number_or_tag: Union[str, int] 300 | ) -> dict: 301 | """ 302 | params: 303 | block_number: block number to get (can be int, string, or hash, or tag like "latest", "earliest", "pending") 304 | returns: 305 | block data 306 | """ 307 | if block_hash_number_or_tag in POSSIBLE_BLOCK_TAGS: 308 | return self.get_block_by_number(block_hash_number_or_tag, True) 309 | if is_hash(block_hash_number_or_tag): 310 | return self.get_block_by_hash(block_hash_number_or_tag, True) 311 | return self.get_block_by_number(block_hash_number_or_tag, True) 312 | 313 | def fee_data(self) -> dict: 314 | return self.get_fee_data() 315 | 316 | def get_fee_data(self) -> dict: 317 | """Returns the recommended fee data to use in a transaction. 318 | For an EIP-1559 transaction, the maxFeePerGas and maxPriorityFeePerGas should be used. 319 | For legacy transactions and networks which do not support EIP-1559, 320 | the gasPrice should be used. 321 | 322 | Returns: 323 | dict: _description_ 324 | """ 325 | max_fee_per_gas = self.get_max_fee_per_gas() 326 | max_priority_fee_per_gas = self.get_max_priority_fee_per_gas() 327 | gas_price = self.get_gas_price() 328 | return { 329 | "max_fee_per_gas": max_fee_per_gas, 330 | "max_priority_fee_per_gas": max_priority_fee_per_gas, 331 | "gas_price": gas_price, 332 | } 333 | 334 | def get_datetime_of_blocks( 335 | self, blocks=None, from_block=None, to_block=None 336 | ) -> dict: 337 | """ 338 | params: 339 | from_block as an INT 340 | to_block as an INT 341 | returns: 342 | A dictionary, result[block] = block_date 343 | """ 344 | blocks = list(range(from_block, to_block)) if blocks is None else blocks 345 | result = {} 346 | for block in blocks: 347 | payload = { 348 | "id": self.call_id, 349 | "jsonrpc": "2.0", 350 | "method": "eth_getBlockByNumber", 351 | "params": [hex(block), False], 352 | } 353 | json_response = self._handle_api_call(payload) 354 | result_raw = json_response.get("result", None) 355 | block = int(result_raw["number"], 16) 356 | block_date = datetime.fromtimestamp(int(result_raw["timestamp"], 16)) 357 | result[block] = block_date 358 | return result 359 | 360 | def max_priority_fee_per_gas(self) -> int: 361 | return self.get_max_priority_fee_per_gas() 362 | 363 | def get_max_priority_fee_per_gas(self) -> int: 364 | """ 365 | params: 366 | None 367 | returns: 368 | current max priority fee per gas in wei 369 | """ 370 | payload = { 371 | "id": self.call_id, 372 | "jsonrpc": "2.0", 373 | "method": "eth_maxPriorityFeePerGas", 374 | "params": [], 375 | } 376 | json_response = self._handle_api_call(payload) 377 | result = json_response.get("result", "0") 378 | return HexIntStringNumber(result).int 379 | 380 | def get_fee_history( 381 | self, 382 | block_count: int, 383 | newest_block: Union[str, int], 384 | reward_percentiles: int = None, 385 | ) -> dict: 386 | """ 387 | params: 388 | None 389 | returns: 390 | current fee history 391 | """ 392 | if newest_block in POSSIBLE_BLOCK_TAGS: 393 | newest_block = self.get_block(newest_block)["number"] 394 | params = ( 395 | [HexIntStringNumber(block_count).hex, HexIntStringNumber(newest_block).hex] 396 | if not reward_percentiles 397 | else [block_count, newest_block, reward_percentiles] 398 | ) 399 | payload = { 400 | "id": self.call_id, 401 | "jsonrpc": "2.0", 402 | "method": "eth_feeHistory", 403 | "params": params, 404 | } 405 | json_response = self._handle_api_call(payload) 406 | result = json_response.get("result", {}) 407 | return result 408 | 409 | def fee_history( 410 | self, 411 | block_count: int, 412 | newest_block: Union[str, int], 413 | reward_percentiles: int = None, 414 | ) -> dict: 415 | return self.get_fee_history(block_count, newest_block, reward_percentiles) 416 | 417 | def max_fee_per_gas(self) -> int: 418 | return self.get_max_fee_per_gas() 419 | 420 | def get_max_fee_per_gas(self) -> int: 421 | base_fee_per_gas = self.get_base_fee_per_gas() 422 | max_priority_fee_per_gas = self.get_max_priority_fee_per_gas() 423 | return base_fee_per_gas + max_priority_fee_per_gas 424 | 425 | def get_base_fee_per_gas(self) -> int: 426 | fee_history = self.fee_history(1, "latest") 427 | base_fee_per_gas = fee_history["baseFeePerGas"][0] 428 | return HexIntStringNumber(base_fee_per_gas).int 429 | 430 | def base_fee_per_gas(self) -> int: 431 | return self.get_base_fee_per_gas() 432 | 433 | def get_token_balances( 434 | self, 435 | address: str, 436 | token_addresses_or_type: Union[List[str], str, None] = None, 437 | options_or_page_key: Union[dict, str, None] = None, 438 | ) -> dict: 439 | """ 440 | params: 441 | address: Address to get token balances for 442 | token_addresses: List of token addresses to get balances for, OR the string "DEFAULT_TOKENS" 443 | or the string "erc20", or none 444 | options_or_page_key: Optional dictionary of options that contains a pagekey, or a page key 445 | returns: 446 | Dictionary of token balances 447 | """ 448 | payload = { 449 | "id": self.call_id, 450 | "jsonrpc": "2.0", 451 | "method": "alchemy_getTokenBalances", 452 | } 453 | json_response = {} 454 | if isinstance(token_addresses_or_type, list): 455 | if len(token_addresses_or_type) > 1500: 456 | raise ValueError("Too many token addresses") 457 | if len(token_addresses_or_type) == 0: 458 | raise ValueError("No token addresses") 459 | payload["params"] = [address, token_addresses_or_type] 460 | json_response = self._handle_api_call(payload, endpoint="getTokenBalances") 461 | else: 462 | params = [address] 463 | if not token_addresses_or_type: 464 | params.append("erc20") 465 | else: 466 | params.append(token_addresses_or_type) 467 | if isinstance(options_or_page_key, str): 468 | params.append({"pageKey": options_or_page_key}) 469 | else: 470 | params.append(options_or_page_key or {}) 471 | payload["params"] = params 472 | json_response = self._handle_api_call(payload, endpoint="getTokenBalances") 473 | result = json_response.get("result", {}) 474 | return result 475 | 476 | def get_token_metadata(self, token_address: str) -> dict: 477 | """ 478 | params: 479 | token_address: Address of the token to get metadata for 480 | returns: 481 | Dictionary of token metadata 482 | """ 483 | payload = { 484 | "id": self.call_id, 485 | "jsonrpc": "2.0", 486 | "method": "alchemy_getTokenMetadata", 487 | "params": [token_address], 488 | } 489 | json_response = self._handle_api_call(payload, endpoint="getTokenMetadata") 490 | result = json_response.get("result", {}) 491 | return result 492 | 493 | def send(self, method: str, parameters: list) -> dict: 494 | """Allows sending a raw message to the Alchemy backend. 495 | 496 | params: 497 | method: RPC method to call 498 | parameters: List of parameters to pass to the method 499 | returns: 500 | Dictionary of the response 501 | """ 502 | if not isinstance(parameters, list): 503 | parameters = [parameters] 504 | payload = { 505 | "id": self.call_id, 506 | "jsonrpc": "2.0", 507 | "method": method, 508 | "params": parameters, 509 | } 510 | json_response = self._handle_api_call(payload) 511 | result = json_response.get("result", {}) 512 | return result 513 | 514 | ############################################################ 515 | ################ NFT Methods ############################### 516 | ############################################################ 517 | 518 | def get_nfts_for_owner( 519 | self, 520 | owner: str, 521 | page_key: Optional[str] = None, 522 | page_size: Optional[int] = 100, 523 | contract_addresses: Optional[List[str]] = None, 524 | omit_metadata: Optional[bool] = False, 525 | token_uri_timeout_in_ms: Union[int, None] = None, 526 | filters: Optional[List[str]] = None, 527 | ) -> dict: 528 | """Gets all NFTs currently owned by a given address. 529 | 530 | params: 531 | owner: Address of the owner to get NFTs for 532 | page_key: Optional page key to get the next page of results 533 | page_size: Optional page size to get the next page of results 534 | contract_addresses: Optional list of contract addresses to get NFTs for 535 | omit_metadata: Optional boolean to omit metadata 536 | token_uri_timeout_in_ms: Optional timeout in ms for token URI requests 537 | filters: Optional list of strings from "SPAM" and "AIRDROPS" 538 | returns: 539 | Dictionary of NFTs 540 | """ 541 | return self.get_nfts( 542 | owner, 543 | page_key, 544 | page_size, 545 | contract_addresses, 546 | with_metadata=not omit_metadata, 547 | token_uri_timeout_in_ms=token_uri_timeout_in_ms, 548 | exclude_filters=filters, 549 | ) 550 | 551 | def get_nfts( 552 | self, 553 | owner: str, 554 | page_key: Optional[str] = None, 555 | page_size: Optional[int] = 100, 556 | contract_addresses: Optional[List[str]] = None, 557 | with_metadata: Optional[bool] = False, 558 | token_uri_timeout_in_ms: Union[int, None] = None, 559 | exclude_filters: Optional[List[str]] = None, 560 | include_filters: Optional[List[str]] = None, 561 | order_by: Optional[str] = None, 562 | ) -> dict: 563 | """Gets all NFTs currently owned by a given address. 564 | 565 | params: 566 | owner: Address of the owner to get NFTs for 567 | page_key: Optional page key to get the next page of results 568 | page_size: Optional page size to get the next page of results 569 | contract_addresses: Optional list of contract addresses to get NFTs for 570 | with_metadata: Optional boolean to include metadata 571 | token_uri_timeout_in_ms: Optional timeout in ms for token URI requests 572 | exclude_filters: Optional list of strings from "SPAM" and "AIRDROPS" 573 | include_filters: Optional list of strings from "SPAM" and "AIRDROPS" 574 | order_by: Optional string to order by "transferTime" or None 575 | 576 | returns: 577 | Dictionary of NFTs 578 | """ 579 | params = {"owner": owner} 580 | if page_key: 581 | params["pageKey"] = page_key 582 | if page_size: 583 | params["pageSize"] = page_size 584 | if contract_addresses: 585 | params["contractAddresses"] = contract_addresses 586 | if with_metadata: 587 | params["withMetadata"] = with_metadata 588 | if token_uri_timeout_in_ms: 589 | params["tokenUriTimeoutInMs"] = token_uri_timeout_in_ms 590 | if exclude_filters: 591 | params["excludeFilters"] = exclude_filters 592 | if include_filters: 593 | params["includeFilters"] = include_filters 594 | if order_by: 595 | params["orderBy"] = order_by 596 | json_response = self._handle_get_call( 597 | "getNFTs", 598 | params=params, 599 | endpoint="getNFTsForOwner", 600 | url=self.nft_url, 601 | ) 602 | return json_response 603 | 604 | def get_owners_for_token( 605 | self, contract_address: str, token_id: Union[str, int] 606 | ) -> dict: 607 | """Get the owner(s) for a token. 608 | 609 | Args: 610 | contract_address (str): Contract address of the token 611 | token_id (Union[str, int, None]): Token ID of the token 612 | 613 | Returns: 614 | dict: Dictionary of owners 615 | """ 616 | params = {"contractAddress": contract_address} 617 | params["tokenId"] = HexIntStringNumber(token_id).int 618 | json_response = self._handle_get_call( 619 | "getOwnersForToken", 620 | params=params, 621 | endpoint="getOwnersForToken", 622 | url=self.nft_url, 623 | ) 624 | return json_response 625 | 626 | def get_owners_for_nft( 627 | self, contract_address: str, token_id: Union[str, int] 628 | ) -> dict: 629 | """Get the owner(s) for a token. 630 | 631 | Args: 632 | contract_address (str): Contract address of the token 633 | token_id (Union[str, int, None]): Token ID of the token 634 | 635 | Returns: 636 | dict: Dictionary of owners 637 | """ 638 | return self.get_owners_for_token(contract_address, token_id) 639 | 640 | def get_owners_for_collection( 641 | self, 642 | contract_address: str, 643 | with_token_balances: bool, 644 | block: Union[str, int], 645 | page_key: Optional[str] = None, 646 | ) -> dict: 647 | """Gets all owners for a given NFT contract. 648 | 649 | Returns: 650 | dict: Dictionary of owners 651 | """ 652 | params = { 653 | "contractAddress": contract_address, 654 | "withTokenBalances": with_token_balances or False, 655 | "block": HexIntStringNumber(block).str, 656 | } 657 | if page_key: 658 | params["pageKey"] = page_key 659 | json_response = self._handle_get_call( 660 | "getOwnersForCollection", 661 | params=params, 662 | endpoint="getOwnersForCollection", 663 | url=self.nft_url, 664 | ) 665 | return json_response 666 | 667 | def is_holder_of_collection(self, wallet: str, contract_address: str) -> dict: 668 | """Checks if a wallet is a holder of a collection. 669 | 670 | Args: 671 | wallet (str): Wallet address to check 672 | contract_address (str): Contract address of the collection 673 | 674 | Returns: 675 | dict: Dictionary of owners 676 | """ 677 | params = { 678 | "wallet": wallet, 679 | "contractAddress": contract_address, 680 | } 681 | json_response = self._handle_get_call( 682 | "isHolderOfCollection", 683 | params=params, 684 | endpoint="isHolderOfCollection", 685 | url=self.nft_url, 686 | ) 687 | return json_response 688 | 689 | def get_contracts_for_owner( 690 | self, 691 | owner: str, 692 | page_key: Optional[str] = None, 693 | page_size: Optional[int] = 100, 694 | include_filters: Optional[List[str]] = None, 695 | exclude_filters: Optional[List[str]] = None, 696 | order_by: Optional[str] = None, 697 | ) -> dict: 698 | """Gets all contracts for a given owner. 699 | 700 | Args: 701 | owner (str): Owner address to check 702 | page_key (Optional[str], optional): Page key to get the next page of results. Defaults to None. 703 | page_size (Optional[int], optional): Page size to get the next page of results. Defaults to 100. 704 | include_filters (Optional[List[str]], optional): Optional list of strings from "SPAM" and "AIRDROPS". Defaults to None. 705 | exclude_filters (Optional[List[str]], optional): Optional list of strings from "SPAM" and "AIRDROPS". Defaults to None. 706 | order_by (Optional[str], optional): Optional string to order by "transferTime" or None. Defaults to None. 707 | 708 | Returns: 709 | dict: Dictionary of contracts 710 | """ 711 | params = { 712 | "owner": owner, 713 | } 714 | if page_key: 715 | params["pageKey"] = page_key 716 | if page_size: 717 | params["pageSize"] = page_size 718 | if include_filters: 719 | params["includeFilters"] = include_filters 720 | if exclude_filters: 721 | params["excludeFilters"] = exclude_filters 722 | if order_by: 723 | params["orderBy"] = order_by 724 | json_response = self._handle_get_call( 725 | "getContractsForOwner", 726 | params=params, 727 | endpoint="getContractsForOwner", 728 | url=self.nft_url, 729 | ) 730 | return json_response 731 | 732 | def get_nft_metadata( 733 | self, 734 | contract_address: str, 735 | token_id: Union[str, int], 736 | token_type: str = "ERC721", 737 | token_uri_timeout_in_ms: int = 0, 738 | refresh_cache: bool = False, 739 | ) -> dict: 740 | """Gets the metadata for a given NFT. 741 | 742 | Args: 743 | contract_address (str): Contract address of the NFT 744 | token_id (Union[str, int]): Token ID of the NFT 745 | token_type (str, optional): Token type of the NFT. Defaults to "ERC721", can also be "ERC1155". 746 | token_uri_timeout_in_ms (int, optional): Timeout in ms for the token URI. Defaults to 0. 747 | refresh_cache (bool, optional): Refresh the cache. Defaults to False. 748 | 749 | Returns: 750 | dict: Dictionary of metadata 751 | """ 752 | params = { 753 | "contractAddress": contract_address, 754 | "tokenId": HexIntStringNumber(token_id).int, 755 | } 756 | if token_type: 757 | params["tokenType"] = token_type 758 | if token_uri_timeout_in_ms: 759 | params["tokenUriTimeoutInMs"] = token_uri_timeout_in_ms 760 | if refresh_cache: 761 | params["refreshCache"] = refresh_cache 762 | json_response = self._handle_get_call( 763 | "getNFTMetadata", 764 | params=params, 765 | endpoint="getNFTMetadata", 766 | url=self.nft_url, 767 | ) 768 | return json_response 769 | 770 | def get_contract_metadata(self, contract_address: str) -> dict: 771 | """Queries NFT high-level collection/contract level information. 772 | 773 | Args: 774 | contract_address (str): Contract address of the NFT 775 | 776 | Returns: 777 | dict: Dictionary of metadata 778 | """ 779 | params = { 780 | "contractAddress": contract_address, 781 | } 782 | json_response = self._handle_get_call( 783 | "getContractMetadata", 784 | params=params, 785 | endpoint="getContractMetadata", 786 | url=self.nft_url, 787 | ) 788 | return json_response 789 | 790 | def get_nfts_for_contract( 791 | self, 792 | contract_address: str, 793 | omit_metadata: bool = False, 794 | start_token: Union[str, int, None] = None, 795 | limit: Optional[int] = 100, 796 | token_uri_timeout_in_ms: Optional[int] = None, 797 | ) -> dict: 798 | """Gets all NFTs for a given NFT contract. 799 | 800 | Args: 801 | contract_address (str): Contract address of the NFT 802 | omit_metadata (bool, optional): Omit metadata. Defaults to False. 803 | startToken (Union[str, int, None], optional): Start token. Defaults to None. 804 | limit (Optional[int], optional): Limit. Defaults to 100. 805 | token_uri_timeout_in_ms (Optional[int], optional): Token URI timeout in ms. Defaults to None. 806 | 807 | Returns: 808 | dict: Dictionary of NFTs 809 | """ 810 | return self.get_nfts_for_collection( 811 | contract_address, 812 | with_metadata=not omit_metadata, 813 | start_token=start_token, 814 | limit=limit, 815 | token_uri_timeout_in_ms=token_uri_timeout_in_ms, 816 | ) 817 | 818 | def get_nfts_for_collection( 819 | self, 820 | contract_address: str, 821 | with_metadata: Optional[bool] = False, 822 | start_token: Optional[int] = None, 823 | limit: Optional[int] = 100, 824 | token_uri_timeout_in_ms: Optional[int] = None, 825 | ) -> dict: 826 | """Gets all NFTs for a given NFT contract. 827 | 828 | Args: 829 | contract_address (str): Contract address of the NFT 830 | with_metadata (Optional[bool], optional): With metadata. Defaults to False. 831 | start_token (Optional[int], optional): Start token. Defaults to None. 832 | limit (Optional[int], optional): Limit. Defaults to 100. 833 | token_uri_timeout_in_ms (Optional[int], optional): Token URI timeout in ms. Defaults to None. 834 | 835 | Returns: 836 | dict: Dictionary of NFTs 837 | """ 838 | params = { 839 | "contractAddress": contract_address, 840 | } 841 | if with_metadata: 842 | params["withMetadata"] = with_metadata 843 | if start_token: 844 | params["startToken"] = start_token 845 | if limit: 846 | params["limit"] = limit 847 | if token_uri_timeout_in_ms: 848 | params["tokenUriTimeoutInMs"] = token_uri_timeout_in_ms 849 | json_response = self._handle_get_call( 850 | "getNFTsForCollection", 851 | params=params, 852 | endpoint="getNFTsForCollection", 853 | url=self.nft_url, 854 | ) 855 | return json_response 856 | 857 | def get_owners_for_contract( 858 | self, 859 | contract_address: str, 860 | with_token_balances: Optional[bool], 861 | block: Union[str, int], 862 | page_key: Optional[str] = None, 863 | ) -> dict: 864 | """Gets all owners for a given NFT contract. 865 | 866 | Returns: 867 | dict: Dictionary of owners 868 | """ 869 | return self.get_owners_for_collection( 870 | contract_address, 871 | with_token_balances, 872 | block, 873 | page_key=page_key, 874 | ) 875 | 876 | def get_spam_contracts(self) -> dict: 877 | """Gets all spam contracts. 878 | 879 | Returns: 880 | dict: Dictionary of spam contracts 881 | """ 882 | json_response = self._handle_get_call( 883 | "getSpamContracts", 884 | endpoint="getSpamContracts", 885 | url=self.nft_url, 886 | ) 887 | return json_response 888 | 889 | def is_spam_contract(self, contract_address: str) -> bool: 890 | """Checks if a contract is a spam contract. 891 | 892 | Returns: 893 | bool: True if spam contract, False otherwise 894 | """ 895 | spam_contracts = self.get_spam_contracts() 896 | return contract_address in spam_contracts 897 | 898 | def reingest_contract(self, contract_address: str) -> dict: 899 | """Refreshes a contract. 900 | 901 | Returns: 902 | dict: Dictionary of refreshed contract 903 | """ 904 | params = { 905 | "contractAddress": contract_address, 906 | } 907 | json_response = self._handle_get_call( 908 | "reingestContract", 909 | params=params, 910 | endpoint="reingestContract", 911 | url=self.nft_url, 912 | ) 913 | return json_response 914 | 915 | def refresh_contract(self, contract_address: str) -> dict: 916 | """Reingests a contract. 917 | 918 | Returns: 919 | dict: Dictionary of reingested contract 920 | """ 921 | return self.reingest_contract(contract_address) 922 | 923 | def get_floor_price(self, contract_address) -> dict: 924 | """Gets floor price for a contract. 925 | 926 | Returns: 927 | dict: Dictionary of floor price 928 | """ 929 | params = { 930 | "contractAddress": contract_address, 931 | } 932 | json_response = self._handle_get_call( 933 | "getFloorPrice", 934 | params=params, 935 | endpoint="getFloorPrice", 936 | url=self.nft_url, 937 | ) 938 | return json_response 939 | 940 | def compute_rarity(self, contract_address: str, token_id: Union[str, int]) -> dict: 941 | """Computes rarity for a given token. 942 | 943 | Returns: 944 | dict: Dictionary of rarity 945 | """ 946 | params = { 947 | "contractAddress": contract_address, 948 | "tokenId": HexIntStringNumber(token_id).hex, 949 | } 950 | json_response = self._handle_get_call( 951 | "computeRarity", 952 | params=params, 953 | endpoint="computeRarity", 954 | url=self.nft_url, 955 | ) 956 | return json_response 957 | 958 | def verify_nft_ownership( 959 | self, wallet_address: str, contract_addresses: Union[str, List[str]] 960 | ) -> dict: 961 | """Verifies if a wallet owns a given NFT. 962 | 963 | Returns: 964 | dict: Returns a dict of contract addresses and whether or not the wallet owns the NFT 965 | """ 966 | if isinstance(contract_addresses, str): 967 | contract_addresses = [contract_addresses] 968 | contract_addresses = [contract.lower() for contract in contract_addresses] 969 | nfts_for_owner = self.get_nfts_for_owner( 970 | wallet_address, contract_addresses=contract_addresses, omit_metadata=True 971 | ) 972 | contract_addresses_dict = {contract: False for contract in contract_addresses} 973 | for nft in nfts_for_owner["ownedNfts"]: 974 | if nft["contract"]["address"].lower() in contract_addresses_dict: 975 | contract_addresses_dict[nft["contract"]["address"]] = True 976 | return contract_addresses_dict 977 | 978 | ############################################################ 979 | ################ Transact Methods ########################## 980 | ############################################################ 981 | 982 | def get_transaction(self, transaction_hash: str) -> dict: 983 | """Gets a transaction by hash. 984 | 985 | Returns: 986 | dict: Dictionary of transaction 987 | """ 988 | return self.get_transaction_by_hash(transaction_hash) 989 | 990 | def send_transaction(self, signed_transaction: str) -> dict: 991 | """Sends a signed transaction. 992 | 993 | Returns: 994 | dict: Dictionary of transaction 995 | """ 996 | return self.send_raw_transaction(signed_transaction) 997 | 998 | def send_private_transaction( 999 | self, 1000 | method: str = "eth_sendPrivateTransaction", 1001 | tx: str = None, 1002 | max_block_number: Union[int, str] = 999999999, 1003 | fast: bool = False, 1004 | ) -> dict: 1005 | """Sends a private transaction. 1006 | 1007 | Returns: 1008 | dict: Dictionary of transaction 1009 | """ 1010 | params = [ 1011 | { 1012 | "tx": tx, 1013 | "maxBlockNumber": HexIntStringNumber(max_block_number).hex, 1014 | "preferences": {"fast": fast}, 1015 | } 1016 | ] 1017 | response = self.send(method, params) 1018 | return response 1019 | 1020 | def cancel_private_transaction( 1021 | self, 1022 | transaction_hash: str = None, 1023 | ) -> dict: 1024 | """Cancels a private transaction. 1025 | 1026 | Returns: 1027 | dict: Dictionary of transaction 1028 | """ 1029 | method = "eth_cancelPrivateTransaction" 1030 | params = [ 1031 | { 1032 | "txHash": transaction_hash, 1033 | } 1034 | ] 1035 | response = self.send(method, params) 1036 | return response 1037 | 1038 | def wait_for_transaction( 1039 | self, 1040 | transaction_hash: str, 1041 | confirmations: Optional[int] = 1, 1042 | timeout: Optional[int] = 60, 1043 | ) -> dict: 1044 | """Waits for a transaction to be confirmed. 1045 | 1046 | Returns: 1047 | dict: Dictionary of transaction 1048 | """ 1049 | receipt = self.get_transaction_receipt(transaction_hash) 1050 | if receipt["confirmations"] >= confirmations: 1051 | return receipt 1052 | # start a loop that will check the number of confirmatoins every 5 seconds 1053 | start_time = time.time() 1054 | while ( 1055 | time.time() - start_time < timeout 1056 | and receipt["confirmations"] < confirmations 1057 | ): 1058 | time.sleep(5) 1059 | receipt = self.get_transaction_receipt(transaction_hash) 1060 | if receipt["confirmations"] >= confirmations: 1061 | return receipt 1062 | raise TimeoutError( 1063 | f"Transaction {transaction_hash} did not get {confirmations} confirmations in {timeout} seconds" 1064 | ) 1065 | 1066 | ############################################################ 1067 | ################ Settings Methods ########################## 1068 | ############################################################ 1069 | 1070 | def set_api_key(self, api_key: str): 1071 | """ 1072 | params: 1073 | key: API key 1074 | returns: 1075 | None 1076 | """ 1077 | if not isinstance(api_key, str): 1078 | raise ValueError(NO_API_KEY_ERROR) 1079 | self.api_key = api_key 1080 | 1081 | def set_network(self, network: str): 1082 | """ 1083 | params: 1084 | network: Network to use 1085 | returns: 1086 | None 1087 | """ 1088 | self.network = Network(network) 1089 | url_network_name = self.network.name.replace("_", "-") 1090 | self.base_url_without_key = f"https://{url_network_name}.g.alchemy.com/v2/" 1091 | self.base_url = f"{self.base_url_without_key}{self.api_key}" 1092 | 1093 | def set_settings(self, key: Optional[str] = None, network: Optional[str] = None): 1094 | """ 1095 | params: 1096 | key: API key 1097 | network: Network to use 1098 | returns: 1099 | None 1100 | """ 1101 | if key: 1102 | self.set_api_key(key) 1103 | if network: 1104 | self.set_network(network) 1105 | 1106 | ############################################################ 1107 | ################ Internal/Raw Methods ###################### 1108 | ############################################################ 1109 | 1110 | def _handle_get_call( 1111 | self, 1112 | rest_endpoint: str, 1113 | params: Optional[dict] = None, 1114 | endpoint: Optional[str] = None, 1115 | url: Optional[str] = None, 1116 | ) -> dict: 1117 | """Handles a GET call to the Alchemy backend. Sort of shitty. Should be refactored with `_handle_api_call`. 1118 | 1119 | params: 1120 | rest_endpoint: REST endpoint to call 1121 | params: Optional dictionary of parameters to pass to the endpoint 1122 | endpoint: Optional endpoint to pass to the backend 1123 | url: Optional URL to call 1124 | returns: 1125 | Dictionary of the response 1126 | """ 1127 | url = self.base_url if url is None else url 1128 | url = f"{url}/{rest_endpoint}" 1129 | headers = HEADERS 1130 | if endpoint is not None: 1131 | headers["Alchemy-Python-Sdk-Method"] = endpoint 1132 | response = requests.get(url, params=params, headers=headers, proxies=self.proxy) 1133 | if response.status_code != 200: 1134 | retries_here = 0 1135 | while retries_here < self.retries and response.status_code != 200: 1136 | retries_here = retries_here + 1 1137 | response = requests.post( 1138 | url, params=params, headers=headers, proxies=self.proxy 1139 | ) 1140 | if response.status_code != 200: 1141 | raise ConnectionError( 1142 | f"Status {response.status_code} with params {params}:\n >>> Response with Error: {response.text}" 1143 | ) 1144 | json_response = response.json() 1145 | if isinstance(json_response, dict): 1146 | if json_response.get("error", None) is not None: 1147 | raise ConnectionError( 1148 | f"Status {response.status_code} with params {params}:\n >>> Response with Error: {response.text}" 1149 | ) 1150 | self.call_id = self.call_id + 1 1151 | return json_response 1152 | --------------------------------------------------------------------------------