Skip to content

Commit 590c49a

Browse files
authored
Support to/from json raw plutus data (#300)
* Add to_json for RawPlutusData * Add from_json for RawPlutusData * Add test for serialization and deserialization of generic datums in PlutusData * Ensure that non-CborTag primitives are correctly handled as well * Fix bug in conversion
1 parent df5ba28 commit 590c49a

File tree

2 files changed

+180
-7
lines changed

2 files changed

+180
-7
lines changed

pycardano/plutus.py

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,26 @@ def get_tag(constr_id: int) -> Optional[int]:
449449
return None
450450

451451

452+
def get_constructor_id_and_fields(
453+
raw_tag: CBORTag,
454+
) -> typing.Tuple[int, typing.List[Any]]:
455+
tag = raw_tag.tag
456+
if tag == 102:
457+
if len(raw_tag.value) != 2:
458+
raise DeserializeException(
459+
f"Expect the length of value to be exactly 2, got {len(raw_tag.value)} instead."
460+
)
461+
return raw_tag.value[0], raw_tag.value[1]
462+
else:
463+
if 121 <= tag < 128:
464+
constr = tag - 121
465+
elif 1280 <= tag < 1536:
466+
constr = tag - 1280 + 7
467+
else:
468+
raise DeserializeException(f"Unexpected tag for RawPlutusData: {tag}")
469+
return constr, raw_tag.value
470+
471+
452472
def id_map(cls, skip_constructor=False):
453473
"""
454474
Constructs a unique representation of a PlutusData type definition.
@@ -579,14 +599,12 @@ def from_primitive(cls: Type[PlutusData], value: CBORTag) -> PlutusData:
579599
def hash(self) -> DatumHash:
580600
return datum_hash(self)
581601

582-
def to_json(self, **kwargs) -> str:
583-
"""Convert to a json string
584-
585-
Args:
586-
**kwargs: Extra key word arguments to be passed to `json.dumps()`
602+
def to_dict(self) -> dict:
603+
"""
604+
Convert to a dictionary.
587605
588606
Returns:
589-
str: a JSON encoded PlutusData.
607+
str: a dict PlutusData that can be JSON encoded.
590608
"""
591609

592610
def _dfs(obj):
@@ -610,10 +628,26 @@ def _dfs(obj):
610628
"constructor": obj.CONSTR_ID,
611629
"fields": [_dfs(getattr(obj, f.name)) for f in fields(obj)],
612630
}
631+
elif isinstance(obj, RawPlutusData):
632+
return obj.to_dict()
633+
elif isinstance(obj, RawCBOR):
634+
return RawPlutusData.from_cbor(obj.cbor).to_dict()
613635
else:
614636
raise TypeError(f"Unexpected type {type(obj)}")
615637

616-
return json.dumps(_dfs(self), **kwargs)
638+
return _dfs(self)
639+
640+
def to_json(self, **kwargs) -> str:
641+
"""Convert to a json string
642+
643+
Args:
644+
**kwargs: Extra key word arguments to be passed to `json.dumps()`
645+
646+
Returns:
647+
str: a JSON encoded PlutusData.
648+
"""
649+
650+
return json.dumps(self.to_dict(), **kwargs)
617651

