diff --git a/poetry.lock b/poetry.lock index 7f859f4c..5768d2e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -51,6 +51,18 @@ test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", toml = ["tomli (>=1.1.0)"] yaml = ["PyYAML"] +[[package]] +name = "base58" +version = "2.1.1" +description = "Base58 and Base58Check implementation." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, + {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, +] + [[package]] name = "black" version = "23.3.0" @@ -1087,6 +1099,37 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[[package]] +name = "pysha3" +version = "1.0.2" +description = "SHA-3 (Keccak) for Python 2.7 - 3.5" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pysha3-1.0.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6e6a84efb7856f5d760ee55cd2b446972cb7b835676065f6c4f694913ea8f8d9"}, + {file = "pysha3-1.0.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f9046d59b3e72aa84f6dae83a040bd1184ebd7fef4e822d38186a8158c89e3cf"}, + {file = "pysha3-1.0.2-cp27-cp27m-win32.whl", hash = "sha256:9fdd28884c5d0b4edfed269b12badfa07f1c89dbc5c9c66dd279833894a9896b"}, + {file = "pysha3-1.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:41be70b06c8775a9e4d4eeb52f2f6a3f356f17539a54eac61f43a29e42fd453d"}, + {file = "pysha3-1.0.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:68c3a60a39f9179b263d29e221c1bd6e01353178b14323c39cc70593c30f21c5"}, + {file = "pysha3-1.0.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:59111c08b8f34495575d12e5f2ce3bafb98bea470bc81e70c8b6df99aef0dd2f"}, + {file = "pysha3-1.0.2-cp33-cp33m-win32.whl", hash = "sha256:571a246308a7b63f15f5aa9651f99cf30f2a6acba18eddf28f1510935968b603"}, + {file = "pysha3-1.0.2-cp33-cp33m-win_amd64.whl", hash = "sha256:93abd775dac570cb9951c4e423bcb2bc6303a9d1dc0dc2b7afa2dd401d195b24"}, + {file = "pysha3-1.0.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:11a2ba7a2e1d9669d0052fc8fb30f5661caed5512586ecbeeaf6bf9478ab5c48"}, + {file = "pysha3-1.0.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:5ec8da7c5c70a53b5fa99094af3ba8d343955b212bc346a0d25f6ff75853999f"}, + {file = "pysha3-1.0.2-cp34-cp34m-win32.whl", hash = "sha256:9c778fa8b161dc9348dc5cc361e94d54aa5ff18413788f4641f6600d4893a608"}, + {file = "pysha3-1.0.2-cp34-cp34m-win_amd64.whl", hash = "sha256:fd7e66999060d079e9c0e8893e78d8017dad4f59721f6fe0be6307cd32127a07"}, + {file = "pysha3-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:827b308dc025efe9b6b7bae36c2e09ed0118a81f792d888548188e97b9bf9a3d"}, + {file = "pysha3-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4416f16b0f1605c25f627966f76873e432971824778b369bd9ce1bb63d6566d9"}, + {file = "pysha3-1.0.2-cp35-cp35m-win32.whl", hash = "sha256:c93a2676e6588abcfaecb73eb14485c81c63b94fca2000a811a7b4fb5937b8e8"}, + {file = "pysha3-1.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:684cb01d87ed6ff466c135f1c83e7e4042d0fc668fa20619f581e6add1d38d77"}, + {file = "pysha3-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:386998ee83e313b6911327174e088021f9f2061cbfa1651b97629b761e9ef5c4"}, + {file = "pysha3-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c7c2adcc43836223680ebdf91f1d3373543dc32747c182c8ca2e02d1b69ce030"}, + {file = "pysha3-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:cd5c961b603bd2e6c2b5ef9976f3238a561c58569945d4165efb9b9383b050ef"}, + {file = "pysha3-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0060a66be16665d90c432f55a0ba1f6480590cfb7d2ad389e688a399183474f0"}, + {file = "pysha3-1.0.2.tar.gz", hash = "sha256:fe988e73f2ce6d947220624f04d467faf05f1bbdbc64b0a201296bb3af92739e"}, +] + [[package]] name = "pytest" version = "7.3.0" 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/test_btc_address.py b/tests/crypto_addresses/test_btc_address.py similarity index 100% rename from tests/test_btc_address.py rename to tests/crypto_addresses/test_btc_address.py 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/tests/crypto_addresses/test_trx_address.py b/tests/crypto_addresses/test_trx_address.py new file mode 100644 index 00000000..3d5c3c41 --- /dev/null +++ b/tests/crypto_addresses/test_trx_address.py @@ -0,0 +1,54 @@ +"""Test TRX address.""" +# -*- coding: utf-8 -*- + +# external +import pytest + +# local +from validators import trx_address, ValidationFailure + + +@pytest.mark.parametrize( + "value", + [ + "TLjfbTbpZYDQ4EoA4N5CLNgGjfbF8ZWz38", + "TDQ6C92wuNqvMWE967sMptCFaXq77uj1PF", + "TFuGbxCQGSL4oLnJzVsen844LDwFbrUY4e", + "TFAPKADDRhkSe3v27CsR8TZSjN8eJ8ycDK", + "TSJHywLNva2MNjCD5iYfn5QAKD9Rk5Ncit", + "TEi1qhi5LuTicg1u9oAstyXCSf5uibSyqo", + "TAGvx5An6VBeHTu91cQwdABNcAYMRPcP4n", + "TXbE5tXTejqT3Q47sYKCDb9NJDm3xrFpab", + "TMTxQWNuWHXvHcYXc5D1wQhFmZFJijAxcG", + "TPHgw9E8QYM3esNWih5KVnUVpUHwLTPfpA", + "TFFLtBTi9jdaGwV3hznjCmPYaJme5AeqwU", + ], +) +def test_returns_true_on_valid_trx_address(value: str): + """Test returns true on valid trx address.""" + assert trx_address(value) + + +@pytest.mark.parametrize( + "value", + [ + "T12345678901234567890123456789012345", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ12345678", + "TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vd", + "TP6ah2v5mdsj8Z3hGz1yDMvDq7BzEbK8o", + "TQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4XX", + "TQNy2C6VHJPk4P32bsEX3QSGx2Qqm4J2k9", + "TP6ah2v5mdsj8Z3hGz1yDMvDq7BzEbK8oN", + "TSTVdfU1x4L7K3Bc3v5C28Gp2J1rPyeL3f", + "THPByuCzvU5QER9j2NC2mUQ2JPyRCam4e7", + "TW5eZqUZgdW4rxFKAKsc2ryJbfFA94WXvD", + "TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vdd", + "tQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4X", + "TR2G7Rm4vFqF8EpY4U5xdLdQ7Xg", + "TQmmhp6uz2Xre8yL3FsPYZyo4mhtw4vg4x", + "my-trox-address.trx" + ], +) +def test_returns_failed_validation_on_invalid_trx_address(value: str): + """Test returns failed validation on invalid trx address.""" + assert isinstance(trx_address(value), ValidationFailure) diff --git a/validators/__init__.py b/validators/__init__.py index c78c27ab..b84a5c1b 100644 --- a/validators/__init__.py +++ b/validators/__init__.py @@ -9,7 +9,6 @@ # local from .between import between -from .btc_address import btc_address from .card import amex, card_number, diners, discover, jcb, mastercard, unionpay, visa from .domain import domain from .email import email @@ -24,12 +23,14 @@ from .utils import validator, ValidationFailure from .uuid import uuid +from .crypto_addresses import btc_address, eth_address, trx_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", @@ -49,6 +50,7 @@ "sha256", "sha512", "slug", + "trx_address", "unionpay", "url", "uuid", diff --git a/validators/crypto_addresses/__init__.py b/validators/crypto_addresses/__init__.py new file mode 100644 index 00000000..25f1183d --- /dev/null +++ b/validators/crypto_addresses/__init__.py @@ -0,0 +1,11 @@ +"""Crypto addresses.""" +# -*- coding: utf-8 -*- + +# isort: skip_file + +# local +from .btc_address import btc_address +from .eth_address import eth_address +from .trx_address import trx_address + +__all__ = ("btc_address", "eth_address", "trx_address") diff --git a/validators/btc_address.py b/validators/crypto_addresses/btc_address.py similarity index 97% rename from validators/btc_address.py rename to validators/crypto_addresses/btc_address.py index e8267ddc..a92513d8 100644 --- a/validators/btc_address.py +++ b/validators/crypto_addresses/btc_address.py @@ -6,7 +6,7 @@ import re # local -from .utils import validator +from validators.utils import validator def _decode_base58(addr: str): diff --git a/validators/crypto_addresses/eth_address.py b/validators/crypto_addresses/eth_address.py new file mode 100644 index 00000000..e181b0b7 --- /dev/null +++ b/validators/crypto_addresses/eth_address.py @@ -0,0 +1,57 @@ +"""ETH Address.""" +# -*- coding: utf-8 -*- + +# standard +import re + +from sha3 import keccak_256 + +# 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_256(addr.lower().encode("ascii")).hexdigest() + + 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) + ) diff --git a/validators/crypto_addresses/trx_address.py b/validators/crypto_addresses/trx_address.py new file mode 100644 index 00000000..03ff98b9 --- /dev/null +++ b/validators/crypto_addresses/trx_address.py @@ -0,0 +1,60 @@ +"""TRX Address.""" +# -*- coding: utf-8 -*- + +# standard +from _sha256 import sha256 +import re + +import base58 + +# local +from validators.utils import validator + + +def _validate_trx_checksum_address(addr: str): + """Validate TRX type checksum address.""" + if len(addr) != 34: + return False + + address = base58.b58decode(addr) + if len(address) != 25: + return False + + if address[0] != 0x41: + return False + + check_sum = sha256(sha256(address[:-4]).digest()).digest()[:4] + if address[-4:] == check_sum: + return True + + +@validator +def trx_address(value: str, /): + """Return whether or not given value is a valid tron (trx) address. + + Full validation is implemented for TRC20 tron addresses. + + Examples: + >>> trx_address('TLjfbTbpZYDQ4EoA4N5CLNgGjfbF8ZWz38') + # Output: True + >>> trx_address('TR2G7Rm4vFqF8EpY4U5xdLdQ7XgJ2U8Vd') + # Output: ValidationFailure(func=trx_address, args=...) + + Args: + value: + Tron address string to validate. + + Returns: + (Literal[True]): + If `value` is a valid tron address. + (ValidationFailure): + If `value` is an invalid tron address. + + """ + if not value: + return False + + return ( + re.compile(r"^(T|41)[a-zA-Z0-9]{33}$").match(value) and + _validate_trx_checksum_address(value) + )