Skip to content

add validator ETH addresses (ERC20) #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions tests/crypto_addresses/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Test crypto addresses."""
# -*- coding: utf-8 -*-

# isort: skip_file
45 changes: 45 additions & 0 deletions tests/crypto_addresses/test_eth_address.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions validators/crypto_addresses/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Crypto addresses."""
# -*- coding: utf-8 -*-

# isort: skip_file

# local
from .eth_address import eth_address

__all__ = ("eth_address",)
58 changes: 58 additions & 0 deletions validators/crypto_addresses/eth_address.py
Original file line number Diff line number Diff line change
@@ -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)