Skip to content
Draft
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
55 changes: 55 additions & 0 deletions src/hiero_sdk_python/crypto/evm_address.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from hiero_sdk_python.crypto.key import Key
from hiero_sdk_python.utils.crypto_utils import keccak256


class EvmAddress(Key):
Expand Down Expand Up @@ -49,6 +50,60 @@ def to_string(self) -> str:
def __str__(self) -> str:
return self.to_string()

def to_checksum_address(self) -> str:
"""Return the EIP-55 checksum address.
Reference: https://eips.ethereum.org/EIPS/eip-55
"""
lower_address = self.to_string().lower()

address_hash = keccak256(lower_address.encode("ascii")).hex()

checksummed = "".join(
char.upper() if char.isalpha() and int(address_hash[index], 16) >= 8 else char
for index, char in enumerate(lower_address)
)

return f"0x{checksummed}"

@staticmethod
def normalize(address: str) -> str:
"""Normalize an EVM address to 40 lowercase hex characters without ``0x``."""
if not isinstance(address, str):
raise TypeError("address must be a string")
addr = address.lower()
if addr.startswith("0x"):
addr = addr[2:]

if len(addr) != 40 or not all(c in "0123456789abcdef" for c in addr):
raise ValueError("Invalid address")

return addr

@staticmethod
def is_valid(address: str) -> bool:
"""Return ``True`` when ``address`` is valid lowercase/uppercase hex EVM address."""
if not isinstance(address, str):
return False

addr = address.lower()
if addr.startswith("0x"):
addr = addr[2:]
return len(addr) == 40 and all(c in "0123456789abcdef" for c in addr)

@staticmethod
def is_checksum_valid(address: str) -> bool:
"""Return ``True`` only if ``address`` is ``0x``-prefixed and EIP-55 checksummed."""
if not isinstance(address, str) or not address.startswith("0x"):
return False

raw = address[2:]

if not EvmAddress.is_valid(raw):
return False

checksummed = EvmAddress.from_string(raw).to_checksum_address()
return checksummed == address

def __repr__(self) -> str:
return f"<EvmAddress hex={self.to_string()}>"

Expand Down
88 changes: 88 additions & 0 deletions tests/unit/evm_address_test.py
Copy link
Copy Markdown

@chaitanyamedidar chaitanyamedidar May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @iron-prog , implementation looks good. One small non blocking polish item I'd suggest is few of the newly added unit tests still do not have docstrings, while the surrounding evm_address_test.py tests generally do. Since the contributor testing guide asks for clear test names plus brief docstrings, it may be worth adding one line docstrings to the new validation/normalization tests before merge for consistency.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion I've update the all test cases with valid doctoring

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,46 @@
pytestmark = pytest.mark.unit


def test_is_valid_accepts_valid_address():
"""Test validation accepts a valid EVM address."""
assert EvmAddress.is_valid("0x52908400098527886E0F7030069857D2E4169EE7")


def test_is_valid_rejects_non_string():
"""Test validation rejects non-string inputs."""
assert not EvmAddress.is_valid(123)


def test_is_checksum_valid_rejects_missing_prefix():
"""Test checksum validation rejects addresses without ``0x`` prefix."""
assert not EvmAddress.is_checksum_valid("52908400098527886E0F7030069857D2E4169EE7")


def test_is_checksum_valid_rejects_invalid_length():
"""Test checksum validation rejects addresses with invalid length."""
assert not EvmAddress.is_checksum_valid("0x123")


def test_is_checksum_valid_rejects_non_hex():
"""Test checksum validation rejects non-hexadecimal addresses."""
assert not EvmAddress.is_checksum_valid("0xZZZZ8400098527886E0F7030069857D2E4169EE7")
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_is_valid_accepts_lowercase_without_prefix():
"""Test validation accepts lowercase addresses without ``0x``."""
assert EvmAddress.is_valid("52908400098527886e0f7030069857d2e4169ee7")


def test_is_valid_rejects_invalid_length():
"""Test validation rejects addresses with invalid length."""
assert not EvmAddress.is_valid("123")


def test_is_checksum_valid_rejects_non_string():
"""Test checksum validation rejects non-string inputs."""
assert not EvmAddress.is_checksum_valid(123)


def test_from_string_without_prefix():
"""Test creating EvmAddress from valid 40-character hex string."""
hex_str = "1234567890abcdef1234567890abcdef12345678"
Expand Down Expand Up @@ -74,3 +114,51 @@ def test_to_proto_key():

with pytest.raises(RuntimeError, match=re.escape("to_proto_key() not implemented for EvmAddress")):
address.to_proto_key()


def test_to_checksum_address_matches_eip55_reference():
"""Test checksum formatting follows EIP-55."""
addr = EvmAddress.from_string("0x52908400098527886e0f7030069857d2e4169ee7")

assert addr.to_checksum_address() == "0x52908400098527886E0F7030069857D2E4169EE7"


def test_to_checksum_address_is_stable_for_lowercase_input():
"""Test checksum formatting works for all-lower input."""
addr = EvmAddress.from_string("de709f2102306220921060314715629080e2fb77")

assert addr.to_checksum_address() == "0xde709f2102306220921060314715629080e2fb77"


def test_checksum_from_mixed_case():
"""Test checksum generation normalizes mixed-case input."""
addr = EvmAddress.from_string("52908400098527886E0f7030069857d2e4169ee7")

assert addr.to_checksum_address() == "0x52908400098527886E0F7030069857D2E4169EE7"


def test_is_checksum_valid():
"""Test checksum validation accepts valid EIP-55 addresses."""
assert EvmAddress.is_checksum_valid("0x52908400098527886E0F7030069857D2E4169EE7")


def test_invalid_checksum():
"""Test checksum validation rejects incorrectly cased addresses."""
assert not EvmAddress.is_checksum_valid("0x52908400098527886e0f7030069857d2e4169ee7")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_invalid_embedded_prefix():
"""Test embedded ``0x`` substrings are rejected as invalid addresses."""
bad = "52908400098527886e0f70300698" + "0x" + "57d2e4169ee7"

assert not EvmAddress.is_valid(bad)

with pytest.raises(ValueError):
EvmAddress.normalize(bad)


def test_normalize():
"""Test normalization returns lowercase address without ``0x`` prefix."""
assert (
EvmAddress.normalize("0x52908400098527886E0F7030069857D2E4169EE7") == "52908400098527886e0f7030069857d2e4169ee7"
)
Comment thread
iron-prog marked this conversation as resolved.