diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 955d49eb..9a41be30 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,8 +35,8 @@ jobs: if: ${{ matrix.python-version == '3.11' }} uses: codecov/codecov-action@v4 with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN || '' }} - name: Run static analyses if: ${{ matrix.python-version == '3.11' }} run: | @@ -72,8 +72,8 @@ jobs: if: ${{ matrix.python-version == '3.11' }} uses: codecov/codecov-action@v4 with: - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN || '' }} - name: Dump docker logs if: failure() diff --git a/pycardano/backend/blockfrost.py b/pycardano/backend/blockfrost.py index f5a906ea..cbd81285 100644 --- a/pycardano/backend/blockfrost.py +++ b/pycardano/backend/blockfrost.py @@ -20,13 +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, - PlutusV3Script, - script_hash, -) +from pycardano.plutus import ExecutionUnits, PlutusScript, ScriptType, script_hash from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, @@ -42,9 +36,7 @@ __all__ = ["BlockFrostChainContext"] -def _try_fix_script( - scripth: str, script: Union[PlutusV1Script, PlutusV2Script, PlutusV3Script] -) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script]: +def _try_fix_script(scripth: str, script: PlutusScript) -> PlutusScript: if str(script_hash(script)) == scripth: return script else: @@ -180,25 +172,13 @@ def protocol_param(self) -> ProtocolParameters: ) return self._protocol_param - def _get_script( - self, script_hash: str - ) -> Union[PlutusV1Script, PlutusV2Script, PlutusV3Script, NativeScript]: + def _get_script(self, script_hash: str) -> ScriptType: script_type = self.api.script(script_hash).type - if script_type == "plutusV1": - v1script = PlutusV1Script( - bytes.fromhex(self.api.script_cbor(script_hash).cbor) - ) - return _try_fix_script(script_hash, v1script) - elif script_type == "plutusV2": - v2script = PlutusV2Script( - 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) + if script_type.startsWith("plutusV"): + ps = PlutusScript.from_version( + script_type[-1], bytes.fromhex(self.api.script_cbor(script_hash).cbor) ) - return _try_fix_script(script_hash, v3script) + return _try_fix_script(script_hash, ps) else: script_json: JsonDict = self.api.script_json( script_hash, return_type="json" diff --git a/pycardano/backend/kupo.py b/pycardano/backend/kupo.py index 36fa409e..9e7fb9a5 100644 --- a/pycardano/backend/kupo.py +++ b/pycardano/backend/kupo.py @@ -8,12 +8,7 @@ from pycardano.backend.blockfrost import _try_fix_script from pycardano.hash import DatumHash, ScriptHash from pycardano.network import Network -from pycardano.plutus import ( - ExecutionUnits, - PlutusV1Script, - PlutusV2Script, - PlutusV3Script, -) +from pycardano.plutus import ExecutionUnits, PlutusScript from pycardano.serialization import RawCBOR from pycardano.transaction import ( Asset, @@ -177,14 +172,11 @@ 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:v3": - script = PlutusV3Script(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"])) + ver = int(script["language"].removeprefix("plutus:v")) + if 1 <= ver <= 3: + script = PlutusScript.from_version( + ver, bytes.fromhex(script["script"]) + ) script = _try_fix_script(script_hash, script) else: raise ValueError("Unknown plutus script type") diff --git a/pycardano/backend/ogmios_v6.py b/pycardano/backend/ogmios_v6.py index 304cf24f..dffae96d 100644 --- a/pycardano/backend/ogmios_v6.py +++ b/pycardano/backend/ogmios_v6.py @@ -21,9 +21,7 @@ PLUTUS_V1_COST_MODEL, PLUTUS_V2_COST_MODEL, ExecutionUnits, - PlutusV1Script, - PlutusV2Script, - PlutusV3Script, + PlutusScript, ) from pycardano.serialization import RawCBOR from pycardano.transaction import ( @@ -261,12 +259,11 @@ def _utxo_from_ogmios_result(self, utxo: OgmiosUtxo) -> UTxO: script = utxo.script if script: # TODO: Need to test with native scripts - if script["language"] == "plutus:v3": - script = PlutusV3Script(bytes.fromhex(script["cbor"])) - elif script["language"] == "plutus:v2": - script = PlutusV2Script(bytes.fromhex(script["cbor"])) - elif script["language"] == "plutus:v1": - script = PlutusV1Script(bytes.fromhex(script["cbor"])) + if script["language"].startswith("plutus:v"): + script = PlutusScript.from_version( + int(script["language"].removeprefix("plutus:v")), + bytes.fromhex(script["cbor"]), + ) else: raise ValueError("Unknown plutus script type") datum_hash = ( diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 1f32f713..ea9b172e 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -40,6 +40,7 @@ "Datum", "RedeemerTag", "ExecutionUnits", + "PlutusScript", "PlutusV1Script", "PlutusV2Script", "PlutusV3Script", @@ -1049,13 +1050,11 @@ class RedeemerMap(DictCBORSerializable): Redeemers = Union[List[Redeemer], RedeemerMap] -def plutus_script_hash( - script: Union[bytes, PlutusV1Script, PlutusV2Script] -) -> ScriptHash: +def plutus_script_hash(script: Union[NativeScript, PlutusScript]) -> ScriptHash: """Calculates the hash of a Plutus script. Args: - script (Union[bytes, PlutusV1Script, PlutusV2Script]): A plutus script. + script (Union[bytes, PlutusScript]): A plutus script. Returns: ScriptHash: blake2b hash of the script. @@ -1063,19 +1062,54 @@ def plutus_script_hash( return script_hash(script) -class PlutusV1Script(bytes): - pass +class PlutusScript(bytes): + @property + def version(self) -> int: + raise NotImplementedError("") + + @classmethod + def from_version(cls, version: int, script_data: bytes) -> "PlutusScript": + if version == 1: + return PlutusV1Script(script_data) + elif version == 2: + return PlutusV2Script(script_data) + elif version == 3: + return PlutusV3Script(script_data) + else: + raise ValueError(f"No Plutus script class found for version {version}") + + def get_script_hash_prefix(self) -> bytes: + raise NotImplementedError("") + + +class PlutusV1Script(PlutusScript): + def get_script_hash_prefix(self) -> bytes: + return bytes.fromhex("01") + + @property + def version(self) -> int: + return 1 -class PlutusV2Script(bytes): - pass +class PlutusV2Script(PlutusScript): + def get_script_hash_prefix(self) -> bytes: + return bytes.fromhex("02") + @property + def version(self) -> int: + return 2 -class PlutusV3Script(bytes): - pass +class PlutusV3Script(PlutusScript): + def get_script_hash_prefix(self) -> bytes: + return bytes.fromhex("03") -ScriptType = Union[bytes, NativeScript, PlutusV1Script, PlutusV2Script] + @property + def version(self) -> int: + return 3 + + +ScriptType = Union[NativeScript, PlutusScript] """Script type. A Union type that contains all valid script types.""" @@ -1090,17 +1124,17 @@ def script_hash(script: ScriptType) -> ScriptHash: """ if isinstance(script, NativeScript): return script.hash() - elif isinstance(script, PlutusV1Script) or type(script) is bytes: + elif isinstance(script, PlutusScript): return ScriptHash( - blake2b(bytes.fromhex("01") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) - ) - elif isinstance(script, PlutusV2Script): - return ScriptHash( - blake2b(bytes.fromhex("02") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) + blake2b( + script.get_script_hash_prefix() + script, + SCRIPT_HASH_SIZE, + encoder=RawEncoder, + ) ) - elif isinstance(script, PlutusV3Script): + elif type(script) is bytes: return ScriptHash( - blake2b(bytes.fromhex("03") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) + blake2b(bytes.fromhex("01") + script, SCRIPT_HASH_SIZE, encoder=RawEncoder) ) else: raise TypeError(f"Unexpected script type: {type(script)}") diff --git a/pycardano/transaction.py b/pycardano/transaction.py index 5d5c7c0e..2c4d2909 100644 --- a/pycardano/transaction.py +++ b/pycardano/transaction.py @@ -28,13 +28,7 @@ from pycardano.metadata import AuxiliaryData from pycardano.nativescript import NativeScript from pycardano.network import Network -from pycardano.plutus import ( - Datum, - PlutusV1Script, - PlutusV2Script, - PlutusV3Script, - RawPlutusData, -) +from pycardano.plutus import Datum, PlutusScript, RawPlutusData from pycardano.serialization import ( ArrayCBORSerializable, CBORSerializable, @@ -310,29 +304,21 @@ def to_shallow_primitive(self): class _Script(ArrayCBORSerializable): _TYPE: int = field(init=False, default=0) - script: Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] + script: Union[NativeScript, PlutusScript] def __post_init__(self): if isinstance(self.script, NativeScript): self._TYPE = 0 - elif isinstance(self.script, PlutusV1Script): - self._TYPE = 1 - elif isinstance(self.script, PlutusV2Script): - self._TYPE = 2 - else: - self._TYPE = 3 + elif isinstance(self.script, PlutusScript): + self._TYPE = self.script.version @classmethod def from_primitive(cls: Type[_Script], values: List[Primitive]) -> _Script: if values[0] == 0: return cls(NativeScript.from_primitive(values[1])) assert isinstance(values[1], bytes) - if values[0] == 1: - return cls(PlutusV1Script(values[1])) - elif values[0] == 2: - return cls(PlutusV2Script(values[1])) - else: - return cls(PlutusV3Script(values[1])) + assert isinstance(values[0], int) + return cls(PlutusScript.from_version(values[0], values[1])) @dataclass(repr=False) @@ -401,7 +387,7 @@ class _TransactionOutputPostAlonzo(MapCBORSerializable): @property def script( self, - ) -> Optional[Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]]: + ) -> Optional[Union[NativeScript, PlutusScript]]: if self.script_ref: return self.script_ref.script.script else: @@ -427,9 +413,7 @@ class TransactionOutput(CBORSerializable): datum: Optional[Datum] = None - script: Optional[ - Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] - ] = None + script: Optional[Union[NativeScript, PlutusScript]] = None post_alonzo: Optional[bool] = False diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index f1f2ff50..56f9c6d3 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -34,11 +34,10 @@ from pycardano.metadata import AuxiliaryData from pycardano.nativescript import NativeScript, ScriptAll, ScriptAny, ScriptPubkey from pycardano.plutus import ( - PLUTUS_V1_COST_MODEL, - PLUTUS_V2_COST_MODEL, CostModels, Datum, ExecutionUnits, + PlutusScript, PlutusV1Script, PlutusV2Script, PlutusV3Script, @@ -167,9 +166,9 @@ class TransactionBuilder: init=False, default_factory=lambda: {} ) - _reference_scripts: List[ - Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] - ] = field(init=False, default_factory=lambda: []) + _reference_scripts: List[Union[NativeScript, PlutusScript]] = field( + init=False, default_factory=lambda: [] + ) _should_estimate_execution_units: Optional[bool] = field(init=False, default=None) @@ -214,9 +213,7 @@ def _consolidate_redeemer(self, redeemer): def add_script_input( self, utxo: UTxO, - script: Optional[ - Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script] - ] = None, + script: Optional[Union[UTxO, NativeScript, PlutusScript]] = None, datum: Optional[Datum] = None, redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: @@ -224,7 +221,7 @@ def add_script_input( Args: utxo (UTxO): Script UTxO to be added. - script (Optional[Union[UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script]]): + script (Optional[Union[UTxO, NativeScript, PlutusScript]]): 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. @@ -277,7 +274,7 @@ def add_script_input( # collect potential scripts to fulfill the input candidate_scripts: List[ Tuple[ - Union[NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script], + Union[NativeScript, PlutusScript], Optional[UTxO], ] ] = [] @@ -320,15 +317,13 @@ def add_script_input( def add_minting_script( self, - script: Union[ - UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script - ], + script: Union[UTxO, NativeScript, PlutusScript], redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: """Add a minting script along with its datum and redeemer to this transaction. Args: - script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. + script (Union[UTxO, PlutusScript): A plutus script. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. Returns: @@ -354,15 +349,13 @@ def add_minting_script( def add_withdrawal_script( self, - script: Union[ - UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script - ], + script: Union[UTxO, NativeScript, PlutusScript], redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: """Add a withdrawal script along with its redeemer to this transaction. Args: - script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. + script (Union[UTxO, PlutusScript]): A plutus script. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. Returns: @@ -390,9 +383,7 @@ def add_withdrawal_script( def add_certificate_script( self, - script: Union[ - UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script - ], + script: Union[UTxO, NativeScript, PlutusScript], redeemer: Optional[Redeemer] = None, ) -> TransactionBuilder: """Add a certificate script along with its redeemer to this transaction. @@ -400,7 +391,7 @@ def add_certificate_script( The index of the redeemer will be set to the index of the last certificate added. Args: - script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script. + script (Union[UTxO, PlutusScript]): A plutus script. redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO. Returns: @@ -577,19 +568,16 @@ def script_data_hash(self) -> Optional[ScriptDataHash]: if self.datums or self._redeemer_list: cost_models = {} for s in self.all_scripts: - if isinstance(s, PlutusV1Script) or type(s) is bytes: - cost_models[0] = ( - self.context.protocol_param.cost_models.get("PlutusV1") - or PLUTUS_V1_COST_MODEL - ) - if isinstance(s, PlutusV2Script): - cost_models[1] = ( - self.context.protocol_param.cost_models.get("PlutusV2") - or PLUTUS_V2_COST_MODEL - ) - if isinstance(s, PlutusV3Script): - cost_models[2] = self.context.protocol_param.cost_models.get( - "PlutusV3", {} + version = -1 + if isinstance(s, PlutusScript): + version = s.version + elif type(s) is bytes: + version = 1 + if version != -1: + cost_models[version - 1] = ( + self.context.protocol_param.cost_models.get( + f"PlutusV{version}", {} + ) ) return script_data_hash( self.redeemers(), list(self.datums.values()), CostModels(cost_models)