88
99from __future__ import annotations
1010
11+ import binascii
1112import os
1213from enum import Enum
1314from typing import Optional , Type , Union
1415
16+ import base58
17+ import cbor2
18+ from cbor2 import CBORTag
1519from typing_extensions import override
1620
1721from pycardano .crypto .bech32 import decode , encode
@@ -202,12 +206,32 @@ def __init__(
202206 self ._payment_part = payment_part
203207 self ._staking_part = staking_part
204208 self ._network = network
209+
210+ # Byron address fields (only populated when decoding Byron addresses)
211+ self ._byron_payload_hash : Optional [bytes ] = None
212+ self ._byron_attributes : Optional [dict ] = None
213+ self ._byron_type : Optional [int ] = None
214+ self ._byron_crc32 : Optional [int ] = None
215+
205216 self ._address_type = self ._infer_address_type ()
206- self ._header_byte = self ._compute_header_byte ()
207- self ._hrp = self ._compute_hrp ()
217+ self ._header_byte = self ._compute_header_byte () if not self .is_byron else None
218+ self ._hrp = self ._compute_hrp () if not self .is_byron else None
219+
220+ @property
221+ def is_byron (self ) -> bool :
222+ """Check if this is a Byron-era address.
223+
224+ Returns:
225+ bool: True if this is a Byron address, False if Shelley/later.
226+ """
227+ return self ._byron_payload_hash is not None
208228
209229 def _infer_address_type (self ):
210230 """Guess address type from the combination of payment part and staking part."""
231+ # Check if this is a Byron address
232+ if self .is_byron :
233+ return AddressType .BYRON
234+
211235 payment_type = type (self .payment_part )
212236 staking_type = type (self .staking_part )
213237 if payment_type == VerificationKeyHash :
@@ -263,15 +287,35 @@ def address_type(self) -> AddressType:
263287 return self ._address_type
264288
265289 @property
266- def header_byte (self ) -> bytes :
267- """Header byte that identifies the type of address."""
290+ def header_byte (self ) -> Optional [ bytes ] :
291+ """Header byte that identifies the type of address. None for Byron addresses. """
268292 return self ._header_byte
269293
270294 @property
271- def hrp (self ) -> str :
272- """Human-readable prefix for bech32 encoder."""
295+ def hrp (self ) -> Optional [ str ] :
296+ """Human-readable prefix for bech32 encoder. None for Byron addresses. """
273297 return self ._hrp
274298
299+ @property
300+ def payload_hash (self ) -> Optional [bytes ]:
301+ """Byron address payload hash (28 bytes). None for Shelley addresses."""
302+ return self ._byron_payload_hash if self .is_byron else None
303+
304+ @property
305+ def byron_attributes (self ) -> Optional [dict ]:
306+ """Byron address attributes. None for Shelley addresses."""
307+ return self ._byron_attributes if self .is_byron else None
308+
309+ @property
310+ def byron_type (self ) -> Optional [int ]:
311+ """Byron address type (0=Public Key, 2=Redemption). None for Shelley addresses."""
312+ return self ._byron_type if self .is_byron else None
313+
314+ @property
315+ def crc32_checksum (self ) -> Optional [int ]:
316+ """Byron address CRC32 checksum. None for Shelley addresses."""
317+ return self ._byron_crc32 if self .is_byron else None
318+
275319 def _compute_header_byte (self ) -> bytes :
276320 """Compute the header byte."""
277321 return (self .address_type .value << 4 | self .network .value ).to_bytes (
@@ -294,6 +338,16 @@ def _compute_hrp(self) -> str:
294338 return prefix + suffix
295339
296340 def __bytes__ (self ):
341+ if self .is_byron :
342+ payload = cbor2 .dumps (
343+ [
344+ self ._byron_payload_hash ,
345+ self ._byron_attributes ,
346+ self ._byron_type ,
347+ ]
348+ )
349+ return cbor2 .dumps ([CBORTag (24 , payload ), self ._byron_crc32 ])
350+
297351 payment = self .payment_part or bytes ()
298352 if self .staking_part is None :
299353 staking = bytes ()
@@ -304,19 +358,21 @@ def __bytes__(self):
304358 return self .header_byte + bytes (payment ) + bytes (staking )
305359
306360 def encode (self ) -> str :
307- """Encode the address in Bech32 format.
361+ """Encode the address in Bech32 format (Shelley) or Base58 format (Byron) .
308362
309363 More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
310364
311365 Returns:
312- str: Encoded address in Bech32.
366+ str: Encoded address in Bech32 (Shelley) or Base58 (Byron) .
313367
314368 Examples:
315369 >>> payment_hash = VerificationKeyHash(
316370 ... bytes.fromhex("cc30497f4ff962f4c1dca54cceefe39f86f1d7179668009f8eb71e59"))
317371 >>> print(Address(payment_hash).encode())
318372 addr1v8xrqjtlfluk9axpmjj5enh0uw0cduwhz7txsqyl36m3ukgqdsn8w
319373 """
374+ if self .is_byron :
375+ return base58 .b58encode (bytes (self )).decode ("ascii" )
320376 return encode (self .hrp , bytes (self ))
321377
322378 @classmethod
@@ -345,8 +401,38 @@ def to_primitive(self) -> bytes:
345401 @classmethod
346402 @limit_primitive_type (bytes , str )
347403 def from_primitive (cls : Type [Address ], value : Union [bytes , str ]) -> Address :
404+ # Convert string to bytes
348405 if isinstance (value , str ):
349- value = bytes (decode (value ))
406+ # Check for Byron Base58 prefixes (common Byron patterns)
407+ if value .startswith (("Ae2td" , "Ddz" )):
408+ return cls ._from_byron_base58 (value )
409+
410+ # Try Bech32 decode for Shelley addresses
411+ original_str = value
412+ try :
413+ value = bytes (decode (value ))
414+ except Exception :
415+ try :
416+ return cls ._from_byron_base58 (original_str )
417+ except Exception as e :
418+ raise DecodingException (f"Failed to decode address string: { e } " )
419+
420+ # At this point, value is always bytes
421+ # Check if it's a Byron address (CBOR with tag 24)
422+ try :
423+ decoded = cbor2 .loads (value )
424+ if isinstance (decoded , (tuple , list )) and len (decoded ) == 2 :
425+ if isinstance (decoded [0 ], CBORTag ) and decoded [0 ].tag == 24 :
426+ # This is definitely a Byron address - validate and decode it
427+ return cls ._from_byron_cbor (value )
428+ except DecodingException :
429+ # Byron decoding failed with validation error - re-raise it
430+ raise
431+ except Exception :
432+ # Not Byron CBOR (general CBOR decode error), continue with Shelley decoding
433+ pass
434+
435+ # Shelley address decoding (existing logic)
350436 header = value [0 ]
351437 payload = value [1 :]
352438 addr_type = AddressType ((header & 0xF0 ) >> 4 )
@@ -397,16 +483,150 @@ def from_primitive(cls: Type[Address], value: Union[bytes, str]) -> Address:
397483 return cls (None , ScriptHash (payload ), network )
398484 raise DeserializeException (f"Error in deserializing bytes: { value } " )
399485
486+ @classmethod
487+ def _from_byron_base58 (cls : Type [Address ], base58_str : str ) -> Address :
488+ """Decode a Byron address from Base58 string.
489+
490+ Args:
491+ base58_str: Base58-encoded Byron address string.
492+
493+ Returns:
494+ Address: Decoded Byron address instance.
495+
496+ Raises:
497+ DecodingException: When decoding fails.
498+ """
499+ try :
500+ cbor_bytes = base58 .b58decode (base58_str )
501+ except Exception as e :
502+ raise DecodingException (f"Failed to decode Base58 string: { e } " )
503+
504+ return cls ._from_byron_cbor (cbor_bytes )
505+
506+ @classmethod
507+ def _from_byron_cbor (cls : Type [Address ], cbor_bytes : bytes ) -> Address :
508+ """Decode a Byron address from CBOR bytes.
509+
510+ Args:
511+ cbor_bytes: CBOR-encoded Byron address bytes.
512+
513+ Returns:
514+ Address: Decoded Byron address instance.
515+
516+ Raises:
517+ DecodingException: When decoding fails.
518+ """
519+ try :
520+ decoded = cbor2 .loads (cbor_bytes )
521+ except Exception as e :
522+ raise DecodingException (f"Failed to decode CBOR bytes: { e } " )
523+
524+ # Byron address structure: [CBORTag(24, payload), crc32]
525+ if not isinstance (decoded , (tuple , list )) or len (decoded ) != 2 :
526+ raise DecodingException (
527+ f"Byron address must be a 2-element array, got { type (decoded )} "
528+ )
529+
530+ tagged_payload , crc32_checksum = decoded
531+
532+ if not isinstance (tagged_payload , CBORTag ) or tagged_payload .tag != 24 :
533+ raise DecodingException (
534+ f"Byron address must use CBOR tag 24, got { tagged_payload } "
535+ )
536+
537+ payload_cbor = tagged_payload .value
538+ if not isinstance (payload_cbor , bytes ):
539+ raise DecodingException (
540+ f"Tag 24 must contain bytes, got { type (payload_cbor )} "
541+ )
542+
543+ computed_crc32 = binascii .crc32 (payload_cbor ) & 0xFFFFFFFF
544+ if computed_crc32 != crc32_checksum :
545+ raise DecodingException (
546+ f"CRC32 checksum mismatch: expected { crc32_checksum } , got { computed_crc32 } "
547+ )
548+
549+ try :
550+ payload = cbor2 .loads (payload_cbor )
551+ except Exception as e :
552+ raise DecodingException (f"Failed to decode Byron address payload: { e } " )
553+
554+ if not isinstance (payload , (tuple , list )) or len (payload ) != 3 :
555+ raise DecodingException (
556+ f"Byron address payload must be a 3-element array, got { payload } "
557+ )
558+
559+ payload_hash , attributes , byron_type = payload
560+
561+ if not isinstance (payload_hash , bytes ) or len (payload_hash ) != 28 :
562+ size = (
563+ len (payload_hash )
564+ if isinstance (payload_hash , bytes )
565+ else f"type { type (payload_hash ).__name__ } "
566+ )
567+ raise DecodingException (f"Payload hash must be 28 bytes, got { size } " )
568+
569+ if not isinstance (attributes , dict ):
570+ raise DecodingException (
571+ f"Attributes must be a dict, got { type (attributes )} "
572+ )
573+
574+ if byron_type not in (0 , 2 ):
575+ raise DecodingException (f"Byron type must be 0 or 2, got { byron_type } " )
576+
577+ # Create Address instance with Byron fields
578+ addr = cls .__new__ (cls )
579+ addr ._payment_part = None
580+ addr ._staking_part = None
581+ addr ._byron_payload_hash = payload_hash
582+ addr ._byron_attributes = attributes
583+ addr ._byron_type = byron_type
584+ addr ._byron_crc32 = crc32_checksum
585+ addr ._network = addr ._infer_byron_network ()
586+ addr ._address_type = AddressType .BYRON
587+ addr ._header_byte = None
588+ addr ._hrp = None
589+ return addr
590+
591+ def _infer_byron_network (self ) -> Network :
592+ """Infer network from Byron address attributes.
593+
594+ Returns:
595+ Network: MAINNET or TESTNET (defaults to MAINNET).
596+ """
597+ if self ._byron_attributes and 2 in self ._byron_attributes :
598+ network_bytes = self ._byron_attributes [2 ]
599+ if isinstance (network_bytes , bytes ):
600+ try :
601+ network_discriminant = cbor2 .loads (network_bytes )
602+ # Mainnet: 764824073 (0x2D964A09), Testnet: 1097911063 (0x42659F17)
603+ if network_discriminant == 1097911063 :
604+ return Network .TESTNET
605+ except Exception :
606+ pass
607+ return Network .MAINNET
608+
400609 def __eq__ (self , other ):
401610 if not isinstance (other , Address ):
402611 return False
403- else :
612+
613+ if self .is_byron != other .is_byron :
614+ return False
615+
616+ if self .is_byron :
404617 return (
405- other .payment_part == self .payment_part
406- and other .staking_part == self .staking_part
407- and other .network == self .network
618+ self ._byron_payload_hash == other ._byron_payload_hash
619+ and self ._byron_attributes == other ._byron_attributes
620+ and self ._byron_type == other ._byron_type
621+ and self ._byron_crc32 == other ._byron_crc32
408622 )
409623
624+ return (
625+ self .payment_part == other .payment_part
626+ and self .staking_part == other .staking_part
627+ and self .network == other .network
628+ )
629+
410630 def __repr__ (self ):
411631 return f"{ self .encode ()} "
412632
0 commit comments