44from enum import Enum , unique
55from typing import Optional , Tuple , Type , Union
66
7- from pycardano .exception import DeserializeException
8- from pycardano .hash import AnchorDataHash , PoolKeyHash , ScriptHash , VerificationKeyHash
7+ from pycardano .crypto .bech32 import bech32_decode , convertbits , encode
8+ from pycardano .exception import (
9+ DecodingException ,
10+ DeserializeException ,
11+ SerializeException ,
12+ )
13+ from pycardano .hash import (
14+ CIP129_PAYLOAD_SIZE ,
15+ VERIFICATION_KEY_HASH_SIZE ,
16+ AnchorDataHash ,
17+ PoolKeyHash ,
18+ ScriptHash ,
19+ VerificationKeyHash ,
20+ )
921from pycardano .serialization import (
1022 ArrayCBORSerializable ,
1123 CodedSerializable ,
3648 "RegDRepCert" ,
3749 "UnregDRepCertificate" ,
3850 "UpdateDRepCertificate" ,
51+ "GovernanceCredential" ,
52+ "GovernanceKeyType" ,
3953]
4054
4155from pycardano .pool_params import PoolParams
@@ -92,15 +106,162 @@ def __hash__(self):
92106 return hash (self .to_cbor ())
93107
94108
109+ class IdFormat (Enum ):
110+ """
111+ Id format definition.
112+ """
113+
114+ CIP129 = "cip129"
115+ CIP105 = "cip105"
116+
117+
118+ class CredentialType (Enum ):
119+ """
120+ Credential type definition.
121+ """
122+
123+ KEY_HASH = 0b0010
124+ """Key hash"""
125+
126+ SCRIPT_HASH = 0b0011
127+ """Script hash"""
128+
129+
130+ class GovernanceKeyType (Enum ):
131+ """
132+ Governance key type definition.
133+ """
134+
135+ CC_HOT = 0b0000
136+ """Committee cold hot key"""
137+
138+ CC_COLD = 0b0001
139+ """Committee cold key"""
140+
141+ DREP = 0b0010
142+ """DRep key"""
143+
144+
145+ @dataclass (repr = False )
146+ class GovernanceCredential (StakeCredential ):
147+ """Represents a governance credential."""
148+
149+ governance_key_type : GovernanceKeyType = field (init = False )
150+ """Governance key type."""
151+
152+ id_format : IdFormat = field (default = IdFormat .CIP129 , compare = False )
153+ """Id format."""
154+
155+ def __repr__ (self ):
156+ return f"{ self .encode ()} "
157+
158+ def __bytes__ (self ):
159+ if self .id_format == IdFormat .CIP129 :
160+ return self ._compute_header_byte () + bytes (self .credential .payload )
161+ else :
162+ return bytes (self .credential .payload )
163+
164+ @property
165+ def credential_type (self ) -> CredentialType :
166+ """Credential type."""
167+ if isinstance (self .credential , VerificationKeyHash ):
168+ return CredentialType .KEY_HASH
169+ else :
170+ return CredentialType .SCRIPT_HASH
171+
172+ def _compute_header_byte (self ) -> bytes :
173+ """Compute the header byte."""
174+ return (
175+ self .governance_key_type .value << 4 | self .credential_type .value
176+ ).to_bytes (1 , byteorder = "big" )
177+
178+ def _compute_hrp (self , id_format : IdFormat = IdFormat .CIP129 ) -> str :
179+ """Compute human-readable prefix for bech32 encoder.
180+
181+ Based on
182+ `miscellaneous section <https://github.com/cardano-foundation/CIPs/tree/master/CIP-0005#miscellaneous>`_
183+ in CIP-5.
184+ """
185+ prefix = ""
186+ if self .governance_key_type == GovernanceKeyType .CC_HOT :
187+ prefix = "cc_hot"
188+ elif self .governance_key_type == GovernanceKeyType .CC_COLD :
189+ prefix = "cc_cold"
190+ elif self .governance_key_type == GovernanceKeyType .DREP :
191+ prefix = "drep"
192+
193+ suffix = ""
194+ if isinstance (self .credential , VerificationKeyHash ):
195+ suffix = ""
196+ elif isinstance (self .credential , ScriptHash ):
197+ suffix = "_script"
198+
199+ return prefix + suffix if id_format == IdFormat .CIP105 else prefix
200+
201+ def encode (self ) -> str :
202+ """Encode the governance credential in Bech32 format.
203+
204+ More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
205+
206+ Returns:
207+ str: Encoded governance credential in Bech32 format.
208+ """
209+ data = bytes (self )
210+ return encode (self ._compute_hrp (self .id_format ), data )
211+
212+ @classmethod
213+ def decode (cls : Type [GovernanceCredential ], data : str ) -> GovernanceCredential :
214+ """Decode a bech32 string into a governance credential object.
215+
216+ Args:
217+ data (str): Bech32-encoded string.
218+
219+ Returns:
220+ GovernanceCredential: Decoded governance credential.
221+
222+ Raises:
223+ DecodingException: When the input string is not a valid governance credential.
224+ """
225+ hrp , checksum , _ = bech32_decode (data )
226+ value = bytes (convertbits (checksum , 5 , 8 , False ))
227+ if len (value ) == VERIFICATION_KEY_HASH_SIZE :
228+ # CIP-105
229+ if "script" in hrp :
230+ return cls (credential = ScriptHash (value ))
231+ else :
232+ return cls (credential = VerificationKeyHash (value ))
233+ elif len (value ) == CIP129_PAYLOAD_SIZE :
234+ header = value [0 ]
235+ payload = value [1 :]
236+
237+ key_type = GovernanceKeyType ((header & 0xF0 ) >> 4 )
238+ credential_type = CredentialType (header & 0x0F )
239+
240+ if key_type != cls .governance_key_type :
241+ raise DecodingException (f"Invalid key type: { key_type } " )
242+
243+ if credential_type == CredentialType .KEY_HASH :
244+ return cls (credential = VerificationKeyHash (payload ))
245+ elif credential_type == CredentialType .SCRIPT_HASH :
246+ return cls (credential = ScriptHash (payload ))
247+ else :
248+ raise DecodingException (f"Invalid credential type: { credential_type } " )
249+ else :
250+ raise DecodingException (f"Invalid data length: { len (value )} " )
251+
252+ def to_primitive (self ):
253+ return [self ._CODE , self .credential .to_primitive ()]
254+
255+
95256@dataclass (repr = False )
96- class DRepCredential (StakeCredential ):
257+ class DRepCredential (GovernanceCredential ):
97258 """Represents a Delegate Representative (DRep) credential.
98259
99260 This credential type is specifically used for DReps in the governance system,
100- inheriting from StakeCredential .
261+ inheriting from GovernanceCredential .
101262 """
102263
103- pass
264+ governance_key_type : GovernanceKeyType = GovernanceKeyType . DREP
104265
105266
106267@unique
@@ -135,13 +296,26 @@ class DRep(ArrayCBORSerializable):
135296 )
136297 """The credential associated with this DRep, if applicable"""
137298
299+ id_format : IdFormat = field (default = IdFormat .CIP129 , compare = False )
300+
301+ def __repr__ (self ):
302+ return f"{ self .encode ()} "
303+
304+ def __bytes__ (self ):
305+ if self .credential is not None :
306+ drep_credential = DRepCredential (
307+ credential = self .credential , id_format = self .id_format
308+ )
309+ return bytes (drep_credential )
310+ return b""
311+
138312 @classmethod
139313 @limit_primitive_type (list )
140314 def from_primitive (cls : Type [DRep ], values : Union [list , tuple ]) -> DRep :
141315 try :
142316 kind = DRepKind (values [0 ])
143- except ValueError :
144- raise DeserializeException (f"Invalid DRep type { values [0 ]} " )
317+ except ValueError as e :
318+ raise DeserializeException (f"Invalid DRep type { values [0 ]} " ) from e
145319
146320 if kind == DRepKind .VERIFICATION_KEY_HASH :
147321 return cls (kind = kind , credential = VerificationKeyHash (values [1 ]))
@@ -159,6 +333,65 @@ def to_primitive(self):
159333 return [self .kind .value , self .credential .to_primitive ()]
160334 return [self .kind .value ]
161335
336+ def encode (self ) -> str :
337+ """Encode the DRep in Bech32 format.
338+
339+ More info about Bech32 `here <https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32>`_.
340+
341+ Returns:
342+ str: Encoded DRep in Bech32 format.
343+
344+ Examples:
345+ >>> vkey_bytes = bytes.fromhex("00000000000000000000000000000000000000000000000000000000")
346+ >>> credential = VerificationKeyHash(vkey_bytes)
347+ >>> print(DRep(kind=DRepKind.VERIFICATION_KEY_HASH, credential=credential).encode())
348+ drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n
349+ """
350+ if self .kind == DRepKind .ALWAYS_ABSTAIN :
351+ return "drep_always_abstain"
352+ elif self .kind == DRepKind .ALWAYS_NO_CONFIDENCE :
353+ return "drep_always_no_confidence"
354+ elif self .credential is not None :
355+ drep_credential = DRepCredential (
356+ credential = self .credential , id_format = self .id_format
357+ )
358+ return drep_credential .encode ()
359+ else :
360+ raise SerializeException ("DRep credential is None" )
361+
362+ @classmethod
363+ def decode (cls : Type [DRep ], data : str ) -> DRep :
364+ """Decode a bech32 string into a DRep object.
365+
366+ Args:
367+ data (str): Bech32-encoded string.
368+
369+ Returns:
370+ DRep: Decoded DRep.
371+
372+ Raises:
373+ DecodingException: When the input string is not a valid DRep.
374+
375+ Examples:
376+ >>> credential = DRep.decode("drep1ygqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq7vlc9n")
377+ >>> khash = VerificationKeyHash(bytes.fromhex("00000000000000000000000000000000000000000000000000000000"))
378+ >>> assert credential == DRep(DRepKind.VERIFICATION_KEY_HASH, khash)
379+ """
380+ if data == "drep_always_abstain" :
381+ return cls (kind = DRepKind .ALWAYS_ABSTAIN )
382+ elif data == "drep_always_no_confidence" :
383+ return cls (kind = DRepKind .ALWAYS_NO_CONFIDENCE )
384+ else :
385+ drep_credential = DRepCredential .decode (data )
386+ return cls (
387+ kind = (
388+ DRepKind .VERIFICATION_KEY_HASH
389+ if isinstance (drep_credential .credential , VerificationKeyHash )
390+ else DRepKind .SCRIPT_HASH
391+ ),
392+ credential = drep_credential .credential ,
393+ )
394+
162395
163396@dataclass (repr = False )
164397class StakeRegistration (CodedSerializable ):
0 commit comments