From d28967cf54c4095bdfa39e13a7f42d1455b42221 Mon Sep 17 00:00:00 2001 From: Alex Barnsley <8069294+alexbarnsley@users.noreply.github.com> Date: Thu, 21 May 2026 16:12:40 +0100 Subject: [PATCH] feat: add legacy address handling --- crypto/identity/legacy_address.py | 35 +++++++++++++++++++++++++++ tests/identity/conftest.py | 12 +++++++++ tests/identity/test_legacy_address.py | 32 ++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 crypto/identity/legacy_address.py create mode 100644 tests/identity/test_legacy_address.py diff --git a/crypto/identity/legacy_address.py b/crypto/identity/legacy_address.py new file mode 100644 index 0000000..28b8633 --- /dev/null +++ b/crypto/identity/legacy_address.py @@ -0,0 +1,35 @@ +import hashlib + +from base58 import b58decode_check, b58encode_check + +from Cryptodome.Hash import RIPEMD160 + +from crypto.identity.private_key import PrivateKey + + +class LegacyAddress: + @classmethod + def from_public_key(cls, public_key: str, pub_key_hash: int) -> str: + ripemd160 = RIPEMD160.new(bytes.fromhex(public_key)) + payload = bytes([pub_key_hash]) + ripemd160.digest() + return b58encode_check(payload).decode() + + @classmethod + def from_private_key(cls, private_key: str, pub_key_hash: int) -> str: + public_key = PrivateKey.from_hex(private_key).public_key + return cls.from_public_key(public_key, pub_key_hash) + + @classmethod + def from_passphrase(cls, passphrase: str, pub_key_hash: int) -> str: + private_key = hashlib.sha256(passphrase.encode()).hexdigest() + return cls.from_private_key(private_key, pub_key_hash) + + @classmethod + def validate(cls, address: str, pub_key_hash: int) -> bool: + try: + decoded = b58decode_check(address) + if len(decoded) != 21: + return False + return decoded[0] == pub_key_hash + except Exception: + return False diff --git a/tests/identity/conftest.py b/tests/identity/conftest.py index df8a6fb..12bb19e 100644 --- a/tests/identity/conftest.py +++ b/tests/identity/conftest.py @@ -17,6 +17,18 @@ def identity(): } return data +@pytest.fixture +def legacy_identity(): + return { + 'data': { + 'public_key': '02a7c5ca78f6abbced169cb883aec3ffc0a0950affc0de575fb211873b5846e668', + 'private_key': 'c7a0df6e1c42268946af49af28c49c6da64419f0203fa970b6e9be9f85a44875', + 'address': 'D6WFwqYDRiFkSf4ezzWRt3jCsUp2sRmDMi', + }, + 'passphrase': 'enact busy minimum fantasy endless shoot reduce few inject ostrich snow promote', + 'pub_key_hash': 30, + } + @pytest.fixture def sign_compact(): """Identity fixture diff --git a/tests/identity/test_legacy_address.py b/tests/identity/test_legacy_address.py new file mode 100644 index 0000000..f3507fd --- /dev/null +++ b/tests/identity/test_legacy_address.py @@ -0,0 +1,32 @@ +from crypto.identity.legacy_address import LegacyAddress + + +def test_from_passphrase(legacy_identity): + address = LegacyAddress.from_passphrase(legacy_identity['passphrase'], legacy_identity['pub_key_hash']) + assert address == legacy_identity['data']['address'] + + +def test_from_public_key(legacy_identity): + address = LegacyAddress.from_public_key(legacy_identity['data']['public_key'], legacy_identity['pub_key_hash']) + assert address == legacy_identity['data']['address'] + + +def test_from_private_key(legacy_identity): + address = LegacyAddress.from_private_key(legacy_identity['data']['private_key'], legacy_identity['pub_key_hash']) + assert address == legacy_identity['data']['address'] + + +def test_validate(legacy_identity): + assert LegacyAddress.validate(legacy_identity['data']['address'], legacy_identity['pub_key_hash']) is True + + +def test_validate_incorrect_pub_key_hash(legacy_identity): + assert LegacyAddress.validate(legacy_identity['data']['address'], 32) is False + + +def test_validate_invalid_address(legacy_identity): + assert LegacyAddress.validate('D2WFnqYDRiFkSf4ezzWRt3jCsUp2sRmDMifwd', legacy_identity['pub_key_hash']) is False + + +def test_validate_decoding_error(legacy_identity): + assert LegacyAddress.validate('invalid', legacy_identity['pub_key_hash']) is False