Skip to content
1 change: 1 addition & 0 deletions pycardano/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def _validate_type_and_size(data):
if len(data) > self.MAX_ITEM_SIZE:
raise InvalidArgumentException(
f"The size of {data} exceeds {self.MAX_ITEM_SIZE} bytes."
"Use pycardano.serialization.ByteString for long bytes."
)
elif isinstance(data, str):
if len(data.encode("utf-8")) > self.MAX_ITEM_SIZE:
Expand Down
21 changes: 18 additions & 3 deletions pycardano/plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
from nacl.encoding import RawEncoder
from nacl.hash import blake2b

from pycardano.exception import DeserializeException
from pycardano.exception import DeserializeException, InvalidArgumentException
from pycardano.hash import DATUM_HASH_SIZE, SCRIPT_HASH_SIZE, DatumHash, ScriptHash
from pycardano.nativescript import NativeScript
from pycardano.serialization import (
ArrayCBORSerializable,
ByteString,
CBORSerializable,
DictCBORSerializable,
IndefiniteList,
Expand Down Expand Up @@ -468,6 +469,8 @@ class will reduce the complexity of serialization and deserialization tremendous
>>> assert test == Test.from_cbor("d87a9f187b43333231ff")
"""

MAX_BYTES_SIZE = 64

@classproperty
def CONSTR_ID(cls):
"""
Expand All @@ -489,13 +492,20 @@ def CONSTR_ID(cls):
return getattr(cls, k)

def __post_init__(self):
valid_types = (PlutusData, dict, IndefiniteList, int, bytes)
valid_types = (PlutusData, dict, IndefiniteList, int, ByteString, bytes)
for f in fields(self):
if inspect.isclass(f.type) and not issubclass(f.type, valid_types):
raise TypeError(
f"Invalid field type: {f.type}. A field in PlutusData should be one of {valid_types}"
)

data = getattr(self, f.name)
if isinstance(data, bytes) and len(data) > 64:
raise InvalidArgumentException(
f"The size of {data} exceeds {self.MAX_BYTES_SIZE} bytes. "
"Use pycardano.serialization.ByteString for long bytes."
)

def to_shallow_primitive(self) -> CBORTag:
primitives: Primitive = super().to_shallow_primitive()
if primitives:
Expand Down Expand Up @@ -553,6 +563,8 @@ def _dfs(obj):
return {"int": obj}
elif isinstance(obj, bytes):
return {"bytes": obj.hex()}
elif isinstance(obj, ByteString):
return {"bytes": obj.value.hex()}
elif isinstance(obj, IndefiniteList) or isinstance(obj, list):
return {"list": [_dfs(item) for item in obj]}
elif isinstance(obj, dict):
Expand Down Expand Up @@ -667,7 +679,10 @@ def _dfs(obj):
elif "int" in obj:
return obj["int"]
elif "bytes" in obj:
return bytes.fromhex(obj["bytes"])
if len(obj["bytes"]) > 64:
return ByteString(bytes.fromhex(obj["bytes"]))
else:
return bytes.fromhex(obj["bytes"])
elif "list" in obj:
return IndefiniteList([_dfs(item) for item in obj["list"]])
else:
Expand Down
26 changes: 26 additions & 0 deletions pycardano/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ class IndefiniteFrozenList(FrozenList, IndefiniteList): # type: ignore
pass


@dataclass
class ByteString:
value: bytes

def __hash__(self):
return hash(self.value)

def __eq__(self, other: object):
if isinstance(other, ByteString):
return self.value == other.value
elif isinstance(other, bytes):
return self.value == other
else:
return False


@dataclass
class RawCBOR:
"""A wrapper class for bytes that represents a CBOR value."""
Expand Down Expand Up @@ -160,6 +176,7 @@ def default_encoder(
assert isinstance(
value,
(
ByteString,
CBORSerializable,
IndefiniteList,
RawCBOR,
Expand All @@ -178,6 +195,15 @@ def default_encoder(
for item in value:
encoder.encode(item)
encoder.write(b"\xff")
elif isinstance(value, ByteString):
if len(value.value) > 64:
encoder.write(b"\x5f")
for i in range(0, len(value.value), 64):
imax = min(i + 64, len(value.value))
encoder.encode(value.value[i:imax])
encoder.write(b"\xff")
else:
encoder.encode(value.value)
elif isinstance(value, RawCBOR):
encoder.write(value.cbor)
elif isinstance(value, FrozenList):
Expand Down
24 changes: 23 additions & 1 deletion test/pycardano/test_plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
RedeemerTag,
plutus_script_hash,
)
from pycardano.serialization import IndefiniteList
from pycardano.serialization import ByteString, IndefiniteList


@dataclass
Expand Down Expand Up @@ -396,3 +396,25 @@ class A(PlutusData):
assert (
res == res2
), "Same class has different default constructor id in two consecutive runs"


def test_plutus_data_long_bytes():
@dataclass
class A(PlutusData):
a: ByteString

quote = (
"The line separating good and evil passes ... right through every human heart."
)

quote_hex = (
"d866821a8e5890cf9f5f5840546865206c696e652073657061726174696e6720676f6f6420616"
"e64206576696c20706173736573202e2e2e207269676874207468726f7567682065766572794d"
"2068756d616e2068656172742effff"
)

A_tmp = A(ByteString(quote.encode()))

assert (
A_tmp.to_cbor_hex() == quote_hex
), "Long metadata bytestring is encoded incorrectly."