1
- from typing import Union
1
+ from typing import Union , Dict , List , Any , TypedDict , Required
2
+ from dataclasses import dataclass
3
+ from cbor2 import CBORTag
2
4
3
5
from pycardano .cip .cip67 import CIP67TokenName
4
- from pycardano .plutus import PlutusData
5
- from pycardano .serialization import ArrayCBORSerializable
6
- from pycardano .serialization import MapCBORSerializable
6
+ from pycardano .plutus import PlutusData , Unit , Primitive
7
7
from pycardano .transaction import AssetName
8
+ from pycardano .serialization import IndefiniteList
8
9
9
10
10
11
class InvalidCIP68ReferenceNFT (Exception ):
11
12
pass
12
13
13
14
14
15
class CIP68TokenName (CIP67TokenName ):
16
+ """Generates a CIP-68 reference token name from an input asset name.
17
+
18
+ The reference_token property generates a reference token name by slicing off the label
19
+ portion of the asset name and assigning the (100) label hex value.
20
+
21
+ For more information on CIP-68 labels:
22
+ https://github.com/cardano-foundation/CIPs/tree/master/CIP-0068
23
+
24
+ Args:
25
+ data: The token name as bytes, str, or AssetName
26
+ """
15
27
@property
16
28
def reference_token (self ) -> "CIP68ReferenceNFTName" :
17
29
ref_token = self .payload .hex ()[0 ] + "00643b" + self .payload .hex ()[7 :]
@@ -20,6 +32,7 @@ def reference_token(self) -> "CIP68ReferenceNFTName":
20
32
21
33
22
34
class CIP68ReferenceNFTName (CIP68TokenName ):
35
+ """Validates that an asset name has the 100 label for reference NFTs."""
23
36
def __init__ (self , data : Union [bytes , str , AssetName ]):
24
37
super ().__init__ (data )
25
38
@@ -28,66 +41,119 @@ def __init__(self, data: Union[bytes, str, AssetName]):
28
41
29
42
30
43
class CIP68UserNFTName (CIP68TokenName ):
44
+ """Validates that an asset name has the 222 label for NFTs."""
31
45
def __init__ (self , data : Union [bytes , str , AssetName ]):
32
46
super ().__init__ (data )
33
47
34
48
if self .label != 222 :
35
49
raise InvalidCIP68ReferenceNFT ("User NFT must have label 222." )
36
50
37
51
38
- class CIP68UserNFTFiles (MapCBORSerializable ):
39
- name : Union [bytes , None ] = None
40
- mediaType : bytes
41
- src : bytes
52
+ class CIP68UserNFTFile (TypedDict , total = False ):
53
+ """Metadata for a single file in NFT metadata."""
54
+ name : bytes
55
+ mediaType : Required [bytes ]
56
+ src : Required [bytes ]
42
57
43
58
44
- class CIP68UserNFTMetadata (MapCBORSerializable ):
45
- name : bytes
46
- image : bytes
47
- description : Union [bytes , None ] = None
48
- files : Union [CIP68UserNFTFiles , None ] = None
59
+ class CIP68UserNFTMetadata (TypedDict , total = False ):
60
+ """Metadata for a user NFT.
61
+
62
+ Multiple files can be included as a list of dictionaries or CIP68UserNFTFile objects.
63
+ """
64
+ name : Required [bytes ]
65
+ image : Required [bytes ]
66
+ description : bytes
67
+ files : Union [List [CIP68UserNFTFile ], None ]
49
68
50
69
51
70
class CIP68UserFTName (CIP68TokenName ):
71
+ """Validates that an asset name has the 333 label for FTs."""
52
72
def __init__ (self , data : Union [bytes , str , AssetName ]):
53
73
super ().__init__ (data )
54
74
55
75
if self .label != 333 :
56
76
raise InvalidCIP68ReferenceNFT ("User NFT must have label 333." )
57
77
58
78
59
- class CIP68UserFTMetadata (MapCBORSerializable ):
60
- name : bytes
61
- description : bytes
62
- ticker : Union [ bytes , None ] = None
63
- url : Union [ bytes , None ] = None
64
- decimals : Union [ int , None ] = None
65
- logo : Union [ bytes , None ] = None
79
+ class CIP68UserFTMetadata (TypedDict , total = False ):
80
+ name : Required [ bytes ]
81
+ description : Required [ bytes ]
82
+ ticker : bytes
83
+ url : bytes
84
+ logo : bytes
85
+ decimals : int
66
86
67
87
68
88
class CIP68UserRFTName (CIP68TokenName ):
89
+ """Validates that an asset name has the 444 label for RFTs."""
69
90
def __init__ (self , data : Union [bytes , str , AssetName ]):
70
91
super ().__init__ (data )
71
92
72
93
if self .label != 444 :
73
94
raise InvalidCIP68ReferenceNFT ("User NFT must have label 444." )
74
95
75
96
76
- class CIP68UserRFTMetadata (MapCBORSerializable ):
77
- name : bytes
78
- image : bytes
79
- description : Union [bytes , None ] = None
80
- decimals : Union [int , None ] = None
81
- files : Union [CIP68UserNFTFiles , None ] = None
82
-
83
-
84
- class CIP68Metadata (ArrayCBORSerializable ):
85
- metadata : Union [
86
- CIP68UserNFTMetadata ,
87
- CIP68UserFTMetadata ,
88
- CIP68UserRFTMetadata ,
89
- MapCBORSerializable ,
90
- ArrayCBORSerializable ,
91
- ]
97
+ class CIP68UserRFTMetadata (TypedDict , total = False ):
98
+ name : Required [bytes ]
99
+ image : Required [bytes ]
100
+ description : bytes
101
+
102
+
103
+ @dataclass
104
+ class CIP68Datum (PlutusData ):
105
+ """Wrapper class for CIP-68 metadata to be used as inline datum.
106
+
107
+ For detailed information on CIP-68 metadata structure and token types:
108
+ https://github.com/cardano-foundation/CIPs/tree/master/CIP-0068
109
+
110
+ This class wraps metadata dictionaries in a PlutusData class for attaching to a
111
+ reference NFT transaction as an inline datum.
112
+
113
+ Args:
114
+ metadata: A metadata dictionary. TypedDict classes are provided to define required
115
+ fields for each token type.
116
+ version: Metadata version number as 'int'
117
+ extra: Required - must be a PlutusData, or Unit() for empty PlutusData.
118
+
119
+ Example:
120
+ metadata = {
121
+ b"name": b"My NFT",
122
+ b"image": b"ipfs://...",
123
+ b"files": [{"mediaType": b"image/png", "src": b"ipfs://..."}]
124
+ }
125
+ datum = CIP68Datum(metadata=metadata, version=1, extra=Unit())
126
+ """
127
+ CONSTR_ID = 0
128
+
129
+ metadata : Dict [bytes , Any ]
92
130
version : int
93
- extra : Union [PlutusData , None ] = None
131
+ extra : Any # This should be PlutusData or Unit() for empty PlutusData
132
+
133
+ def __post_init__ (self ):
134
+ converted_metadata : Dict [bytes , Any ] = {}
135
+ for k , v in self .metadata .items ():
136
+ key = k .encode () if isinstance (k , str ) else k
137
+ if isinstance (v , dict ):
138
+ v = dict ((k .encode () if isinstance (k , str ) else k , v ) for k , v in v .items ())
139
+ elif isinstance (v , list ):
140
+ v = IndefiniteList ([dict ((k .encode () if isinstance (k , str ) else k , v ) for k , v in item .items ())
141
+ if isinstance (item , dict ) else item for item in v ])
142
+ converted_metadata [key ] = v
143
+ self .metadata = converted_metadata
144
+
145
+ def to_shallow_primitive (self ) -> CBORTag :
146
+ """Wraps PlutusData in 'extra' field in an indefinite list when converted to a CBOR primitive."""
147
+ primitives : Primitive = super ().to_shallow_primitive ()
148
+ if isinstance (primitives , CBORTag ):
149
+ value = primitives .value
150
+ if value :
151
+ extra = value [2 ]
152
+ if isinstance (extra , Unit ):
153
+ extra = CBORTag (121 , IndefiniteList ([]))
154
+ elif isinstance (extra , CBORTag ):
155
+ extra = CBORTag (extra .tag , IndefiniteList (extra .value ))
156
+ value = [value [0 ], value [1 ], extra ]
157
+ return CBORTag (121 , value )
158
+
159
+
0 commit comments