From 93288214813c8b82ba0a95d1baed3983dcdb49e6 Mon Sep 17 00:00:00 2001 From: Sonoko Mizuki Date: Tue, 17 Sep 2024 13:44:08 +0900 Subject: [PATCH 1/3] Add PlutusV3Script class --- pycardano/plutus.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index b1ee6e81..42e3c711 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -41,6 +41,7 @@ "ExecutionUnits", "PlutusV1Script", "PlutusV2Script", + "PlutusV3Script", "RawPlutusData", "Redeemer", "ScriptType", @@ -993,12 +994,12 @@ def from_primitive(cls: Type[Redeemer], values: list) -> Redeemer: def plutus_script_hash( - script: Union[bytes, PlutusV1Script, PlutusV2Script] + script: Union[bytes, PlutusV1Script, PlutusV2Script, PlutusV3Script] ) -> ScriptHash: """Calculates the hash of a Plutus script. Args: - script (Union[bytes, PlutusV1Script, PlutusV2Script]): A plutus script. + script (Union[bytes, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. Returns: ScriptHash: blake2b hash of the script. @@ -1013,8 +1014,11 @@ class PlutusV1Script(bytes): class PlutusV2Script(bytes): pass +class PlutusV3Script(bytes): + pass + -ScriptType = Union[bytes, NativeScript, PlutusV1Script, PlutusV2Script] +ScriptType = Union[bytes, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] """Script type. A Union type that contains all valid script types.""" @@ -1037,6 +1041,10 @@ def script_hash(script: ScriptType) -> ScriptHash: return ScriptHash( blake2b(bytes.fromhex("02") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) ) + elif isinstance(script, PlutusV3Script): + return ScriptHash( + blake2b(bytes.fromhex("03") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) + ) else: raise TypeError(f"Unexpected script type: {type(script)}") From e451385ff0abef56d936a4c52a2d692a69df9757 Mon Sep 17 00:00:00 2001 From: Sonoko Mizuki Date: Tue, 17 Sep 2024 13:44:20 +0900 Subject: [PATCH 2/3] Support PlutusV3Script --- pycardano/backend/blockfrost.py | 13 +++++++++---- pycardano/backend/cardano_cli.py | 9 +++++++-- pycardano/backend/ogmios.py | 19 ++++++++++++------- pycardano/serialization.py | 2 +- pycardano/transaction.py | 17 ++++++++++------- pycardano/txbuilder.py | 26 ++++++++++++++++++-------- pycardano/witness.py | 9 ++++++++- 7 files changed, 65 insertions(+), 30 deletions(-) diff --git a/pycardano/backend/blockfrost.py b/pycardano/backend/blockfrost.py index 40c9ccc7..e053afc2 100644 --- a/pycardano/backend/blockfrost.py +++ b/pycardano/backend/blockfrost.py @@ -20,7 +20,7 @@ from pycardano.hash import SCRIPT_HASH_SIZE, DatumHash, ScriptHash from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script, script_hash +from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script, PlutusV3Script, script_hash from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, @@ -37,8 +37,8 @@ def _try_fix_script( - scripth: str, script: Union[PlutusV1Script, PlutusV2Script] -) -> Union[PlutusV1Script, PlutusV2Script]: + scripth: str, script: Union[PlutusV1Script, PlutusV2Script, PlutusV3Script] +) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script]: if str(script_hash(script)) == scripth: return script else: @@ -170,7 +170,7 @@ def protocol_param(self) -> ProtocolParameters: def _get_script( self, script_hash: str - ) -> Union[PlutusV1Script, PlutusV2Script, NativeScript]: + ) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script, NativeScript]: script_type = self.api.script(script_hash).type if script_type == "plutusV1": v1script = PlutusV1Script( @@ -182,6 +182,11 @@ def _get_script( bytes.fromhex(self.api.script_cbor(script_hash).cbor) ) return _try_fix_script(script_hash, v2script) + elif script_type == "plutusV3": + v3script = PlutusV3Script( + bytes.fromhex(self.api.script_cbor(script_hash).cbor) + ) + return _try_fix_script(script_hash, v3script) else: script_json: JsonDict = self.api.script_json( script_hash, return_type="json" diff --git a/pycardano/backend/cardano_cli.py b/pycardano/backend/cardano_cli.py index 5c0072ea..457d4b39 100644 --- a/pycardano/backend/cardano_cli.py +++ b/pycardano/backend/cardano_cli.py @@ -32,7 +32,7 @@ from pycardano.hash import DatumHash, ScriptHash from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.plutus import Datum, PlutusV1Script, PlutusV2Script, RawPlutusData +from pycardano.plutus import Datum, PlutusV1Script, PlutusV2Script, PlutusV3Script, RawPlutusData from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, @@ -384,7 +384,7 @@ def version(self): @staticmethod def _get_script( reference_script: dict, - ) -> Union[PlutusV1Script, PlutusV2Script, NativeScript]: + ) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script, NativeScript]: """ Get a script object from a reference script dictionary. Args: @@ -405,6 +405,11 @@ def _get_script( cbor2.loads(bytes.fromhex(script_json["cborHex"])) ) return v2script + elif script_type == "PlutusScriptV3": + v3script = PlutusV3Script( + cbor2.loads(bytes.fromhex(script_json["cborHex"])) + ) + return v3script else: return NativeScript.from_dict(script_json) diff --git a/pycardano/backend/ogmios.py b/pycardano/backend/ogmios.py index 63ffaf14..f29f0537 100644 --- a/pycardano/backend/ogmios.py +++ b/pycardano/backend/ogmios.py @@ -20,7 +20,7 @@ from pycardano.exception import TransactionFailedException from pycardano.hash import DatumHash, ScriptHash from pycardano.network import Network -from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script +from pycardano.plutus import ExecutionUnits, PlutusV1Script, PlutusV2Script, PlutusV3Script from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, @@ -342,11 +342,14 @@ def _utxos_kupo(self, address: str) -> List[UTxO]: if script_hash: kupo_script_url = self._kupo_url + "/scripts/" + script_hash script = requests.get(kupo_script_url).json() - if script["language"] == "plutus:v2": + if script["language"] == "plutus:v1": + script = PlutusV1Script(bytes.fromhex(script["script"])) + script = _try_fix_script(script_hash, script) + elif script["language"] == "plutus:v2": script = PlutusV2Script(bytes.fromhex(script["script"])) script = _try_fix_script(script_hash, script) - elif script["language"] == "plutus:v1": - script = PlutusV1Script(bytes.fromhex(script["script"])) + elif script["language"] == "plutus:v3": + script = PlutusV3Script(bytes.fromhex(script["script"])) script = _try_fix_script(script_hash, script) else: raise ValueError("Unknown plutus script type") @@ -462,10 +465,12 @@ def _utxo_from_ogmios_result(self, result) -> UTxO: lovelace_amount = output["value"]["coins"] script = output.get("script", None) if script: - if "plutus:v2" in script: - script = PlutusV2Script(bytes.fromhex(script["plutus:v2"])) - elif "plutus:v1" in script: + if "plutus:v1" in script: script = PlutusV1Script(bytes.fromhex(script["plutus:v1"])) + elif "plutus:v2" in script: + script = PlutusV2Script(bytes.fromhex(script["plutus:v2"])) + elif "plutus:v3" in script: + script = PlutusV3Script(bytes.fromhex(script["plutus:v3"])) else: raise ValueError("Unknown plutus script type") datum_hash = ( diff --git a/pycardano/serialization.py b/pycardano/serialization.py index 110e2dcf..63cadca9 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -536,7 +536,7 @@ def _restore_typed_primitive( if not isinstance(v, bytes): raise DeserializeException(f"Expected type bytes but got {type(v)}") return ByteString(v) - elif isclass(t) and t.__name__ in ["PlutusV1Script", "PlutusV2Script"]: + elif isclass(t) and t.__name__ in ["PlutusV1Script", "PlutusV2Script", "PlutusV3Script"]: if not isinstance(v, bytes): raise DeserializeException(f"Expected type bytes but got {type(v)}") return t(v) diff --git a/pycardano/transaction.py b/pycardano/transaction.py index 79b7ce01..292b5ba1 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -28,7 +28,7 @@ from pycardano.metadata import AuxiliaryData from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.plutus import Datum, PlutusV1Script, PlutusV2Script, RawPlutusData +from pycardano.plutus import Datum, PlutusV1Script, PlutusV2Script, PlutusV3Script, RawPlutusData from pycardano.serialization import ( ArrayCBORSerializable, CBORSerializable, @@ -259,15 +259,17 @@ def to_shallow_primitive(self): class _Script(ArrayCBORSerializable): _TYPE: int = field(init=False, default=0) - script: Union[NativeScript, PlutusV1Script, PlutusV2Script] + script: Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] def __post_init__(self): if isinstance(self.script, NativeScript): self._TYPE = 0 elif isinstance(self.script, PlutusV1Script): self._TYPE = 1 - else: + elif isinstance(self.script, PlutusV2Script): self._TYPE = 2 + else: + self._TYPE = 3 @classmethod def from_primitive(cls: Type[_Script], values: List[Primitive]) -> _Script: @@ -276,9 +278,10 @@ def from_primitive(cls: Type[_Script], values: List[Primitive]) -> _Script: assert isinstance(values[1], bytes) if values[0] == 1: return cls(PlutusV1Script(values[1])) - else: + elif values[0] == 2: return cls(PlutusV2Script(values[1])) - + else: + return cls(PlutusV3Script(values[1])) @dataclass(repr=False) class _DatumOption(ArrayCBORSerializable): @@ -344,7 +347,7 @@ class _TransactionOutputPostAlonzo(MapCBORSerializable): ) @property - def script(self) -> Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script]]: + def script(self) -> Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]]: if self.script_ref: return self.script_ref.script.script else: @@ -370,7 +373,7 @@ class TransactionOutput(CBORSerializable): datum: Optional[Datum] = None - script: Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script]] = None + script: Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]] = None post_alonzo: Optional[bool] = False diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index 63ea93cc..fe12501c 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -40,6 +40,7 @@ ExecutionUnits, PlutusV1Script, PlutusV2Script, + PlutusV3Script, Redeemer, RedeemerTag, ScriptType, @@ -155,7 +156,7 @@ class TransactionBuilder: init=False, default_factory=lambda: {} ) - _reference_scripts: List[Union[NativeScript, PlutusV1Script, PlutusV2Script]] = ( + _reference_scripts: List[Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]] = ( field(init=False, default_factory=lambda: []) ) @@ -203,7 +204,7 @@ def add_script_input( self, utxo: UTxO, script: Optional[ - Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script] + Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] ] = None, datum: Optional[Datum] = None, redeemer: Optional[Redeemer] = None, @@ -212,7 +213,7 @@ def add_script_input( Args: utxo (UTxO): Script UTxO to be added. - script (Optional[Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script]]): A plutus script. + script (Optional[Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]]): A plutus script. If not provided, the script will be inferred from the input UTxO (first arg of this method). The script can also be a specific UTxO whose output contains an inline script. datum (Optional[Datum]): A plutus datum to unlock the UTxO. @@ -263,7 +264,7 @@ def add_script_input( # collect potential scripts to fulfill the input candidate_scripts: List[ - Tuple[Union[NativeScript, PlutusV1Script, PlutusV2Script], Optional[UTxO]] + Tuple[Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script], Optional[UTxO]] ] = [] if utxo.output.script: candidate_scripts.append((utxo.output.script, utxo)) @@ -301,13 +302,13 @@ def add_script_input( def add_minting_script( self, - script: Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script], + script: Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script], redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: """Add a minting script along with its datum and redeemer to this transaction. Args: - script (Union[UTxO, PlutusV1Script, PlutusV2Script]): A plutus script. + script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. Returns: @@ -333,13 +334,13 @@ def add_minting_script( def add_withdrawal_script( self, - script: Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script], + script: Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script], redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: """Add a withdrawal script along with its redeemer to this transaction. Args: - script (Union[UTxO, PlutusV1Script, PlutusV2Script]): A plutus script. + script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. Returns: @@ -495,6 +496,11 @@ def script_data_hash(self) -> Optional[ScriptDataHash]: self.context.protocol_param.cost_models.get("PlutusV2") or PLUTUS_V2_COST_MODEL ) + if isinstance(s, PlutusV3Script): + cost_models[1] = ( + self.context.protocol_param.cost_models.get("PlutusV3") + or PLUTUS_V2_COST_MODEL + ) return script_data_hash( self.redeemers, list(self.datums.values()), CostModels(cost_models) ) @@ -941,6 +947,7 @@ def build_witness_set(self) -> TransactionWitnessSet: native_scripts: List[NativeScript] = [] plutus_v1_scripts: List[PlutusV1Script] = [] plutus_v2_scripts: List[PlutusV2Script] = [] + plutus_v3_scripts: List[PlutusV3Script] = [] for script in self.scripts: if isinstance(script, NativeScript): @@ -951,6 +958,8 @@ def build_witness_set(self) -> TransactionWitnessSet: plutus_v1_scripts.append(PlutusV1Script(script)) elif isinstance(script, PlutusV2Script): plutus_v2_scripts.append(script) + elif isinstance(script, PlutusV3Script): + plutus_v3_scripts.append(script) else: raise InvalidArgumentException( f"Unsupported script type: {type(script)}" @@ -960,6 +969,7 @@ def build_witness_set(self) -> TransactionWitnessSet: native_scripts=native_scripts if native_scripts else None, plutus_v1_script=plutus_v1_scripts if plutus_v1_scripts else None, 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.redeemers else None, plutus_data=list(self.datums.values()) if self.datums else None, ) diff --git a/pycardano/witness.py b/pycardano/witness.py index 9a62923d..01d00834 100644 --- a/pycardano/witness.py +++ b/pycardano/witness.py @@ -7,7 +7,7 @@ from pycardano.key import ExtendedVerificationKey, VerificationKey from pycardano.nativescript import NativeScript -from pycardano.plutus import PlutusV1Script, PlutusV2Script, RawPlutusData, Redeemer +from pycardano.plutus import PlutusV1Script, PlutusV2Script, PlutusV3Script, RawPlutusData, Redeemer from pycardano.serialization import ( ArrayCBORSerializable, MapCBORSerializable, @@ -69,6 +69,10 @@ class TransactionWitnessSet(MapCBORSerializable): default=None, metadata={"optional": True, "key": 6} ) + plutus_v3_script: Optional[List[PlutusV3Script]] = field( + default=None, metadata={"optional": True, "key": 7} + ) + plutus_data: Optional[List[Any]] = field( default=None, metadata={"optional": True, "key": 4, "object_hook": list_hook(RawPlutusData)}, @@ -104,6 +108,9 @@ def _get_plutus_v1_scripts(data: Any): def _get_plutus_v2_scripts(data: Any): return [PlutusV2Script(script) for script in data] if data else None + def _get_plutus_v3_scripts(data: Any): + return [PlutusV3Script(script) for script in data] if data else None + def _get_redeemers(data: Any): return ( [Redeemer.from_primitive(redeemer) for redeemer in data] From 5208975aa75952bcb0c6c5974881913268401543 Mon Sep 17 00:00:00 2001 From: Sonoko Mizuki Date: Tue, 17 Sep 2024 13:44:34 +0900 Subject: [PATCH 3/3] Update test codes --- test/pycardano/test_serialization.py | 4 +++- test/pycardano/test_transaction.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/pycardano/test_serialization.py b/test/pycardano/test_serialization.py index 890eb16a..c8a7dd27 100644 --- a/test/pycardano/test_serialization.py +++ b/test/pycardano/test_serialization.py @@ -8,7 +8,7 @@ import pycardano from pycardano import Datum, RawPlutusData from pycardano.exception import DeserializeException -from pycardano.plutus import PlutusV1Script, PlutusV2Script +from pycardano.plutus import PlutusV1Script, PlutusV2Script, PlutusV3Script from pycardano.serialization import ( ArrayCBORSerializable, CBORSerializable, @@ -295,10 +295,12 @@ def test_script_deserialize(): class Test(MapCBORSerializable): script_1: PlutusV1Script script_2: PlutusV2Script + script_3: PlutusV3Script datum = Test( script_1=PlutusV1Script(b"dummy test script"), script_2=PlutusV2Script(b"dummy test script"), + script_3=PlutusV3Script(b"dummy test script"), ) assert datum == datum.from_cbor(datum.to_cbor()) diff --git a/test/pycardano/test_transaction.py b/test/pycardano/test_transaction.py index e7cc0c33..f3c311f1 100644 --- a/test/pycardano/test_transaction.py +++ b/test/pycardano/test_transaction.py @@ -9,7 +9,7 @@ from pycardano.hash import SCRIPT_HASH_SIZE, ScriptHash, TransactionId from pycardano.key import PaymentKeyPair, PaymentSigningKey, VerificationKey from pycardano.nativescript import ScriptPubkey -from pycardano.plutus import PlutusData, PlutusV1Script, PlutusV2Script, datum_hash +from pycardano.plutus import PlutusData, PlutusV1Script, PlutusV2Script, PlutusV3Script, datum_hash from pycardano.transaction import ( Asset, AssetName,