diff --git a/docs/source/guides/address.rst b/docs/source/guides/address.rst index 0c05c1d7..33751da8 100644 --- a/docs/source/guides/address.rst +++ b/docs/source/guides/address.rst @@ -85,6 +85,7 @@ a stake verification key:: An address object could also be created from an address string directly:: >>> address = Address.from_primitive("addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7") + >>> byron_address = Address.from_primitive("Ae2tdPwUPEZFRbyhz3cpfC2CumGzNkFBN2L42rcUc2yjQpEkxDbkPodpMAi") An enterprise address does not have staking functionalities, it is created from a payment verification key only:: diff --git a/poetry.lock b/poetry.lock index ac544d49..c0dd4358 100644 --- a/poetry.lock +++ b/poetry.lock @@ -85,6 +85,21 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +[[package]] +name = "base58" +version = "2.1.1" +description = "Base58 and Base58Check implementation." +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, + {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, +] + +[package.extras] +tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] + [[package]] name = "black" version = "25.1.0" @@ -2526,4 +2541,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9.1" -content-hash = "f2b6b72546c2e191c2fae046a2c5deef955968c580758e15d18d316b32241e84" +content-hash = "ba802fdd52ec008c3af72f5c7b8765d18ba0875feab49b9a78c6b94cfffae680" diff --git a/pycardano/address.py b/pycardano/address.py index 61547edc..8d660995 100644 --- a/pycardano/address.py +++ b/pycardano/address.py @@ -8,10 +8,14 @@ from __future__ import annotations +import binascii import os from enum import Enum from typing import Optional, Type, Union +import base58 +import cbor2 +from cbor2 import CBORTag from typing_extensions import override from pycardano.crypto.bech32 import decode, encode @@ -202,12 +206,32 @@ def __init__( self._payment_part = payment_part self._staking_part = staking_part self._network = network + + # Byron address fields (only populated when decoding Byron addresses) + self._byron_payload_hash: Optional[bytes] = None + self._byron_attributes: Optional[dict] = None + self._byron_type: Optional[int] = None + self._byron_crc32: Optional[int] = None + self._address_type = self._infer_address_type() - self._header_byte = self._compute_header_byte() - self._hrp = self._compute_hrp() + self._header_byte = self._compute_header_byte() if not self.is_byron else None + self._hrp = self._compute_hrp() if not self.is_byron else None + + @property + def is_byron(self) -> bool: + """Check if this is a Byron-era address. + + Returns: + bool: True if this is a Byron address, False if Shelley/later. + """ + return self._byron_payload_hash is not None def _infer_address_type(self): """Guess address type from the combination of payment part and staking part.""" + # Check if this is a Byron address + if self.is_byron: + return AddressType.BYRON + payment_type = type(self.payment_part) staking_type = type(self.staking_part) if payment_type == VerificationKeyHash: @@ -263,15 +287,35 @@ def address_type(self) -> AddressType: return self._address_type @property - def header_byte(self) -> bytes: - """Header byte that identifies the type of address.""" + def header_byte(self) -> Optional[bytes]: + """Header byte that identifies the type of address. None for Byron addresses.""" return self._header_byte @property - def hrp(self) -> str: - """Human-readable prefix for bech32 encoder.""" + def hrp(self) -> Optional[str]: + """Human-readable prefix for bech32 encoder. None for Byron addresses.""" return self._hrp + @property + def payload_hash(self) -> Optional[bytes]: + """Byron address payload hash (28 bytes). None for Shelley addresses.""" + return self._byron_payload_hash if self.is_byron else None + + @property + def byron_attributes(self) -> Optional[dict]: + """Byron address attributes. None for Shelley addresses.""" + return self._byron_attributes if self.is_byron else None + + @property + def byron_type(self) -> Optional[int]: + """Byron address type (0=Public Key, 2=Redemption). None for Shelley addresses.""" + return self._byron_type if self.is_byron else None + + @property + def crc32_checksum(self) -> Optional[int]: + """Byron address CRC32 checksum. None for Shelley addresses.""" + return self._byron_crc32 if self.is_byron else None + def _compute_header_byte(self) -> bytes: """Compute the header byte.""" return (self.address_type.value << 4 | self.network.value).to_bytes( @@ -294,6 +338,16 @@ def _compute_hrp(self) -> str: return prefix + suffix def __bytes__(self): + if self.is_byron: + payload = cbor2.dumps( + [ + self._byron_payload_hash, + self._byron_attributes, + self._byron_type, + ] + ) + return cbor2.dumps([CBORTag(24, payload), self._byron_crc32]) + payment = self.payment_part or bytes() if self.staking_part is None: staking = bytes() @@ -304,12 +358,12 @@ def __bytes__(self): return self.header_byte + bytes(payment) + bytes(staking) def encode(self) -> str: - """Encode the address in Bech32 format. + """Encode the address in Bech32 format (Shelley) or Base58 format (Byron). More info about Bech32 `here `_. Returns: - str: Encoded address in Bech32. + str: Encoded address in Bech32 (Shelley) or Base58 (Byron). Examples: >>> payment_hash = VerificationKeyHash( @@ -317,6 +371,8 @@ def encode(self) -> str: >>> print(Address(payment_hash).encode()) addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w """ + if self.is_byron: + return base58.b58encode(bytes(self)).decode("ascii") return encode(self.hrp, bytes(self)) @classmethod @@ -345,8 +401,38 @@ def to_primitive(self) -> bytes: @classmethod @limit_primitive_type(bytes, str) def from_primitive(cls: Type[Address], value: Union[bytes, str]) -> Address: + # Convert string to bytes if isinstance(value, str): - value = bytes(decode(value)) + # Check for Byron Base58 prefixes (common Byron patterns) + if value.startswith(("Ae2td", "Ddz")): + return cls._from_byron_base58(value) + + # Try Bech32 decode for Shelley addresses + original_str = value + try: + value = bytes(decode(value)) + except Exception: + try: + return cls._from_byron_base58(original_str) + except Exception as e: + raise DecodingException(f"Failed to decode address string: {e}") + + # At this point, value is always bytes + # Check if it's a Byron address (CBOR with tag 24) + try: + decoded = cbor2.loads(value) + if isinstance(decoded, (tuple, list)) and len(decoded) == 2: + if isinstance(decoded[0], CBORTag) and decoded[0].tag == 24: + # This is definitely a Byron address - validate and decode it + return cls._from_byron_cbor(value) + except DecodingException: + # Byron decoding failed with validation error - re-raise it + raise + except Exception: + # Not Byron CBOR (general CBOR decode error), continue with Shelley decoding + pass + + # Shelley address decoding (existing logic) header = value[0] payload = value[1:] addr_type = AddressType((header & 0xF0) >> 4) @@ -397,16 +483,150 @@ def from_primitive(cls: Type[Address], value: Union[bytes, str]) -> Address: return cls(None, ScriptHash(payload), network) raise DeserializeException(f"Error in deserializing bytes: {value}") + @classmethod + def _from_byron_base58(cls: Type[Address], base58_str: str) -> Address: + """Decode a Byron address from Base58 string. + + Args: + base58_str: Base58-encoded Byron address string. + + Returns: + Address: Decoded Byron address instance. + + Raises: + DecodingException: When decoding fails. + """ + try: + cbor_bytes = base58.b58decode(base58_str) + except Exception as e: + raise DecodingException(f"Failed to decode Base58 string: {e}") + + return cls._from_byron_cbor(cbor_bytes) + + @classmethod + def _from_byron_cbor(cls: Type[Address], cbor_bytes: bytes) -> Address: + """Decode a Byron address from CBOR bytes. + + Args: + cbor_bytes: CBOR-encoded Byron address bytes. + + Returns: + Address: Decoded Byron address instance. + + Raises: + DecodingException: When decoding fails. + """ + try: + decoded = cbor2.loads(cbor_bytes) + except Exception as e: + raise DecodingException(f"Failed to decode CBOR bytes: {e}") + + # Byron address structure: [CBORTag(24, payload), crc32] + if not isinstance(decoded, (tuple, list)) or len(decoded) != 2: + raise DecodingException( + f"Byron address must be a 2-element array, got {type(decoded)}" + ) + + tagged_payload, crc32_checksum = decoded + + if not isinstance(tagged_payload, CBORTag) or tagged_payload.tag != 24: + raise DecodingException( + f"Byron address must use CBOR tag 24, got {tagged_payload}" + ) + + payload_cbor = tagged_payload.value + if not isinstance(payload_cbor, bytes): + raise DecodingException( + f"Tag 24 must contain bytes, got {type(payload_cbor)}" + ) + + computed_crc32 = binascii.crc32(payload_cbor) & 0xFFFFFFFF + if computed_crc32 != crc32_checksum: + raise DecodingException( + f"CRC32 checksum mismatch: expected {crc32_checksum}, got {computed_crc32}" + ) + + try: + payload = cbor2.loads(payload_cbor) + except Exception as e: + raise DecodingException(f"Failed to decode Byron address payload: {e}") + + if not isinstance(payload, (tuple, list)) or len(payload) != 3: + raise DecodingException( + f"Byron address payload must be a 3-element array, got {payload}" + ) + + payload_hash, attributes, byron_type = payload + + if not isinstance(payload_hash, bytes) or len(payload_hash) != 28: + size = ( + len(payload_hash) + if isinstance(payload_hash, bytes) + else f"type {type(payload_hash).__name__}" + ) + raise DecodingException(f"Payload hash must be 28 bytes, got {size}") + + if not isinstance(attributes, dict): + raise DecodingException( + f"Attributes must be a dict, got {type(attributes)}" + ) + + if byron_type not in (0, 2): + raise DecodingException(f"Byron type must be 0 or 2, got {byron_type}") + + # Create Address instance with Byron fields + addr = cls.__new__(cls) + addr._payment_part = None + addr._staking_part = None + addr._byron_payload_hash = payload_hash + addr._byron_attributes = attributes + addr._byron_type = byron_type + addr._byron_crc32 = crc32_checksum + addr._network = addr._infer_byron_network() + addr._address_type = AddressType.BYRON + addr._header_byte = None + addr._hrp = None + return addr + + def _infer_byron_network(self) -> Network: + """Infer network from Byron address attributes. + + Returns: + Network: MAINNET or TESTNET (defaults to MAINNET). + """ + if self._byron_attributes and 2 in self._byron_attributes: + network_bytes = self._byron_attributes[2] + if isinstance(network_bytes, bytes): + try: + network_discriminant = cbor2.loads(network_bytes) + # Mainnet: 764824073 (0x2D964A09), Testnet: 1097911063 (0x42659F17) + if network_discriminant == 1097911063: + return Network.TESTNET + except Exception: + pass + return Network.MAINNET + def __eq__(self, other): if not isinstance(other, Address): return False - else: + + if self.is_byron != other.is_byron: + return False + + if self.is_byron: return ( - other.payment_part == self.payment_part - and other.staking_part == self.staking_part - and other.network == self.network + self._byron_payload_hash == other._byron_payload_hash + and self._byron_attributes == other._byron_attributes + and self._byron_type == other._byron_type + and self._byron_crc32 == other._byron_crc32 ) + return ( + self.payment_part == other.payment_part + and self.staking_part == other.staking_part + and self.network == other.network + ) + def __repr__(self): return f"{self.encode()}" diff --git a/pycardano/transaction.py b/pycardano/transaction.py index a193baf7..da3deb56 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -690,7 +690,7 @@ class Transaction(ArrayCBORSerializable): transaction_witness_set: TransactionWitnessSet - valid: bool = True + valid: Optional[bool] = field(default=True, metadata={"optional": True}) auxiliary_data: Optional[AuxiliaryData] = None diff --git a/pyproject.toml b/pyproject.toml index 3215bbcf..3a684e20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ docker = ">=7.1.0" ogmios = ">=1.4.2" requests = ">=2.32.3" websockets = ">=13.0" +base58 = ">=2.1.0" [tool.poetry.group.dev.dependencies] pytest = ">=8.2.0" diff --git a/test/pycardano/test_byron_address.py b/test/pycardano/test_byron_address.py new file mode 100644 index 00000000..5df8894f --- /dev/null +++ b/test/pycardano/test_byron_address.py @@ -0,0 +1,312 @@ +"""Unit tests for Byron address implementation in Address class.""" + +import binascii + +import base58 +import cbor2 +import pytest +from cbor2 import CBORTag + +from pycardano import Address, Network +from pycardano.exception import DecodingException, InvalidAddressInputException + + +class TestAddress: + """Test cases for Byron address support in Address class.""" + + # Known Byron mainnet address for testing + BYRON_MAINNET_ADDR = "DdzFFzCqrhsxrgB6w6VhgfAqUZ69Va583murc21S4QFTJ6WUHAh4Gk8t1QHofpza5MZxG4dNVQWe8q78h4Utp9MGBQHBLD54rz6CTLsm" + + def test_decode_mainnet_address(self): + """Test decoding a Byron mainnet address.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + assert addr.is_byron # This is a Byron address + assert addr.byron_type == 0 # Public key type + assert addr.network == Network.MAINNET + assert len(addr.payload_hash) == 28 + assert addr.crc32_checksum == 898818764 + + def test_encode_round_trip(self): + """Test encoding and decoding round trip.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + encoded = addr.encode() + + assert encoded == self.BYRON_MAINNET_ADDR + + def test_equality(self): + """Test Byron address equality.""" + addr1 = Address.decode(self.BYRON_MAINNET_ADDR) + addr2 = Address.decode(self.BYRON_MAINNET_ADDR) + + assert addr1 == addr2 + assert not (addr1 != addr2) + + def test_not_equal_to_other_types(self): + """Test that Byron address is not equal to other types.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + assert addr != "not an address" + assert addr != 123 + assert addr != None + + def test_bytes_interface(self): + """Test conversion to and from bytes.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + addr_bytes = bytes(addr) + + # Should be able to decode from bytes + addr_from_bytes = Address.from_primitive(addr_bytes) + + assert addr == addr_from_bytes + + def test_repr(self): + """Test string representation.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + repr_str = repr(addr) + + # __repr__ returns the encoded address string + assert self.BYRON_MAINNET_ADDR in repr_str + + # Note: Cannot test invalid construction since Byron addresses are read-only (decode only) + + def test_invalid_crc32_checksum(self): + """Test that invalid CRC32 checksum is detected.""" + # Decode a valid address + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + # Create corrupted CBOR with wrong CRC32 + corrupted = cbor2.dumps( + [ + CBORTag( + 24, + cbor2.dumps( + [addr.payload_hash, addr.byron_attributes, addr.byron_type] + ), + ), + 12345, # Wrong CRC32 + ] + ) + corrupted_b58 = base58.b58encode(corrupted).decode("ascii") + + # Should raise exception + with pytest.raises(DecodingException, match="CRC32 checksum mismatch"): + Address.decode(corrupted_b58) + + def test_invalid_base58_string(self): + """Test that invalid Base58 string raises exception.""" + with pytest.raises(DecodingException): + Address.decode("not a valid base58 string!!!") + + def test_invalid_cbor_structure(self): + """Test that invalid CBOR structure raises exception.""" + # Create invalid CBOR (not a 2-element array) + invalid_cbor = cbor2.dumps([1, 2, 3]) + invalid_b58 = base58.b58encode(invalid_cbor).decode("ascii") + + with pytest.raises(DecodingException): + Address.decode(invalid_b58) + + def test_missing_cbor_tag(self): + """Test that missing CBOR tag 24 raises exception.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + # Create CBOR without tag 24 + invalid = cbor2.dumps( + [ + cbor2.dumps( + [addr.payload_hash, addr.byron_attributes, addr.byron_type] + ), + addr.crc32_checksum, + ] + ) + invalid_b58 = base58.b58encode(invalid).decode("ascii") + + with pytest.raises(DecodingException, match="CBOR tag 24"): + Address.decode(invalid_b58) + + def test_to_primitive(self): + """Test conversion to primitive types.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + primitive = addr.to_primitive() + + # For Byron addresses, to_primitive returns bytes + assert isinstance(primitive, bytes) + + def test_byron_type_property(self): + """Test Byron address type property.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + assert addr.byron_type in (0, 2) + + def test_network_property(self): + """Test network property.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + assert isinstance(addr.network, Network) + assert addr.network in (Network.MAINNET, Network.TESTNET) + + def test_byron_attributes_property(self): + """Test Byron address attributes property.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + + assert isinstance(addr.byron_attributes, dict) + + def test_save_and_load(self, tmp_path): + """Test saving and loading Byron address to/from file.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + file_path = tmp_path / "byron_address.txt" + + # Save the address + addr.save(str(file_path)) + + # Load the address + loaded_addr = Address.load(str(file_path)) + + assert loaded_addr == addr + + def test_save_existing_file_raises_error(self, tmp_path): + """Test that saving to existing file raises error.""" + addr = Address.decode(self.BYRON_MAINNET_ADDR) + file_path = tmp_path / "byron_address.txt" + + # Save once + addr.save(str(file_path)) + + # Try to save again should raise error + with pytest.raises(IOError): + addr.save(str(file_path)) + + def test_testnet_byron_address(self): + """Test decoding a testnet Byron address.""" + # Create a Byron testnet address with network discriminant + payload_hash = b"\x91\xd0\xa0Q\x8e>vN\x13\xf6\xef7X\nk\xe8\xab\x14\xdaO0f\xfd\x01\xaf\x01\xdaj" + testnet_discriminant = cbor2.dumps(1097911063) # 0x42659F17 + attributes = {2: testnet_discriminant} + byron_type = 0 + + payload = cbor2.dumps([payload_hash, attributes, byron_type]) + crc32 = binascii.crc32(payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, payload), crc32]) + + addr = Address.from_primitive(byron_cbor) + + assert addr.is_byron + assert addr.network == Network.TESTNET + assert addr.byron_type == 0 + + def test_decode_from_raw_cbor_bytes(self): + """Test decoding Byron address directly from raw CBOR bytes.""" + # Decode a known address to get its CBOR representation + addr = Address.decode(self.BYRON_MAINNET_ADDR) + cbor_bytes = bytes(addr) + + # Decode from raw CBOR bytes + addr_from_cbor = Address.from_primitive(cbor_bytes) + + assert addr == addr_from_cbor + assert addr_from_cbor.is_byron + + def test_invalid_network_attribute_type(self): + """Test Byron address with non-bytes network attribute.""" + payload_hash = b"\x00" * 28 + attributes = {2: "not bytes"} # Invalid type for network discriminant + byron_type = 0 + + payload = cbor2.dumps([payload_hash, attributes, byron_type]) + crc32 = binascii.crc32(payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, payload), crc32]) + + # Should still decode (defaults to mainnet) + addr = Address.from_primitive(byron_cbor) + assert addr.network == Network.MAINNET + + def test_invalid_network_discriminant_value(self): + """Test Byron address with unknown network discriminant.""" + payload_hash = b"\x00" * 28 + unknown_discriminant = cbor2.dumps(99999) # Unknown network + attributes = {2: unknown_discriminant} + byron_type = 0 + + payload = cbor2.dumps([payload_hash, attributes, byron_type]) + crc32 = binascii.crc32(payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, payload), crc32]) + + # Should default to mainnet + addr = Address.from_primitive(byron_cbor) + assert addr.network == Network.MAINNET + + def test_malformed_network_cbor(self): + """Test Byron address with malformed network CBOR.""" + payload_hash = b"\x00" * 28 + attributes = {2: b"\xff\xff"} # Invalid CBOR + byron_type = 0 + + payload = cbor2.dumps([payload_hash, attributes, byron_type]) + crc32 = binascii.crc32(payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, payload), crc32]) + + # Should default to mainnet on CBOR decode error + addr = Address.from_primitive(byron_cbor) + assert addr.network == Network.MAINNET + + def test_redemption_address_type(self): + """Test Byron redemption address (type 2).""" + payload_hash = b"\x00" * 28 + attributes = {} + byron_type = 2 # Redemption type + + payload = cbor2.dumps([payload_hash, attributes, byron_type]) + crc32 = binascii.crc32(payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, payload), crc32]) + + addr = Address.from_primitive(byron_cbor) + assert addr.byron_type == 2 + + def test_invalid_payload_not_3_elements(self): + """Test Byron address with wrong number of payload elements.""" + # Create invalid payload with 2 elements instead of 3 + invalid_payload = cbor2.dumps([b"\x00" * 28, {}]) + crc32 = binascii.crc32(invalid_payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, invalid_payload), crc32]) + + with pytest.raises(DecodingException, match="3-element array"): + Address.from_primitive(byron_cbor) + + def test_invalid_payload_hash_wrong_size(self): + """Test Byron address with wrong payload hash size.""" + invalid_payload = cbor2.dumps([b"\x00" * 20, {}, 0]) # 20 bytes instead of 28 + crc32 = binascii.crc32(invalid_payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, invalid_payload), crc32]) + + with pytest.raises(DecodingException, match="28 bytes"): + Address.from_primitive(byron_cbor) + + def test_invalid_attributes_not_dict(self): + """Test Byron address with non-dict attributes.""" + invalid_payload = cbor2.dumps([b"\x00" * 28, "not a dict", 0]) + crc32 = binascii.crc32(invalid_payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, invalid_payload), crc32]) + + with pytest.raises(DecodingException, match="must be a dict"): + Address.from_primitive(byron_cbor) + + def test_invalid_byron_type(self): + """Test Byron address with invalid type (not 0 or 2).""" + invalid_payload = cbor2.dumps([b"\x00" * 28, {}, 1]) # Type 1 is invalid + crc32 = binascii.crc32(invalid_payload) & 0xFFFFFFFF + byron_cbor = cbor2.dumps([CBORTag(24, invalid_payload), crc32]) + + with pytest.raises(DecodingException, match="must be 0 or 2"): + Address.from_primitive(byron_cbor) + + def test_shelley_and_byron_not_equal(self): + """Test that Shelley and Byron addresses are never equal.""" + byron_addr = Address.decode(self.BYRON_MAINNET_ADDR) + + # Create a Shelley address + from pycardano import VerificationKeyHash + + shelley_addr = Address(VerificationKeyHash(b"\x00" * 28), None, Network.MAINNET) + + assert byron_addr != shelley_addr diff --git a/test/pycardano/test_transaction.py b/test/pycardano/test_transaction.py index c14312b1..0beb8631 100644 --- a/test/pycardano/test_transaction.py +++ b/test/pycardano/test_transaction.py @@ -696,3 +696,12 @@ def test_decode_param_update_proposal_tx(): assert tx.transaction_body.proposal_procedures[ 0 ].gov_action.protocol_param_update.treasury_growth_rate == Fraction(1, 10) + + +def test_decode_byron_transaction(): + tx_cbor_hex = """83a400818258205d5f5c04aaa2367c5a700cf6ba9e9da76e214a0a1485a174618cb38b292bf0d9000182825839016a2fcce35ec3795b9418ae49b69074a17cdd0a7c60ae6ba63fc85eff17eabf85728a590b7785f27d60dea7d4bcb356b438b9d577a45547fe1b0000001e3001052482584c82d818584283581c91d0a0518e3e764e13f6ef37580a6be8ab14da4f3066fd01af01da6aa101581e581cabbf051bdee353839fbb21a6d4e6c584138a6a33896bb96d4124a330001a3592e2cc1a0a6526b0021a0002964d031a012f6296a10081825820e8fe69f9fd8afcb4792e3ca0f08b49e6eece1788c2d7b026096cfdbd1344a9bc5840dcef77b73af0922005f4b60d21333628348864c405ff52efd3f72523bf2c790e662650ad9951d306b40ce5beddf5b8eebb6731156b8b7617f6614b9ffdf2fb05f6""" + tx = Transaction.from_cbor(tx_cbor_hex) + check_two_way_cbor(tx) + assert tx.id == TransactionId.from_primitive( + "52e274237caceb4e0916587d2b4ba19d89fb40e8e85338f9bb4f75fcec1256a2" + ) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 111b2b84..13f183c2 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -2466,3 +2466,20 @@ def test_skip_utxo_with_script(chain_context): tx_builder.add_input_address(addr) with pytest.raises(UTxOSelectionException): tx_builder.build() + + +def test_byron_addr_output(chain_context): + tx_builder = TransactionBuilder(chain_context) + sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x" + + byron1 = "DdzFFzCqrhsxrgB6w6VhgfAqUZ69Va583murc21S4QFTJ6WUHAh4Gk8t1QHofpza5MZxG4dNVQWe8q78h4Utp9MGBQHBLD54rz6CTLsm" + byron2 = "Ae2tdPwUPEZFRbyhz3cpfC2CumGzNkFBN2L42rcUc2yjQpEkxDbkPodpMAi" + + # Add sender address as input + tx_builder.add_input_address(sender).add_output( + TransactionOutput.from_primitive([byron1, 500000]) + ).add_output(TransactionOutput.from_primitive([byron2, 1500000])) + + tx_body = tx_builder.build() + assert str(tx_body.outputs[0].address) == byron1 + assert str(tx_body.outputs[1].address) == byron2