diff --git a/poetry.lock b/poetry.lock index 7f859f4c..d1466dc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -285,6 +285,28 @@ files = [ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] +[[package]] +name = "eth-hash" +version = "0.5.2" +description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "eth-hash-0.5.2.tar.gz", hash = "sha256:1b5f10eca7765cc385e1430eefc5ced6e2e463bb18d1365510e2e539c1a6fe4e"}, + {file = "eth_hash-0.5.2-py3-none-any.whl", hash = "sha256:251f62f6579a1e247561679d78df37548bd5f59908da0b159982bf8293ad32f0"}, +] + +[package.dependencies] +pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} + +[package.extras] +dev = ["black (>=23)", "build (>=0.9.0)", "bumpversion (>=0.5.3)", "flake8 (==6.0.0)", "flake8-bugbear (==23.3.23)", "ipython", "isort (>=5.10.1)", "mypy (==0.971)", "pydocstyle (>=6.0.0)", "pytest (>=7.0.0)", "pytest-watch (>=4.1.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] +doc = ["sphinx (>=6.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] +lint = ["black (>=23)", "flake8 (==6.0.0)", "flake8-bugbear (==23.3.23)", "isort (>=5.10.1)", "mypy (==0.971)", "pydocstyle (>=6.0.0)"] +pycryptodome = ["pycryptodome (>=3.6.6,<4)"] +pysha3 = ["pysha3 (>=1.0.0,<2.0.0)", "safe-pysha3 (>=1.0.0)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + [[package]] name = "exceptiongroup" version = "1.1.1" @@ -972,6 +994,47 @@ files = [ {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] +[[package]] +name = "pycryptodome" +version = "3.18.0" +description = "Cryptographic library for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodome-3.18.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:d1497a8cd4728db0e0da3c304856cb37c0c4e3d0b36fcbabcc1600f18504fc54"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:928078c530da78ff08e10eb6cada6e0dff386bf3d9fa9871b4bbc9fbc1efe024"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:157c9b5ba5e21b375f052ca78152dd309a09ed04703fd3721dce3ff8ecced148"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:d20082bdac9218649f6abe0b885927be25a917e29ae0502eaf2b53f1233ce0c2"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e8ad74044e5f5d2456c11ed4cfd3e34b8d4898c0cb201c4038fe41458a82ea27"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-win32.whl", hash = "sha256:62a1e8847fabb5213ccde38915563140a5b338f0d0a0d363f996b51e4a6165cf"}, + {file = "pycryptodome-3.18.0-cp27-cp27m-win_amd64.whl", hash = "sha256:16bfd98dbe472c263ed2821284118d899c76968db1a6665ade0c46805e6b29a4"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7a3d22c8ee63de22336679e021c7f2386f7fc465477d59675caa0e5706387944"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:78d863476e6bad2a592645072cc489bb90320972115d8995bcfbee2f8b209918"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b6a610f8bfe67eab980d6236fdc73bfcdae23c9ed5548192bb2d530e8a92780e"}, + {file = "pycryptodome-3.18.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:422c89fd8df8a3bee09fb8d52aaa1e996120eafa565437392b781abec2a56e14"}, + {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:9ad6f09f670c466aac94a40798e0e8d1ef2aa04589c29faa5b9b97566611d1d1"}, + {file = "pycryptodome-3.18.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:53aee6be8b9b6da25ccd9028caf17dcdce3604f2c7862f5167777b707fbfb6cb"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:10da29526a2a927c7d64b8f34592f461d92ae55fc97981aab5bbcde8cb465bb6"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f21efb8438971aa16924790e1c3dba3a33164eb4000106a55baaed522c261acf"}, + {file = "pycryptodome-3.18.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4944defabe2ace4803f99543445c27dd1edbe86d7d4edb87b256476a91e9ffa4"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:51eae079ddb9c5f10376b4131be9589a6554f6fd84f7f655180937f611cd99a2"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:83c75952dcf4a4cebaa850fa257d7a860644c70a7cd54262c237c9f2be26f76e"}, + {file = "pycryptodome-3.18.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:957b221d062d5752716923d14e0926f47670e95fead9d240fa4d4862214b9b2f"}, + {file = "pycryptodome-3.18.0-cp35-abi3-win32.whl", hash = "sha256:795bd1e4258a2c689c0b1f13ce9684fa0dd4c0e08680dcf597cf9516ed6bc0f3"}, + {file = "pycryptodome-3.18.0-cp35-abi3-win_amd64.whl", hash = "sha256:b1d9701d10303eec8d0bd33fa54d44e67b8be74ab449052a8372f12a66f93fb9"}, + {file = "pycryptodome-3.18.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:cb1be4d5af7f355e7d41d36d8eec156ef1382a88638e8032215c215b82a4b8ec"}, + {file = "pycryptodome-3.18.0-pp27-pypy_73-win32.whl", hash = "sha256:fc0a73f4db1e31d4a6d71b672a48f3af458f548059aa05e83022d5f61aac9c08"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f022a4fd2a5263a5c483a2bb165f9cb27f2be06f2f477113783efe3fe2ad887b"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363dd6f21f848301c2dcdeb3c8ae5f0dee2286a5e952a0f04954b82076f23825"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12600268763e6fec3cefe4c2dcdf79bde08d0b6dc1813887e789e495cb9f3403"}, + {file = "pycryptodome-3.18.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4604816adebd4faf8810782f137f8426bf45fee97d8427fa8e1e49ea78a52e2c"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:01489bbdf709d993f3058e2996f8f40fee3f0ea4d995002e5968965fa2fe89fb"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3811e31e1ac3069988f7a1c9ee7331b942e605dfc0f27330a9ea5997e965efb2"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4b967bb11baea9128ec88c3d02f55a3e338361f5e4934f5240afcb667fdaec"}, + {file = "pycryptodome-3.18.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c8eda4f260072f7dbe42f473906c659dcbadd5ae6159dfb49af4da1293ae380"}, + {file = "pycryptodome-3.18.0.tar.gz", hash = "sha256:c9adee653fc882d98956e33ca2c1fb582e23a8af7ac82fee75bd6113c55a0413"}, +] + [[package]] name = "pydocstyle" version = "6.3.0" @@ -1662,4 +1725,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "228af2ad6c7eccee9ea51a5667ef5b0cd5ae287fdc084ff8fb66ff9c4649ccb4" +content-hash = "228af2ad6c7eccee9ea51a5667ef5b0cd5ae287fdc084ff8fb66ff9c4649ccb4" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d0993845..4471913e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ include = ["CHANGES.md", "docs/*", "docs/validators.1", "validators/py.typed"] [tool.poetry.dependencies] python = "^3.8" +eth-hash = {extras = ["pycryptodome"], version = "^0.5.2"} [tool.poetry.group.docs] optional = true diff --git a/tests/crypto_addresses/__init__.py b/tests/crypto_addresses/__init__.py new file mode 100644 index 00000000..956d8177 --- /dev/null +++ b/tests/crypto_addresses/__init__.py @@ -0,0 +1,4 @@ +"""Test crypto addresses.""" +# -*- coding: utf-8 -*- + +# isort: skip_file diff --git a/tests/crypto_addresses/test_eth_address.py b/tests/crypto_addresses/test_eth_address.py new file mode 100644 index 00000000..4c0c8814 --- /dev/null +++ b/tests/crypto_addresses/test_eth_address.py @@ -0,0 +1,45 @@ +"""Test ETH address.""" +# -*- coding: utf-8 -*- + +# external +import pytest + +# local +from validators import eth_address, ValidationFailure + + +@pytest.mark.parametrize( + "value", + [ + "0x8ba1f109551bd432803012645ac136ddd64dba72", + "0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598", + "0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE", + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "0x1234567890123456789012345678901234567890", + "0x57Ab1ec28D129707052df4dF418D58a2D46d5f51", + ], +) +def test_returns_true_on_valid_eth_address(value: str): + """Test returns true on valid eth address.""" + assert eth_address(value) + + +@pytest.mark.parametrize( + "value", + [ + "0x742d35Cc6634C0532925a3b844Bc454e4438f44g", + "0x742d35Cc6634C0532925a3b844Bc454e4438f44", + "0xAbcdefg1234567890Abcdefg1234567890Abcdefg", + "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c72", + "0x80fBD7F8B3f81D0e1d6EACAb69AF104A6508AFB1", + "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c7g", + "0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c", + "0x7Fb21a171205f3B8d8E4d88A2d2f8A56E45DdB5c", + "validators.eth", + ], +) +def test_returns_failed_validation_on_invalid_eth_address(value: str): + """Test returns failed validation on invalid eth address.""" + assert isinstance(eth_address(value), ValidationFailure) diff --git a/validators/__init__.py b/validators/__init__.py index c78c27ab..2df05054 100644 --- a/validators/__init__.py +++ b/validators/__init__.py @@ -24,12 +24,14 @@ from .utils import validator, ValidationFailure from .uuid import uuid +from .crypto_addresses import eth_address from .i18n import es_cif, es_doi, es_nie, es_nif, fi_business_id, fi_ssn __all__ = ( "amex", "between", "btc_address", + "eth_address", "card_number", "diners", "discover", diff --git a/validators/crypto_addresses/__init__.py b/validators/crypto_addresses/__init__.py new file mode 100644 index 00000000..1c9bd5c6 --- /dev/null +++ b/validators/crypto_addresses/__init__.py @@ -0,0 +1,9 @@ +"""Crypto addresses.""" +# -*- coding: utf-8 -*- + +# isort: skip_file + +# local +from .eth_address import eth_address + +__all__ = ("eth_address",) diff --git a/validators/crypto_addresses/eth_address.py b/validators/crypto_addresses/eth_address.py new file mode 100644 index 00000000..80aaed41 --- /dev/null +++ b/validators/crypto_addresses/eth_address.py @@ -0,0 +1,58 @@ +"""ETH Address.""" +# -*- coding: utf-8 -*- + +# standard +import re + +# external +from eth_hash.auto import keccak + +# local +from validators.utils import validator + + +def _validate_eth_checksum_address(addr: str): + """Validate ETH type checksum address.""" + addr = addr.replace("0x", "") + addr_hash = keccak.new(addr.lower().encode("ascii")).digest().hex() + + if len(addr) != 40: + return False + + for i in range(0, 40): + if (int(addr_hash[i], 16) > 7 and addr[i].upper() != addr[i]) or ( + int(addr_hash[i], 16) <= 7 and addr[i].lower() != addr[i] + ): + return False + return True + + +@validator +def eth_address(value: str, /): + """Return whether or not given value is a valid ethereum address. + + Full validation is implemented for ERC20 addresses. + + Examples: + >>> eth_address('0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598') + # Output: True + >>> eth_address('0x8Ba1f109551bD432803012645Ac136ddd64DBa72') + # Output: ValidationFailure(func=eth_address, args=...) + + Args: + value: + Ethereum address string to validate. + + Returns: + (Literal[True]): + If `value` is a valid ethereum address. + (ValidationFailure): + If `value` is an invalid ethereum address. + + """ + if not value: + return False + + return re.compile(r"^0x[0-9a-f]{40}$|^0x[0-9A-F]{40}$").match( + value + ) or _validate_eth_checksum_address(value)