Skip to content

Commit 60c5314

Browse files
authored
feat: adds ETH addresses (ERC20) validator (#276)
- add validator for etherium addresses - add test for eth address validator - add pysha3 dependency - move eth and btc validators to a new submodule - move eth and btc validators' tests to a new submodule - add init files for new submodules - add trx validator to init - fix bug in importing validator - revert moving btc validator and its tests to new submodules - fix import - remove faulty test value - use relative import - remove trx_validator - fix bugs in test values - combine eth regex checks into one check - check length in checksum validation - use eth-hash instead of pysha3 - use external tag - fix import tags - add eth-hash backends dependency - reformat using black
1 parent 92fe972 commit 60c5314

File tree

7 files changed

+183
-1
lines changed

7 files changed

+183
-1
lines changed

poetry.lock

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ include = ["CHANGES.md", "docs/*", "docs/validators.1", "validators/py.typed"]
4949

5050
[tool.poetry.dependencies]
5151
python = "^3.8"
52+
eth-hash = {extras = ["pycryptodome"], version = "^0.5.2"}
5253

5354
[tool.poetry.group.docs]
5455
optional = true

tests/crypto_addresses/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Test crypto addresses."""
2+
# -*- coding: utf-8 -*-
3+
4+
# isort: skip_file
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Test ETH address."""
2+
# -*- coding: utf-8 -*-
3+
4+
# external
5+
import pytest
6+
7+
# local
8+
from validators import eth_address, ValidationFailure
9+
10+
11+
@pytest.mark.parametrize(
12+
"value",
13+
[
14+
"0x8ba1f109551bd432803012645ac136ddd64dba72",
15+
"0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598",
16+
"0x5AEDA56215b167893e80B4fE645BA6d5Bab767DE",
17+
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
18+
"0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
19+
"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
20+
"0x1234567890123456789012345678901234567890",
21+
"0x57Ab1ec28D129707052df4dF418D58a2D46d5f51",
22+
],
23+
)
24+
def test_returns_true_on_valid_eth_address(value: str):
25+
"""Test returns true on valid eth address."""
26+
assert eth_address(value)
27+
28+
29+
@pytest.mark.parametrize(
30+
"value",
31+
[
32+
"0x742d35Cc6634C0532925a3b844Bc454e4438f44g",
33+
"0x742d35Cc6634C0532925a3b844Bc454e4438f44",
34+
"0xAbcdefg1234567890Abcdefg1234567890Abcdefg",
35+
"0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c72",
36+
"0x80fBD7F8B3f81D0e1d6EACAb69AF104A6508AFB1",
37+
"0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c7g",
38+
"0x7c8EE9977c6f96b6b9774b3e8e4Cc9B93B12b2c",
39+
"0x7Fb21a171205f3B8d8E4d88A2d2f8A56E45DdB5c",
40+
"validators.eth",
41+
],
42+
)
43+
def test_returns_failed_validation_on_invalid_eth_address(value: str):
44+
"""Test returns failed validation on invalid eth address."""
45+
assert isinstance(eth_address(value), ValidationFailure)

validators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
from .utils import validator, ValidationFailure
2525
from .uuid import uuid
2626

27+
from .crypto_addresses import eth_address
2728
from .i18n import es_cif, es_doi, es_nie, es_nif, fi_business_id, fi_ssn
2829

2930
__all__ = (
3031
"amex",
3132
"between",
3233
"btc_address",
34+
"eth_address",
3335
"card_number",
3436
"diners",
3537
"discover",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Crypto addresses."""
2+
# -*- coding: utf-8 -*-
3+
4+
# isort: skip_file
5+
6+
# local
7+
from .eth_address import eth_address
8+
9+
__all__ = ("eth_address",)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""ETH Address."""
2+
# -*- coding: utf-8 -*-
3+
4+
# standard
5+
import re
6+
7+
# external
8+
from eth_hash.auto import keccak
9+
10+
# local
11+
from validators.utils import validator
12+
13+
14+
def _validate_eth_checksum_address(addr: str):
15+
"""Validate ETH type checksum address."""
16+
addr = addr.replace("0x", "")
17+
addr_hash = keccak.new(addr.lower().encode("ascii")).digest().hex()
18+
19+
if len(addr) != 40:
20+
return False
21+
22+
for i in range(0, 40):
23+
if (int(addr_hash[i], 16) > 7 and addr[i].upper() != addr[i]) or (
24+
int(addr_hash[i], 16) <= 7 and addr[i].lower() != addr[i]
25+
):
26+
return False
27+
return True
28+
29+
30+
@validator
31+
def eth_address(value: str, /):
32+
"""Return whether or not given value is a valid ethereum address.
33+
34+
Full validation is implemented for ERC20 addresses.
35+
36+
Examples:
37+
>>> eth_address('0x9cc14ba4f9f68ca159ea4ebf2c292a808aaeb598')
38+
# Output: True
39+
>>> eth_address('0x8Ba1f109551bD432803012645Ac136ddd64DBa72')
40+
# Output: ValidationFailure(func=eth_address, args=...)
41+
42+
Args:
43+
value:
44+
Ethereum address string to validate.
45+
46+
Returns:
47+
(Literal[True]):
48+
If `value` is a valid ethereum address.
49+
(ValidationFailure):
50+
If `value` is an invalid ethereum address.
51+
52+
"""
53+
if not value:
54+
return False
55+
56+
return re.compile(r"^0x[0-9a-f]{40}$|^0x[0-9A-F]{40}$").match(
57+
value
58+
) or _validate_eth_checksum_address(value)

0 commit comments

Comments
 (0)