618652
@classmethod
619653
def from_dict(cls: Type[PlutusData], data: dict) -> PlutusData:
@@ -640,6 +674,8 @@ def _dfs(obj):
640674
f_info.type, PlutusData
641675
):
642676
converted_fields.append(f_info.type.from_dict(f))
677+
elif f_info.type == Datum:
678+
converted_fields.append(RawPlutusData.from_dict(f))
643679
elif (
644680
hasattr(f_info.type, "__origin__")
645681
and f_info.type.__origin__ is Union
@@ -765,11 +801,104 @@ def _dfs(obj):
765801

766802
return _dfs(self.data)
767803

804+
def to_dict(self) -> dict:
805+
"""
806+
Convert to a dictionary.
807+
808+
Returns:
809+
str: a dict RawPlutusData that can be JSON encoded.
810+
"""
811+
812+
def _dfs(obj):
813+
if isinstance(obj, int):
814+
return {"int": obj}
815+
elif isinstance(obj, bytes):
816+
return {"bytes": obj.hex()}
817+
elif isinstance(obj, ByteString):
818+
return {"bytes": obj.value.hex()}
819+
elif isinstance(obj, IndefiniteList) or isinstance(obj, list):
820+
return {"list": [_dfs(item) for item in obj]}
821+
elif isinstance(obj, dict):
822+
return {"map": [{"v": _dfs(v), "k": _dfs(k)} for k, v in obj.items()]}
823+
elif isinstance(obj, CBORTag):
824+
constructor, fields = get_constructor_id_and_fields(obj)
825+
return {"constructor": constructor, "fields": [_dfs(f) for f in fields]}
826+
raise TypeError(f"Unexpected type {type(obj)}")
827+
828+
return _dfs(RawPlutusData.to_primitive(self))
829+
830+
def to_json(self, **kwargs) -> str:
831+
"""Convert to a json string
832+
833+
Args:
834+
**kwargs: Extra key word arguments to be passed to `json.dumps()`
835+
836+
Returns:
837+
str: a JSON encoded RawPlutusData.
838+
"""
839+
840+
return json.dumps(RawPlutusData.to_dict(self), **kwargs)
841+
768842
@classmethod
769843
@limit_primitive_type(CBORTag)
770844
def from_primitive(cls: Type[RawPlutusData], value: CBORTag) -> RawPlutusData:
771845
return cls(value)
772846

847+
@classmethod
848+
def from_dict(cls: Type[RawPlutusData], data: dict) -> RawPlutusData:
849+
"""Convert a dictionary to RawPlutusData
850+
851+
Args:
852+
data (dict): A dictionary.
853+
854+
Returns:
855+
RawPlutusData: Restored RawPlutusData.
856+
"""
857+
858+
def _dfs(obj):
859+
if isinstance(obj, dict):
860+
if "constructor" in obj:
861+
converted_fields = []
862+
for f in obj["fields"]:
863+
converted_fields.append(_dfs(f))
864+
tag = get_tag(obj["constructor"])
865+
if tag is None:
866+
return CBORTag(
867+
102, [obj["constructor"], IndefiniteList(converted_fields)]
868+
)
869+
else:
870+
return CBORTag(tag, converted_fields)
871+
elif "map" in obj:
872+
return {_dfs(pair["k"]): _dfs(pair["v"]) for pair in obj["map"]}
873+
elif "int" in obj:
874+
return obj["int"]
875+
elif "bytes" in obj:
876+
if len(obj["bytes"]) > 64:
877+
return ByteString(bytes.fromhex(obj["bytes"]))
878+
else:
879+
return bytes.fromhex(obj["bytes"])
880+
elif "list" in obj:
881+
return IndefiniteList([_dfs(item) for item in obj["list"]])
882+
else:
883+
raise DeserializeException(f"Unexpected data structure: {obj}")
884+
else:
885+
raise TypeError(f"Unexpected data type: {type(obj)}")
886+
887+
return cls(_dfs(data))
888+
889+
@classmethod
890+
def from_json(cls: Type[RawPlutusData], data: str) -> RawPlutusData:
891+
"""Restore a json encoded string to a RawPlutusData.
892+
893+
Args:
894+
data (str): An encoded json string.
895+
896+
Returns:
897+
RawPlutusData: The restored RawPlutusData.
898+
"""
899+
obj = json.loads(data)
900+
return cls.from_dict(obj)
901+
773902
def __deepcopy__(self, memo):
774903
return self.__class__.from_cbor(self.to_cbor_hex())
775904

test/pycardano/test_plutus.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,50 @@ def test_plutus_data_from_json_wrong_data_structure_type():
207207
MyTest.from_json(test)
208208

209209

210+
def test_raw_plutus_data_json():
211+
key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a")
212+
deadline = 1643235300000
213+
testa = BigTest(MyTest(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}))
214+
testb = LargestTest()
215+
216+
my_vesting = VestingParam(
217+
beneficiary=key_hash, deadline=deadline, testa=testa, testb=testb
218+
)
219+
220+
my_vesting_primitive = my_vesting.to_primitive()
221+
encoded_json = RawPlutusData(my_vesting_primitive).to_json(separators=(",", ":"))
222+
223+
assert (
224+
'{"constructor":1,"fields":[{"bytes":"c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a"},'
225+
'{"int":1643235300000},{"constructor":8,"fields":[{"constructor":130,"fields":[{"int":123},'
226+
'{"bytes":"31323334"},{"list":[{"int":4},{"int":5},{"int":6}]},{"map":[{"v":{"bytes":"31"},'
227+
'"k":{"int":1}},{"v":{"bytes":"32"},"k":{"int":2}}]}]}]},{"constructor":9,"fields":[]}]}'
228+
== encoded_json
229+
)
230+
231+
# note that json encoding is lossy, so we can't compare the original object with the one decoded from json
232+
# but we can compare the jsons
233+
assert encoded_json == RawPlutusData.from_json(encoded_json).to_json(
234+
separators=(",", ":")
235+
)
236+
237+
@dataclass
238+
class C(PlutusData):
239+
CONSTR_ID = 2
240+
x: Datum
241+
y: Datum
242+
z: int
243+
244+
c = C(RawPlutusData(testb.to_primitive()), RawCBOR(testa.to_cbor()), 1)
245+
encoded_json = c.to_json(separators=(",", ":"))
246+
247+
assert (
248+
'{"constructor":2,"fields":[{"constructor":9,"fields":[]},{"constructor":8,"fields":[{"constructor":130,"fields":[{"int":123},{"bytes":"31323334"},{"list":[{"int":4},{"int":5},{"int":6}]},{"map":[{"v":{"bytes":"31"},"k":{"int":1}},{"v":{"bytes":"32"},"k":{"int":2}}]}]}]},{"int":1}]}'
249+
== encoded_json
250+
)
251+
assert encoded_json == C.from_json(encoded_json).to_json(separators=(",", ":"))
252+
253+
210254
def test_plutus_data_hash():
211255
assert (
212256
"923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec"

0 commit comments

Comments
 (0)