├── 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 |
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 |
--------------------------------------------------------------------------------