From 9f0dc9cee6790c5ba2230137d7a473ab2722cfe7 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Fri, 1 Aug 2025 20:01:27 -0500 Subject: [PATCH 01/14] refactor: update script_data_hash to support NonEmptyOrderedSet for datums --- pycardano/utils.py | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/pycardano/utils.py b/pycardano/utils.py index 7c912160..2b529afe 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -13,7 +13,7 @@ from pycardano.backend.base import ChainContext from pycardano.hash import SCRIPT_DATA_HASH_SIZE, SCRIPT_HASH_SIZE, ScriptDataHash from pycardano.plutus import COST_MODELS, CostModels, Datum, Redeemers -from pycardano.serialization import default_encoder +from pycardano.serialization import NonEmptyOrderedSet, default_encoder from pycardano.transaction import MultiAsset, TransactionOutput, Value __all__ = [ @@ -235,35 +235,54 @@ def min_lovelace_post_alonzo(output: TransactionOutput, context: ChainContext) - def script_data_hash( - redeemers: Redeemers, - datums: List[Datum], + redeemers: Optional[Redeemers] = None, + datums: Optional[Union[List[Datum], NonEmptyOrderedSet[Datum]]] = None, cost_models: Optional[Union[CostModels, Dict]] = None, ) -> ScriptDataHash: """Calculate plutus script data hash Args: - redeemers (Redeemers): Redeemers to include. - datums (List[Datum]): Datums to include. + redeemers (Optional[Redeemers]): Redeemers to include. + datums (Optional[Union[List[Datum], NonEmptyOrderedSet[Datum]]]): Datums to include. cost_models (Optional[CostModels]): Cost models. Returns: ScriptDataHash: Plutus script data hash """ + # Handle empty redeemers case - should be encoded as an empty map (A0 in hex) if not redeemers: + redeemer_bytes = cbor2.dumps({}, default=default_encoder) cost_models = {} - elif not cost_models: - cost_models = COST_MODELS + else: + redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) + if not cost_models: + cost_models = COST_MODELS - redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) + # Handle datums - if no datums, use empty bytestring if datums: - datum_bytes = cbor2.dumps(datums, default=default_encoder) + if isinstance(datums, list): + # If datums is a NonEmptyOrderedSet, convert it to a shallow primitive representation + # to ensure correct CBOR encoding + datums = NonEmptyOrderedSet(datums) + datum_bytes = cbor2.dumps( + datums.to_shallow_primitive(), default=default_encoder + ) else: datum_bytes = b"" - cost_models_bytes = cbor2.dumps(cost_models, default=default_encoder) + + # Encode cost models - must use definite length encoding + cost_models_bytes = cbor2.dumps( + cost_models, + default=default_encoder, + canonical=True, # Ensures definite length encoding and canonical map keys + ) + + # Concatenate in order: redeemers || datums || language views + data = redeemer_bytes + datum_bytes + cost_models_bytes return ScriptDataHash( blake2b( - redeemer_bytes + datum_bytes + cost_models_bytes, + data, SCRIPT_DATA_HASH_SIZE, encoder=RawEncoder, ) From 5f4c7a56233a2966311af1d7d255d44cb595c40f Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Fri, 1 Aug 2025 20:02:32 -0500 Subject: [PATCH 02/14] fix: update plutus_data to support NonEmptyOrderedSet --- pycardano/witness.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pycardano/witness.py b/pycardano/witness.py index 3d86e1a5..3bac7bf0 100644 --- a/pycardano/witness.py +++ b/pycardano/witness.py @@ -9,19 +9,12 @@ from pycardano.key import ExtendedVerificationKey, VerificationKey from pycardano.nativescript import NativeScript -from pycardano.plutus import ( - PlutusV1Script, - PlutusV2Script, - PlutusV3Script, - RawPlutusData, - Redeemers, -) +from pycardano.plutus import PlutusV1Script, PlutusV2Script, PlutusV3Script, Redeemers from pycardano.serialization import ( ArrayCBORSerializable, MapCBORSerializable, NonEmptyOrderedSet, limit_primitive_type, - list_hook, ) __all__ = ["VerificationKeyWitness", "TransactionWitnessSet"] @@ -114,9 +107,9 @@ class TransactionWitnessSet(MapCBORSerializable): }, ) - plutus_data: Optional[List[Any]] = field( + plutus_data: Optional[Union[List[Any], NonEmptyOrderedSet[Any]]] = field( default=None, - metadata={"optional": True, "key": 4, "object_hook": list_hook(RawPlutusData)}, + metadata={"optional": True, "key": 4}, ) redeemer: Optional[Redeemers] = field( @@ -150,6 +143,10 @@ def __post_init__(self): self.vkey_witnesses = NonEmptyOrderedSet(self.vkey_witnesses) if isinstance(self.native_scripts, list): self.native_scripts = NonEmptyOrderedSet(self.native_scripts) + if isinstance(self.plutus_data, list) and not isinstance( + self.plutus_data, NonEmptyOrderedSet + ): + self.plutus_data = NonEmptyOrderedSet(list(self.plutus_data)) if isinstance(self.plutus_v1_script, list): self.plutus_v1_script = NonEmptyOrderedSet(self.plutus_v1_script) if isinstance(self.plutus_v2_script, list): From cde7bfe391b6e39dff90946d4431c28759e5330b Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Fri, 1 Aug 2025 20:03:03 -0500 Subject: [PATCH 03/14] fix: update plutus_data to use NonEmptyOrderedSet for improved data handling --- pycardano/txbuilder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 1da8d8c6..7cd7c59d 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -2,7 +2,7 @@ from copy import deepcopy from dataclasses import dataclass, field, fields -from typing import Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union from pycardano import RedeemerMap from pycardano.address import Address, AddressType @@ -1170,6 +1170,7 @@ def build_witness_set( plutus_v1_scripts: NonEmptyOrderedSet[PlutusV1Script] = NonEmptyOrderedSet() plutus_v2_scripts: NonEmptyOrderedSet[PlutusV2Script] = NonEmptyOrderedSet() plutus_v3_scripts: NonEmptyOrderedSet[PlutusV3Script] = NonEmptyOrderedSet() + plutus_data: NonEmptyOrderedSet[Any] = NonEmptyOrderedSet() input_scripts = ( { @@ -1181,6 +1182,9 @@ def build_witness_set( else {} ) + for datum in self.datums.values(): + plutus_data.append(datum) + for script in self.scripts: if script_hash(script) not in input_scripts: if isinstance(script, NativeScript): @@ -1204,7 +1208,7 @@ def build_witness_set( plutus_v2_script=plutus_v2_scripts if plutus_v2_scripts else None, plutus_v3_script=plutus_v3_scripts if plutus_v3_scripts else None, redeemer=self.redeemers() if self._redeemer_list else None, - plutus_data=list(self.datums.values()) if self.datums else None, + plutus_data=plutus_data if plutus_data else None, ) def _ensure_no_input_exclusion_conflict(self): From cd6a1cd61cfd8d6f1746bf75eda347d4a7eb2edb Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Fri, 1 Aug 2025 20:03:35 -0500 Subject: [PATCH 04/14] refactor: enhance OrderedSet to support IndefiniteList for improved serialization --- pycardano/serialization.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index 75c1c1fa..b45df904 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -1128,12 +1128,18 @@ def list_hook( return lambda vals: [cls.from_primitive(v) for v in vals] -class OrderedSet(list, Generic[T], CBORSerializable): - def __init__(self, iterable: Optional[List[T]] = None, use_tag: bool = True): +class OrderedSet(list, IndefiniteList, Generic[T], CBORSerializable): # type: ignore + def __init__( + self, + iterable: Optional[Union[List[T], IndefiniteList]] = None, + use_tag: bool = True, + ): super().__init__() self._set: Set[str] = set() self._use_tag = use_tag + self._is_indefinite_list = False if iterable: + self._is_indefinite_list = isinstance(iterable, IndefiniteList) self.extend(iterable) def append(self, item: T) -> None: @@ -1143,6 +1149,7 @@ def append(self, item: T) -> None: self._set.add(item_key) def extend(self, items: Iterable[T]) -> None: + self._is_indefinite_list = isinstance(items, IndefiniteList) for item in items: self.append(item) @@ -1159,10 +1166,13 @@ def __eq__(self, other: object) -> bool: def __repr__(self) -> str: return f"{self.__class__.__name__}({list(self)})" - def to_shallow_primitive(self) -> Union[CBORTag, List[T]]: + def to_shallow_primitive(self) -> Union[CBORTag, Union[List[T], IndefiniteList]]: if self._use_tag: - return CBORTag(258, list(self)) - return list(self) + return CBORTag( + 258, + IndefiniteList(list(self)) if self._is_indefinite_list else list(self), + ) + return IndefiniteList(list(self)) if self._is_indefinite_list else list(self) @classmethod def from_primitive( @@ -1195,7 +1205,11 @@ def __deepcopy__(self, memo): class NonEmptyOrderedSet(OrderedSet[T]): - def __init__(self, iterable: Optional[List[T]] = None, use_tag: bool = True): + def __init__( + self, + iterable: Optional[Union[List[T], IndefiniteList]] = None, + use_tag: bool = True, + ): super().__init__(iterable, use_tag) def validate(self): From 8f23d5e4217ae1e3cddeb317bfb2141b0abbfd9c Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Fri, 1 Aug 2025 20:04:58 -0500 Subject: [PATCH 05/14] test: update test cases for script_data_hash to reflect changes --- test/pycardano/test_txbuilder.py | 4 ++-- test/pycardano/test_util.py | 35 ++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 31794480..0b2e9878 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -411,12 +411,12 @@ def test_tx_builder_mint_multi_asset(chain_context): [ sender_address.to_primitive(), [ - 5809155, + 5809111, {b"1111111111111111111111111111": {b"Token1": 1, b"Token2": 2}}, ], ], ], - 2: 190845, + 2: 190889, 3: 123456789, 8: 1000, 9: mint, diff --git a/test/pycardano/test_util.py b/test/pycardano/test_util.py index 17b4f647..008e7940 100644 --- a/test/pycardano/test_util.py +++ b/test/pycardano/test_util.py @@ -3,7 +3,17 @@ import pytest from pycardano.hash import SCRIPT_HASH_SIZE, ScriptDataHash -from pycardano.plutus import ExecutionUnits, PlutusData, Redeemer, RedeemerTag, Unit +from pycardano.plutus import ( + COST_MODELS, + ExecutionUnits, + PlutusData, + Redeemer, + RedeemerKey, + RedeemerMap, + RedeemerTag, + RedeemerValue, + Unit, +) from pycardano.transaction import Value from pycardano.utils import ( min_lovelace_pre_alonzo, @@ -156,14 +166,31 @@ def test_script_data_hash(): redeemers = [Redeemer(unit, ExecutionUnits(1000000, 1000000))] redeemers[0].tag = RedeemerTag.SPEND assert ScriptDataHash.from_primitive( - "032d812ee0731af78fe4ec67e4d30d16313c09e6fb675af28f825797e8b5621d" + "2ad155a692b0ddb6752df485de0a6bdb947757f9f998ff34a6f4b06ca0664fbe" ) == script_data_hash(redeemers=redeemers, datums=[unit]) +def test_script_data_hash_redeemer_map(): + unit = Unit() + redeemer = Redeemer(42, ExecutionUnits(573240, 253056459)) + redeemer.tag = RedeemerTag.SPEND + redeemers = RedeemerMap( + { + RedeemerKey(redeemer.tag, redeemer.index): RedeemerValue( + redeemer.data, redeemer.ex_units + ) + } + ) + cost_models = COST_MODELS + assert ScriptDataHash.from_primitive( + "04ad5eb241d1ede2bbbd60c5853de7659d2ecfb1a29d6cbb6921ef7bdd46ca3c" + ) == script_data_hash(redeemers=redeemers, datums=[unit], cost_models=cost_models) + + def test_script_data_hash_datum_only(): unit = Unit() assert ScriptDataHash.from_primitive( - "2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39" + "264ea21d9904cd72ce5038fa60e0ddd0859383f7fbf60ecec6df22e4c4e34a1f" ) == script_data_hash(redeemers=[], datums=[unit]) @@ -171,7 +198,7 @@ def test_script_data_hash_redeemer_only(): unit = Unit() redeemers = [] assert ScriptDataHash.from_primitive( - "a88fe2947b8d45d1f8b798e52174202579ecf847b8f17038c7398103df2d27b0" + "9eb0251b2e85b082c3706a3e79b4cf2a2e96f936e912a398591e2486c757f8c1" ) == script_data_hash(redeemers=redeemers, datums=[]) From 1de5302561d4004b4454488438a963131e846608 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 2 Aug 2025 13:17:39 -0500 Subject: [PATCH 06/14] fix: update redeemer handling to use RedeemerMap for empty cases --- pycardano/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pycardano/utils.py b/pycardano/utils.py index 2b529afe..fbec0761 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -12,7 +12,7 @@ from pycardano.backend.base import ChainContext from pycardano.hash import SCRIPT_DATA_HASH_SIZE, SCRIPT_HASH_SIZE, ScriptDataHash -from pycardano.plutus import COST_MODELS, CostModels, Datum, Redeemers +from pycardano.plutus import COST_MODELS, CostModels, Datum, Redeemers, RedeemerMap from pycardano.serialization import NonEmptyOrderedSet, default_encoder from pycardano.transaction import MultiAsset, TransactionOutput, Value @@ -249,14 +249,14 @@ def script_data_hash( Returns: ScriptDataHash: Plutus script data hash """ - # Handle empty redeemers case - should be encoded as an empty map (A0 in hex) + # Handle empty redeemers case - should be encoded as an empty RedeemerMap (A0 in hex) if not redeemers: - redeemer_bytes = cbor2.dumps({}, default=default_encoder) + redeemers = RedeemerMap() cost_models = {} - else: - redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) - if not cost_models: - cost_models = COST_MODELS + elif not cost_models: + cost_models = COST_MODELS + + redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) # Handle datums - if no datums, use empty bytestring if datums: From d328f96fd1629986d137c25081186614751e8794 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 2 Aug 2025 13:29:08 -0500 Subject: [PATCH 07/14] fix: update datum handling to correctly process NonEmptyOrderedSet or list --- pycardano/utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pycardano/utils.py b/pycardano/utils.py index fbec0761..1435cf9f 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -12,7 +12,7 @@ from pycardano.backend.base import ChainContext from pycardano.hash import SCRIPT_DATA_HASH_SIZE, SCRIPT_HASH_SIZE, ScriptDataHash -from pycardano.plutus import COST_MODELS, CostModels, Datum, Redeemers, RedeemerMap +from pycardano.plutus import COST_MODELS, CostModels, Datum, RedeemerMap, Redeemers from pycardano.serialization import NonEmptyOrderedSet, default_encoder from pycardano.transaction import MultiAsset, TransactionOutput, Value @@ -260,13 +260,13 @@ def script_data_hash( # Handle datums - if no datums, use empty bytestring if datums: - if isinstance(datums, list): - # If datums is a NonEmptyOrderedSet, convert it to a shallow primitive representation - # to ensure correct CBOR encoding - datums = NonEmptyOrderedSet(datums) - datum_bytes = cbor2.dumps( - datums.to_shallow_primitive(), default=default_encoder - ) + if not isinstance(datums, NonEmptyOrderedSet): + # If datums is not a NonEmptyOrderedSet, handle it as a list + datum_bytes = cbor2.dumps(datums, default=default_encoder) + else: + datum_bytes = cbor2.dumps( + datums.to_shallow_primitive(), default=default_encoder + ) else: datum_bytes = b"" From 6c87c320cff5cd3255de1f9574612267569f8afd Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 2 Aug 2025 13:31:47 -0500 Subject: [PATCH 08/14] fix: revert tests hash for lists and pass NonEmptyOrderedSet to script_data_hash in test_script_data_hash_redeemer_map --- test/pycardano/test_util.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/pycardano/test_util.py b/test/pycardano/test_util.py index 008e7940..a7d06f7c 100644 --- a/test/pycardano/test_util.py +++ b/test/pycardano/test_util.py @@ -2,6 +2,7 @@ import pytest +from pycardano import NonEmptyOrderedSet from pycardano.hash import SCRIPT_HASH_SIZE, ScriptDataHash from pycardano.plutus import ( COST_MODELS, @@ -166,7 +167,7 @@ def test_script_data_hash(): redeemers = [Redeemer(unit, ExecutionUnits(1000000, 1000000))] redeemers[0].tag = RedeemerTag.SPEND assert ScriptDataHash.from_primitive( - "2ad155a692b0ddb6752df485de0a6bdb947757f9f998ff34a6f4b06ca0664fbe" + "032d812ee0731af78fe4ec67e4d30d16313c09e6fb675af28f825797e8b5621d" ) == script_data_hash(redeemers=redeemers, datums=[unit]) @@ -184,13 +185,15 @@ def test_script_data_hash_redeemer_map(): cost_models = COST_MODELS assert ScriptDataHash.from_primitive( "04ad5eb241d1ede2bbbd60c5853de7659d2ecfb1a29d6cbb6921ef7bdd46ca3c" - ) == script_data_hash(redeemers=redeemers, datums=[unit], cost_models=cost_models) + ) == script_data_hash( + redeemers=redeemers, datums=NonEmptyOrderedSet([unit]), cost_models=cost_models + ) def test_script_data_hash_datum_only(): unit = Unit() assert ScriptDataHash.from_primitive( - "264ea21d9904cd72ce5038fa60e0ddd0859383f7fbf60ecec6df22e4c4e34a1f" + "244926529564c04ffdea89005076a6b6aac5e4a2f38182cd48bfbc734b3be296" ) == script_data_hash(redeemers=[], datums=[unit]) From 304375c7eaffdefa3a823a38e42b34315cdd8de3 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 2 Aug 2025 16:36:59 -0500 Subject: [PATCH 09/14] fix: update script_data_hash to pass in NonEmptyOrderedSet for datums --- pycardano/txbuilder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 7cd7c59d..d99fb20d 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -616,7 +616,9 @@ def script_data_hash(self) -> Optional[ScriptDataHash]: ) ) return script_data_hash( - self.redeemers(), list(self.datums.values()), CostModels(cost_models) + self.redeemers(), + NonEmptyOrderedSet(list(self.datums.values())), + CostModels(cost_models), ) else: return None From 7f6422cfc8588c8fe676acffdb368e6b6495d648 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sun, 3 Aug 2025 14:28:31 -0500 Subject: [PATCH 10/14] fix: improve redeemer handling for compatibility and remove canonical True on cost_models_bytes --- pycardano/utils.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/pycardano/utils.py b/pycardano/utils.py index 1435cf9f..9704c804 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -249,16 +249,16 @@ def script_data_hash( Returns: ScriptDataHash: Plutus script data hash """ - # Handle empty redeemers case - should be encoded as an empty RedeemerMap (A0 in hex) - if not redeemers: + if redeemers is None: redeemers = RedeemerMap() cost_models = {} + elif len(redeemers) == 0: + cost_models = {} elif not cost_models: cost_models = COST_MODELS redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) - # Handle datums - if no datums, use empty bytestring if datums: if not isinstance(datums, NonEmptyOrderedSet): # If datums is not a NonEmptyOrderedSet, handle it as a list @@ -270,19 +270,11 @@ def script_data_hash( else: datum_bytes = b"" - # Encode cost models - must use definite length encoding - cost_models_bytes = cbor2.dumps( - cost_models, - default=default_encoder, - canonical=True, # Ensures definite length encoding and canonical map keys - ) - - # Concatenate in order: redeemers || datums || language views - data = redeemer_bytes + datum_bytes + cost_models_bytes + cost_models_bytes = cbor2.dumps(cost_models, default=default_encoder) return ScriptDataHash( blake2b( - data, + redeemer_bytes + datum_bytes + cost_models_bytes, SCRIPT_DATA_HASH_SIZE, encoder=RawEncoder, ) From fb6cdeb574c9ad7a56c2052730907e12dee942c6 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sun, 3 Aug 2025 14:30:30 -0500 Subject: [PATCH 11/14] fix: revert expected hash values in script_data_hash tests --- test/pycardano/test_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/pycardano/test_util.py b/test/pycardano/test_util.py index a7d06f7c..8baa6741 100644 --- a/test/pycardano/test_util.py +++ b/test/pycardano/test_util.py @@ -193,7 +193,7 @@ def test_script_data_hash_redeemer_map(): def test_script_data_hash_datum_only(): unit = Unit() assert ScriptDataHash.from_primitive( - "244926529564c04ffdea89005076a6b6aac5e4a2f38182cd48bfbc734b3be296" + "2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39" ) == script_data_hash(redeemers=[], datums=[unit]) @@ -201,7 +201,7 @@ def test_script_data_hash_redeemer_only(): unit = Unit() redeemers = [] assert ScriptDataHash.from_primitive( - "9eb0251b2e85b082c3706a3e79b4cf2a2e96f936e912a398591e2486c757f8c1" + "a88fe2947b8d45d1f8b798e52174202579ecf847b8f17038c7398103df2d27b0" ) == script_data_hash(redeemers=redeemers, datums=[]) From d63d31a24e8183d971588c949991067c07ea34f5 Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 7 Aug 2025 22:08:10 -0700 Subject: [PATCH 12/14] Fix OrderedSet serialization --- pycardano/serialization.py | 36 +++++++++++++++++++++------- pycardano/utils.py | 8 +------ test/pycardano/test_serialization.py | 23 +++++++++++++++--- test/pycardano/test_txbuilder.py | 4 ++-- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index b45df904..bbae9da2 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -24,7 +24,6 @@ List, Optional, Sequence, - Set, Type, TypeVar, Union, @@ -160,6 +159,7 @@ class RawCBOR: Fraction, FrozenList, IndefiniteFrozenList, + ByteString, ) """ A list of types that could be encoded by @@ -1128,14 +1128,15 @@ def list_hook( return lambda vals: [cls.from_primitive(v) for v in vals] -class OrderedSet(list, IndefiniteList, Generic[T], CBORSerializable): # type: ignore +class OrderedSet(Generic[T], CBORSerializable): # type: ignore def __init__( self, iterable: Optional[Union[List[T], IndefiniteList]] = None, use_tag: bool = True, ): super().__init__() - self._set: Set[str] = set() + self._dict: Dict[bytes, int] = {} + self._list: List[T] = [] self._use_tag = use_tag self._is_indefinite_list = False if iterable: @@ -1143,18 +1144,37 @@ def __init__( self.extend(iterable) def append(self, item: T) -> None: - item_key = str(item) - if item_key not in self._set: - super().append(item) - self._set.add(item_key) + if item in self: + return + self._list.append(item) + self._dict[dumps(item, default=default_encoder)] = len(self._list) - 1 def extend(self, items: Iterable[T]) -> None: self._is_indefinite_list = isinstance(items, IndefiniteList) for item in items: self.append(item) + def remove(self, item: T) -> None: + if item not in self: + return + index = self._dict.pop(dumps(item, default=default_encoder)) + self._list.pop(index) + # Update the indices in the dictionary + for key, idx in self._dict.items(): + if idx > index: + self._dict[key] = idx - 1 + def __contains__(self, item: object) -> bool: - return str(item) in self._set + return dumps(item, default=default_encoder) in self._dict + + def __iter__(self): + return iter(self._list) + + def __getitem__(self, index: int) -> T: + return self._list[index] + + def __len__(self) -> int: + return len(self._list) def __eq__(self, other: object) -> bool: if not isinstance(other, OrderedSet): diff --git a/pycardano/utils.py b/pycardano/utils.py index 9704c804..00c5f6a4 100644 --- a/pycardano/utils.py +++ b/pycardano/utils.py @@ -260,13 +260,7 @@ def script_data_hash( redeemer_bytes = cbor2.dumps(redeemers, default=default_encoder) if datums: - if not isinstance(datums, NonEmptyOrderedSet): - # If datums is not a NonEmptyOrderedSet, handle it as a list - datum_bytes = cbor2.dumps(datums, default=default_encoder) - else: - datum_bytes = cbor2.dumps( - datums.to_shallow_primitive(), default=default_encoder - ) + datum_bytes = cbor2.dumps(datums, default=default_encoder) else: datum_bytes = b"" diff --git a/test/pycardano/test_serialization.py b/test/pycardano/test_serialization.py index c75503c5..5cbf5df2 100644 --- a/test/pycardano/test_serialization.py +++ b/test/pycardano/test_serialization.py @@ -638,6 +638,23 @@ def test_ordered_set(): assert list(s) == [1, 2, 3] assert s._use_tag + # Test remove + s = OrderedSet([1, 2, 3, 4]) + s.remove(2) + assert list(s) == [1, 3, 4] + assert 2 not in s + assert 1 in s + assert 3 in s + assert 4 in s + s.remove(2) + assert list(s) == [1, 3, 4] + assert 2 not in s + s.remove(3) + assert list(s) == [1, 4] + assert 3 not in s + s.remove(4) + assert list(s) == [1] + def test_ordered_set_with_complex_types(): # Test with VerificationKeyWitness @@ -909,9 +926,9 @@ class MyOrderedSet(OrderedSet): assert 4 not in s # Test with complex objects - class TestObj: - def __init__(self, value): - self.value = value + @dataclass(repr=False) + class TestObj(ArrayCBORSerializable): + value: str def __str__(self): return f"TestObj({self.value})" diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 0b2e9878..31794480 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -411,12 +411,12 @@ def test_tx_builder_mint_multi_asset(chain_context): [ sender_address.to_primitive(), [ - 5809111, + 5809155, {b"1111111111111111111111111111": {b"Token1": 1, b"Token2": 2}}, ], ], ], - 2: 190889, + 2: 190845, 3: 123456789, 8: 1000, 9: mint, From c2c8b39146410cc0188ce8338ebe7f855e5b69f2 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 8 Aug 2025 08:14:19 -0700 Subject: [PATCH 13/14] Remove unnecessary type ignore --- pycardano/serialization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index bbae9da2..a82e5455 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -1128,7 +1128,7 @@ def list_hook( return lambda vals: [cls.from_primitive(v) for v in vals] -class OrderedSet(Generic[T], CBORSerializable): # type: ignore +class OrderedSet(Generic[T], CBORSerializable): def __init__( self, iterable: Optional[Union[List[T], IndefiniteList]] = None, From 65eedc8265196942712d5ae2326ee1b51399cea7 Mon Sep 17 00:00:00 2001 From: Hareem Adderley Date: Sat, 9 Aug 2025 14:13:22 -0500 Subject: [PATCH 14/14] fix: update plutus_data type to include IndefiniteList for improved compatibility --- pycardano/witness.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pycardano/witness.py b/pycardano/witness.py index 3bac7bf0..54a94ce7 100644 --- a/pycardano/witness.py +++ b/pycardano/witness.py @@ -12,6 +12,7 @@ from pycardano.plutus import PlutusV1Script, PlutusV2Script, PlutusV3Script, Redeemers from pycardano.serialization import ( ArrayCBORSerializable, + IndefiniteList, MapCBORSerializable, NonEmptyOrderedSet, limit_primitive_type, @@ -107,9 +108,11 @@ class TransactionWitnessSet(MapCBORSerializable): }, ) - plutus_data: Optional[Union[List[Any], NonEmptyOrderedSet[Any]]] = field( - default=None, - metadata={"optional": True, "key": 4}, + plutus_data: Optional[Union[IndefiniteList, List[Any], NonEmptyOrderedSet[Any]]] = ( + field( + default=None, + metadata={"optional": True, "key": 4}, + ) ) redeemer: Optional[Redeemers] = field(