diff --git a/poetry.lock b/poetry.lock index 298fd53e..ea107675 100644 --- a/poetry.lock +++ b/poetry.lock @@ -580,6 +580,16 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "crc8" +version = "0.2.0" +description = "A module that implements the CRC8 hash algorithm for Python 2 and 3." +optional = false +python-versions = "*" +files = [ + {file = "crc8-0.2.0.tar.gz", hash = "sha256:3c34d0a006ae8ddecfd744ae585eac95120915bf0770011aee9250017c0c40f1"}, +] + [[package]] name = "cryptography" version = "43.0.3" diff --git a/pycardano/cip/cip67.py b/pycardano/cip/cip67.py new file mode 100644 index 00000000..5f6c7bf1 --- /dev/null +++ b/pycardano/cip/cip67.py @@ -0,0 +1,42 @@ +from typing import Union + +from crc8 import crc8 + +from pycardano.transaction import AssetName + + +class InvalidCIP67Token(Exception): + pass + + +class CIP67TokenName(AssetName): + def __repr__(self): + return f"{self.__class__.__name__}({self.payload})" + + def __init__(self, data: Union[bytes, str, AssetName]): + if isinstance(data, AssetName): + data = data.payload + + if isinstance(data, bytes): + data = data.hex() + + if data[0] != "0" or data[7] != "0": + raise InvalidCIP67Token( + "The first and eighth hex values must be 0. Instead found:\n" + + f"first={data[0]}\n" + + f"eigth={data[7]}" + ) + + checksum = crc8(bytes.fromhex(data[1:5])).hexdigest() + if data[5:7] != checksum: + raise InvalidCIP67Token( + f"Token label {data[1:5]} does not match token checksum.\n" + + f"expected={checksum}\n" + + f"received={data[5:7]}" + ) + + super().__init__(bytes.fromhex(data)) + + @property + def label(self) -> int: + return int.from_bytes(self.payload[:3], "big") >> 4 diff --git a/pycardano/cip/cip68.py b/pycardano/cip/cip68.py new file mode 100644 index 00000000..78fad580 --- /dev/null +++ b/pycardano/cip/cip68.py @@ -0,0 +1,93 @@ +from typing import Union + +from pycardano.cip.cip67 import CIP67TokenName +from pycardano.plutus import PlutusData +from pycardano.serialization import ArrayCBORSerializable +from pycardano.serialization import MapCBORSerializable +from pycardano.transaction import AssetName + + +class InvalidCIP68ReferenceNFT(Exception): + pass + + +class CIP68TokenName(CIP67TokenName): + @property + def reference_token(self) -> "CIP68ReferenceNFTName": + ref_token = self.payload.hex()[0] + "00643b" + self.payload.hex()[7:] + + return CIP68ReferenceNFTName(ref_token) + + +class CIP68ReferenceNFTName(CIP68TokenName): + def __init__(self, data: Union[bytes, str, AssetName]): + super().__init__(data) + + if self.label != 100: + raise InvalidCIP68ReferenceNFT("Reference NFT must have label 100.") + + +class CIP68UserNFTName(CIP68TokenName): + def __init__(self, data: Union[bytes, str, AssetName]): + super().__init__(data) + + if self.label != 222: + raise InvalidCIP68ReferenceNFT("User NFT must have label 222.") + + +class CIP68UserNFTFiles(MapCBORSerializable): + name: Union[bytes, None] = None + mediaType: bytes + src: bytes + + +class CIP68UserNFTMetadata(MapCBORSerializable): + name: bytes + image: bytes + description: Union[bytes, None] = None + files: Union[CIP68UserNFTFiles, None] = None + + +class CIP68UserFTName(CIP68TokenName): + def __init__(self, data: Union[bytes, str, AssetName]): + super().__init__(data) + + if self.label != 333: + raise InvalidCIP68ReferenceNFT("User NFT must have label 333.") + + +class CIP68UserFTMetadata(MapCBORSerializable): + name: bytes + description: bytes + ticker: Union[bytes, None] = None + url: Union[bytes, None] = None + decimals: Union[int, None] = None + logo: Union[bytes, None] = None + + +class CIP68UserRFTName(CIP68TokenName): + def __init__(self, data: Union[bytes, str, AssetName]): + super().__init__(data) + + if self.label != 444: + raise InvalidCIP68ReferenceNFT("User NFT must have label 444.") + + +class CIP68UserRFTMetadata(MapCBORSerializable): + name: bytes + image: bytes + description: Union[bytes, None] = None + decimals: Union[int, None] = None + files: Union[CIP68UserNFTFiles, None] = None + + +class CIP68Metadata(ArrayCBORSerializable): + metadata: Union[ + CIP68UserNFTMetadata, + CIP68UserFTMetadata, + CIP68UserRFTMetadata, + MapCBORSerializable, + ArrayCBORSerializable, + ] + version: int + extra: Union[PlutusData, None] = None