From 04adf634c64afe1e5e78de9eed19c6e894a9892e Mon Sep 17 00:00:00 2001 From: SCM Date: Fri, 15 Nov 2024 13:41:46 +0000 Subject: [PATCH 1/3] Cp api_v2 to api_v1 --- plutus_bench/ledger/api_v1.py | 481 ++++++++++++++++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 plutus_bench/ledger/api_v1.py diff --git a/plutus_bench/ledger/api_v1.py b/plutus_bench/ledger/api_v1.py new file mode 100644 index 0000000..4208400 --- /dev/null +++ b/plutus_bench/ledger/api_v1.py @@ -0,0 +1,481 @@ +""" +The PlutusV2 ledger API. +All classes involved in defining the ScriptContext passed by the node. +""" + +from dataclasses import dataclass +from typing import Dict, List, Union + +from pycardano import Datum as Anything, PlutusData + + +# Plutus V2 +@dataclass(unsafe_hash=True) +class TxId(PlutusData): + """ + A transaction id, a 64 bytes long hash of the transaction body (also called transaction hash). + + Example value: TxId(bytes.fromhex("842a4d37b036da6ab3c04331240e67d81746beb44f23ad79703e026705361956")) + """ + + CONSTR_ID = 0 + + tx_id: bytes + + +@dataclass(unsafe_hash=True) +class TrueData(PlutusData): + """ + A Datum that represents True in Haskell implementations. + It is thus used as an encoding for True in the ScriptContext. + + Example value: TrueData() + """ + + CONSTR_ID = 0 + + +@dataclass(unsafe_hash=True) +class FalseData(PlutusData): + """ + A Datum that represents False in Haskell implementations. + It is thus used as an encoding for False in the ScriptContext. + + Example value: FalseData() + """ + + CONSTR_ID = 1 + + +# A Datum that represents a boolean value in Haskell implementations. +# It is thus used as an encoding for booleans in the ScriptContext. +# +# Example value: TrueData() +BoolData = Union[TrueData, FalseData] + + +@dataclass(unsafe_hash=True) +class TxOutRef(PlutusData): + """ + A reference to a transaction output (hash/id + index) + """ + + CONSTR_ID = 0 + + id: TxId + idx: int + + +# A public key hash, used to identify signatures provided by a wallet +PubKeyHash = bytes + + +@dataclass(unsafe_hash=True) +class PubKeyCredential(PlutusData): + """ + Part of an address that is authenticated by a public key hash + + Example value: PubKeyCredential(bytes.fromhex("c06ddaad12fc4ded18e56feac72957c1aa75fce6096b40e63ec88274")) + """ + + CONSTR_ID = 0 + credential_hash: PubKeyHash + + +# A validator hash, used to identify signatures provided by a smart contract +ValidatorHash = bytes + + +@dataclass(unsafe_hash=True) +class ScriptCredential(PlutusData): + """ + Part of an address that is authenticated by a smart cotnract + + Example value: ScriptCredential(bytes.fromhex("c06ddaad12fc4ded18e56feac72957c1aa75fce6096b40e63ec88274")) + """ + + CONSTR_ID = 1 + credential_hash: ValidatorHash + + +# A credential, either smart contract or public key hash +Credential = Union[PubKeyCredential, ScriptCredential] + + +@dataclass(unsafe_hash=True) +class StakingHash(PlutusData): + """ + Indicates that the stake of this address is controlled by the associated credential + """ + + CONSTR_ID = 0 + value: Credential + + +@dataclass(unsafe_hash=True) +class StakingPtr(PlutusData): + """ + Indicates that the stake of this address is controlled by the associated pointer. + + In an address, a chain pointer refers to a point of the chain containing a stake key registration certificate. + A point is identified by the 3 coordinates in this object. + """ + + CONSTR_ID = 1 + # an absolute slot number + slot_no: int + # a transaction index (within that slot) + tx_index: int + # a (delegation) certificate index (within that transaction) + cert_index: int + + +# Part of an address that controls who can delegate the stake associated with an address +StakingCredential = Union[StakingHash, StakingPtr] + + +@dataclass(unsafe_hash=True) +class NoStakingCredential(PlutusData): + """ + Indicates that this address has no staking credentials. + Its funds can not be delegated. + """ + + CONSTR_ID = 1 + + +@dataclass(unsafe_hash=True) +class SomeStakingCredential(PlutusData): + """ + Indicates that this address has staking credentials. + Its funds can be delegated by the credentialed user. + """ + + CONSTR_ID = 0 + staking_credential: StakingCredential + + +@dataclass(unsafe_hash=True) +class Address(PlutusData): + """ + A Shelley address, consisting of a payment and staking credential + """ + + CONSTR_ID = 0 + + payment_credential: Credential + staking_credential: Union[NoStakingCredential, SomeStakingCredential] + + +# The policy Id of a token +PolicyId = bytes + +# The name of a token in bytes (not textual representation!) +TokenName = bytes + +# The Plutus representation of amounts of tokens being spent, sent or minted +# It is a two-layered dictionary that stores for each policy id and token name +# the amount of the token that is being sent/minted/burned etc +# +# Lovelace is represented with policy id b"" and token name b"" +Value = Dict[PolicyId, Dict[TokenName, int]] + +# A hash of a Datum +DatumHash = bytes + + +@dataclass(unsafe_hash=True) +class SomeDatumHash(PlutusData): + """ + Indicates that there is a datum associated with this output, which has the given hash. + """ + + CONSTR_ID = 1 + datum_hash: DatumHash + + +@dataclass(unsafe_hash=True) +class SomeScriptHash(PlutusData): + """ + Indicates that there is a script associated with this output, which has the given hash. + """ + + CONSTR_ID = 0 + script_hash: DatumHash + + +# The abstract super type of any object in opshin. +# Use if you don't know what kind of object is being passed or if it doesn't matter. +BuiltinData = Anything + + +# An abstract type annotation that something is supposed to be used as a redeemer. +Redeemer = BuiltinData + + +# An abstract type annotation that something is supposed to be used as a datum. +Datum = BuiltinData + + +@dataclass(unsafe_hash=True) +class NoOutputDatum(PlutusData): + """ + Indicates that there is no datum associated with an output + """ + + CONSTR_ID = 0 + + +@dataclass(unsafe_hash=True) +class SomeOutputDatumHash(PlutusData): + """ + Indicates that there is an datum associated with an output, which has the attached hash + """ + + CONSTR_ID = 1 + datum_hash: DatumHash + + +@dataclass(unsafe_hash=True) +class SomeOutputDatum(PlutusData): + """ + Indicates that there is an datum associated with an output, which is inlined and equal to the attached datum + """ + + CONSTR_ID = 2 + datum: Datum + + +# Possible cases of datum association with an output +OutputDatum = Union[NoOutputDatum, SomeOutputDatumHash, SomeOutputDatum] + + +@dataclass(unsafe_hash=True) +class NoScriptHash(PlutusData): + """ + Indicates that there is no script associated with an output + """ + + CONSTR_ID = 1 + + +@dataclass(unsafe_hash=True) +class TxOut(PlutusData): + """ + The plutus representation of an transaction output, consisting of + - address: address owning this output + - value: tokens associated with this output + - datum: datum associated with this output + - reference_script: reference script associated with this output + """ + + CONSTR_ID = 0 + + address: Address + value: Value + datum: OutputDatum + reference_script: Union[NoScriptHash, SomeScriptHash] + + +@dataclass(unsafe_hash=True) +class TxInInfo(PlutusData): + """ + The plutus representation of an transaction output, that is consumed by the transaction. + """ + + CONSTR_ID = 0 + + out_ref: TxOutRef + resolved: TxOut + + +@dataclass(unsafe_hash=True) +class DCertDelegRegKey(PlutusData): + CONSTR_ID = 0 + value: StakingCredential + + +@dataclass(unsafe_hash=True) +class DCertDelegDeRegKey(PlutusData): + CONSTR_ID = 1 + value: StakingCredential + + +@dataclass(unsafe_hash=True) +class DCertDelegDelegate(PlutusData): + CONSTR_ID = 2 + delegator: StakingCredential + delegatee: PubKeyHash + + +@dataclass(unsafe_hash=True) +class DCertPoolRegister(PlutusData): + CONSTR_ID = 3 + pool_id: PubKeyHash + pool_vfr: PubKeyHash + + +@dataclass(unsafe_hash=True) +class DCertPoolRetire(PlutusData): + CONSTR_ID = 4 + retirement_certificate: PubKeyHash + epoch: int + + +@dataclass(unsafe_hash=True) +class DCertGenesis(PlutusData): + CONSTR_ID = 5 + + +@dataclass(unsafe_hash=True) +class DCertMir(PlutusData): + CONSTR_ID = 6 + + +DCert = Union[ + DCertDelegRegKey, + DCertDelegDeRegKey, + DCertDelegDelegate, + DCertPoolRegister, + DCertPoolRetire, + DCertGenesis, + DCertMir, +] + + +POSIXTime = int + + +@dataclass(unsafe_hash=True) +class NegInfPOSIXTime(PlutusData): + """ + Negative infinite POSIX time, used to indicate that there is no lower bound for the execution of this transaction + """ + + CONSTR_ID = 0 + + +@dataclass(unsafe_hash=True) +class FinitePOSIXTime(PlutusData): + """ + Finite POSIX time, used to indicate that there is a lower or upper bound for the execution of this transaction + """ + + CONSTR_ID = 1 + time: POSIXTime + + +@dataclass(unsafe_hash=True) +class PosInfPOSIXTime(PlutusData): + """ + Infinite POSIX time, used to indicate that there is no upper bound for the execution of this transaction + """ + + CONSTR_ID = 2 + + +ExtendedPOSIXTime = Union[NegInfPOSIXTime, FinitePOSIXTime, PosInfPOSIXTime] + + +@dataclass(unsafe_hash=True) +class UpperBoundPOSIXTime(PlutusData): + """ + Upper bound for the execution of this transaction + """ + + CONSTR_ID = 0 + limit: ExtendedPOSIXTime + closed: BoolData + + +@dataclass(unsafe_hash=True) +class LowerBoundPOSIXTime(PlutusData): + """ + Lower bound for the execution of this transaction + """ + + CONSTR_ID = 0 + limit: ExtendedPOSIXTime + closed: BoolData + + +@dataclass(unsafe_hash=True) +class POSIXTimeRange(PlutusData): + """ + Time range in which this transaction can be executed + """ + + CONSTR_ID = 0 + + lower_bound: LowerBoundPOSIXTime + upper_bound: UpperBoundPOSIXTime + + +@dataclass(unsafe_hash=True) +class Minting(PlutusData): + """ + Script purpose indicating that the given policy id is being minted or burned + """ + + CONSTR_ID = 0 + policy_id: PolicyId + + +@dataclass(unsafe_hash=True) +class Spending(PlutusData): + """ + Script purpose indicating that the given transaction output is being spent, which is + owned by the invoked contract + """ + + CONSTR_ID = 1 + tx_out_ref: TxOutRef + + +@dataclass(unsafe_hash=True) +class Rewarding(PlutusData): + CONSTR_ID = 2 + staking_credential: StakingCredential + + +@dataclass(unsafe_hash=True) +class Certifying(PlutusData): + CONSTR_ID = 3 + d_cert: DCert + + +# The reason that this script is being invoked +ScriptPurpose = Union[Minting, Spending, Rewarding, Certifying] + + +@dataclass(unsafe_hash=True) +class TxInfo(PlutusData): + """ + A complex agglomeration of everything that could be of interest to the executed script, regarding the transaction + that invoked the script + """ + + CONSTR_ID = 0 + inputs: List[TxInInfo] + reference_inputs: List[TxInInfo] + outputs: List[TxOut] + fee: Value + mint: Value + dcert: List[DCert] + wdrl: Dict[StakingCredential, int] + valid_range: POSIXTimeRange + signatories: List[PubKeyHash] + redeemers: Dict[ScriptPurpose, Redeemer] + data: Dict[DatumHash, Datum] + id: TxId + + +@dataclass(unsafe_hash=True) +class ScriptContext(PlutusData): + """ + Auxiliary information about the transaction and reason for invocation of the called script. + """ + + CONSTR_ID = 0 + tx_info: TxInfo + purpose: ScriptPurpose From f7d850b6d3281920032159b8e8f41b66fd460d63 Mon Sep 17 00:00:00 2001 From: SCM Date: Fri, 15 Nov 2024 13:42:40 +0000 Subject: [PATCH 2/3] api_v1 seems correct --- plutus_bench/ledger/api_v1.py | 67 +++++++---------------------------- 1 file changed, 12 insertions(+), 55 deletions(-) diff --git a/plutus_bench/ledger/api_v1.py b/plutus_bench/ledger/api_v1.py index 4208400..eccaffe 100644 --- a/plutus_bench/ledger/api_v1.py +++ b/plutus_bench/ledger/api_v1.py @@ -1,15 +1,15 @@ """ -The PlutusV2 ledger API. +The PlutusV1 ledger API. All classes involved in defining the ScriptContext passed by the node. """ from dataclasses import dataclass -from typing import Dict, List, Union +from typing import Dict, List, Union, Tuple from pycardano import Datum as Anything, PlutusData -# Plutus V2 +# Plutus V1 @dataclass(unsafe_hash=True) class TxId(PlutusData): """ @@ -184,26 +184,6 @@ class Address(PlutusData): DatumHash = bytes -@dataclass(unsafe_hash=True) -class SomeDatumHash(PlutusData): - """ - Indicates that there is a datum associated with this output, which has the given hash. - """ - - CONSTR_ID = 1 - datum_hash: DatumHash - - -@dataclass(unsafe_hash=True) -class SomeScriptHash(PlutusData): - """ - Indicates that there is a script associated with this output, which has the given hash. - """ - - CONSTR_ID = 0 - script_hash: DatumHash - - # The abstract super type of any object in opshin. # Use if you don't know what kind of object is being passed or if it doesn't matter. BuiltinData = Anything @@ -218,7 +198,7 @@ class SomeScriptHash(PlutusData): @dataclass(unsafe_hash=True) -class NoOutputDatum(PlutusData): +class NoDatumHash(PlutusData): """ Indicates that there is no datum associated with an output """ @@ -227,38 +207,15 @@ class NoOutputDatum(PlutusData): @dataclass(unsafe_hash=True) -class SomeOutputDatumHash(PlutusData): +class SomeDatumHash(PlutusData): """ - Indicates that there is an datum associated with an output, which has the attached hash + Indicates that there is an datum associated with an output, which has the given hash """ CONSTR_ID = 1 datum_hash: DatumHash -@dataclass(unsafe_hash=True) -class SomeOutputDatum(PlutusData): - """ - Indicates that there is an datum associated with an output, which is inlined and equal to the attached datum - """ - - CONSTR_ID = 2 - datum: Datum - - -# Possible cases of datum association with an output -OutputDatum = Union[NoOutputDatum, SomeOutputDatumHash, SomeOutputDatum] - - -@dataclass(unsafe_hash=True) -class NoScriptHash(PlutusData): - """ - Indicates that there is no script associated with an output - """ - - CONSTR_ID = 1 - - @dataclass(unsafe_hash=True) class TxOut(PlutusData): """ @@ -273,8 +230,8 @@ class TxOut(PlutusData): address: Address value: Value - datum: OutputDatum - reference_script: Union[NoScriptHash, SomeScriptHash] + datum: Union[NoDatumHash, SomeDatumHash] # Changes in V2 to allow inline datum + # reference_script: Union[NoScriptHash, SomeScriptHash] # added in V2 @dataclass(unsafe_hash=True) @@ -457,16 +414,16 @@ class TxInfo(PlutusData): CONSTR_ID = 0 inputs: List[TxInInfo] - reference_inputs: List[TxInInfo] + # reference_inputs: List[TxInInfo] # Added in V2 outputs: List[TxOut] fee: Value mint: Value dcert: List[DCert] - wdrl: Dict[StakingCredential, int] + wdrl: List[Tuple[StakingCredential, int]] # Changes to AssocMap in V2 valid_range: POSIXTimeRange signatories: List[PubKeyHash] - redeemers: Dict[ScriptPurpose, Redeemer] - data: Dict[DatumHash, Datum] + # redeemers: Dict[ScriptPurpose, Redeemer] # Added in V2 + data: List[Tuple[DatumHash, Datum]] # Changes to AssocMap in V2 id: TxId From 34c3c152e5baad0e90d0f85549ae0f25c489de8e Mon Sep 17 00:00:00 2001 From: SCM Date: Wed, 27 Nov 2024 14:30:55 +0000 Subject: [PATCH 3/3] to_script_context_v1 and update v2, moving func out of tx_tools --- plutus_bench/mock.py | 40 ++- plutus_bench/to_script_context_v1.py | 255 ++++++++++++++++ plutus_bench/to_script_context_v2.py | 157 +++++++--- plutus_bench/tx_tools.py | 418 +++++++++------------------ tests/fortytwo.py | 61 ++++ tests/gift.py | 6 +- tests/test_fortytwo_v1.py | 36 +++ 7 files changed, 646 insertions(+), 327 deletions(-) create mode 100644 plutus_bench/to_script_context_v1.py create mode 100644 tests/fortytwo.py create mode 100644 tests/test_fortytwo_v1.py diff --git a/plutus_bench/mock.py b/plutus_bench/mock.py index 802a3ec..cf59947 100644 --- a/plutus_bench/mock.py +++ b/plutus_bench/mock.py @@ -211,16 +211,26 @@ def submit_tx(self, tx: Transaction): self.submit_tx_mock(tx) def submit_tx_mock(self, tx: Transaction): - def is_witnessed(address: Union[bytes, pycardano.Address], witness_set: pycardano.TransactionWitnessSet) -> bool: + def is_witnessed( + address: Union[bytes, pycardano.Address], + witness_set: pycardano.TransactionWitnessSet, + ) -> bool: if isinstance(address, bytes): address = pycardano.Address.from_primitive(address) staking_part = address.staking_part if isinstance(staking_part, pycardano.ScriptHash): - scripts = (witness_set.plutus_v1_script or []) + (witness_set.plutus_v2_script or []) + (witness_set.plutus_v3_script or []) - return staking_part in [pycardano.plutus_script_hash(s) for s in scripts] + scripts = ( + (witness_set.plutus_v1_script or []) + + (witness_set.plutus_v2_script or []) + + (witness_set.plutus_v3_script or []) + ) + return staking_part in [ + pycardano.plutus_script_hash(s) for s in scripts + ] else: - raise NotImplementedError() - + raise NotImplementedError( + "Only ScriptHash is currently supported for address staking_part" + ) for input in tx.transaction_body.inputs: utxo = self.get_utxo_from_txid(input.transaction_id, input.index) @@ -265,14 +275,17 @@ def is_witnessed(address: Union[bytes, pycardano.Address], witness_set: pycardan for address in tx.transaction_body.withdraws or {}: value = tx.transaction_body.withdraws[address] stake_address = pycardano.Address.from_primitive(address) - assert is_witnessed(stake_address, tx.transaction_witness_set), f'Withdrawal from address {stake_address} is not witnessed' - assert str(stake_address) in self._reward_account, 'Address {stake_address} not registered' - rewards = self._reward_account[str(stake_address)]['delegation']['rewards'] - assert rewards == value, 'All rewards must be withdrawn. Requested {value} but account contains {rewards}' - self._reward_account[str(stake_address)]['delegation']['rewards'] == 0 - - - + assert is_witnessed( + stake_address, tx.transaction_witness_set + ), f"Withdrawal from address {stake_address} is not witnessed" + assert ( + str(stake_address) in self._reward_account + ), "Address {stake_address} not registered" + rewards = self._reward_account[str(stake_address)]["delegation"]["rewards"] + assert ( + rewards == value + ), "All rewards must be withdrawn. Requested {value} but account contains {rewards}" + self._reward_account[str(stake_address)]["delegation"]["rewards"] == 0 def submit_tx_cbor(self, cbor: Union[bytes, str]): return self.submit_tx(Transaction.from_cbor(cbor)) @@ -297,6 +310,7 @@ def evaluate_tx(self, tx: Transaction) -> Dict[str, ExecutionUnits]: for invocation in script_invocations: # run opshin script if available if self.opshin_scripts.get(invocation.script) is not None: + raise NotImplementedError("This code never seems to be reached") opshin_validator = self.opshin_scripts[invocation.script] evaluate_opshin_validator(opshin_validator, invocation) redeemer = invocation.redeemer diff --git a/plutus_bench/to_script_context_v1.py b/plutus_bench/to_script_context_v1.py new file mode 100644 index 0000000..1826972 --- /dev/null +++ b/plutus_bench/to_script_context_v1.py @@ -0,0 +1,255 @@ +from typing import Optional, Tuple + +import pycardano +from .ledger.api_v1 import * + + +def to_staking_credential( + sk: Union[ + pycardano.VerificationKeyHash, + pycardano.ScriptHash, + pycardano.PointerAddress, + None, + ] +): + try: + return SomeStakingCredential(to_staking_hash(sk)) + except NotImplementedError: + return NoStakingCredential() + + +def to_staking_hash( + sk: Union[ + pycardano.VerificationKeyHash, pycardano.ScriptHash, pycardano.PointerAddress + ] +): + if isinstance(sk, pycardano.PointerAddress): + return StakingPtr(sk.slot, sk.tx_index, sk.cert_index) + if isinstance(sk, pycardano.VerificationKeyHash): + return StakingHash(PubKeyCredential(sk.payload)) + if isinstance(sk, pycardano.ScriptHash): + return StakingHash(ScriptCredential(sk.payload)) + raise NotImplementedError(f"Unknown stake key type {type(sk)}") + + +def to_wdrl(wdrl: Optional[pycardano.Withdrawals]) -> Dict[StakingCredential, int]: + if wdrl is None: + return [] + + def m(k: bytes): + sk = pycardano.Address.from_primitive(k).staking_part + return to_staking_hash(sk) + + return [(m(key), val) for key, val in wdrl.to_primitive().items()] + # return {m(key): val for key, val in wdrl.to_primitive().items()} + + +def to_valid_range(validity_start: Optional[int], ttl: Optional[int], posix_from_slot): + if validity_start is None: + lower_bound = LowerBoundPOSIXTime(NegInfPOSIXTime(), FalseData()) + else: + start = posix_from_slot(validity_start) * 1000 + lower_bound = LowerBoundPOSIXTime(FinitePOSIXTime(start), TrueData()) + if ttl is None: + upper_bound = UpperBoundPOSIXTime(PosInfPOSIXTime(), FalseData()) + else: + end = posix_from_slot(ttl) * 1000 + upper_bound = UpperBoundPOSIXTime(FinitePOSIXTime(end), TrueData()) + return POSIXTimeRange(lower_bound, upper_bound) + + +def to_pubkeyhash(vkh: pycardano.VerificationKeyHash): + return PubKeyHash(vkh.payload) + + +def to_tx_id(tx_id: pycardano.TransactionId): + return TxId(tx_id.payload) + + +def to_dcert(c: pycardano.Certificate) -> DCert: + if isinstance(c, pycardano.StakeRegistration): + return DCertDelegRegKey(to_staking_hash(c.stake_credential.credential)) + elif isinstance(c, pycardano.StakeDelegation): + return DCertDelegDelegate( + to_staking_hash(c.stake_credential.credential), + PubKeyHash(c.pool_keyhash.payload), + ) + elif isinstance(c, pycardano.StakeDeregistration): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + elif isinstance(c, pycardano.PoolRegistration): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + elif isinstance(c, pycardano.PoolRetirement): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + raise NotImplementedError(f"Certificates of type {type(c)} are not implemented") + + +def multiasset_to_value(ma: pycardano.MultiAsset) -> Value: + if ma is None: + return {b"": {b"": 0}} + return { + PolicyId(policy_id): { + TokenName(asset_name): quantity for asset_name, quantity in asset.items() + } + for policy_id, asset in ma.to_shallow_primitive().items() + } + + +def value_to_value(v: pycardano.Value): + ma = multiasset_to_value(v.multi_asset) + ma[b""] = {b"": v.coin} + return ma + + +def to_payment_credential( + c: Union[pycardano.VerificationKeyHash, pycardano.ScriptHash] +): + if isinstance(c, pycardano.VerificationKeyHash): + return PubKeyCredential(PubKeyHash(c.payload)) + if isinstance(c, pycardano.ScriptHash): + return ScriptCredential(ValidatorHash(c.payload)) + raise NotImplementedError(f"Unknown payment key type {type(c)}") + + +def to_address(a: pycardano.Address): + return Address( + to_payment_credential(a.payment_part), + to_staking_credential(a.staking_part), + ) + + +def to_tx_out(o: pycardano.TransactionOutput): + # if o.datum is not None: + # output_datum = SomeOutputDatum(o.datum) + assert o.datum is None, "TxOut datum not supported in plutus v1" + if o.datum_hash is not None: + output_datum = SomeDatumHash(o.datum_hash.payload) + else: + output_datum = NoDatumHash() + return TxOut( + to_address(o.address), + value_to_value(o.amount), + output_datum, + ) + + +def to_tx_out_ref(i: pycardano.TransactionInput): + return TxOutRef( + TxId(i.transaction_id.payload), + i.index, + ) + + +def to_tx_in_info(i: pycardano.TransactionInput, o: pycardano.TransactionOutput): + return TxInInfo( + to_tx_out_ref(i), + to_tx_out(o), + ) + + +def to_redeemer_purpose( + r: Union[pycardano.RedeemerKey, pycardano.Redeemer], + tx_body: pycardano.TransactionBody, +): + v = r.tag + if v == pycardano.RedeemerTag.SPEND: + spent_input = tx_body.inputs[r.index] + return Spending(to_tx_out_ref(spent_input)) + elif v == pycardano.RedeemerTag.MINT: + minted_id = sorted(tx_body.mint.data.keys())[r.index] + return Minting(PolicyId(minted_id.payload)) + elif v == pycardano.RedeemerTag.CERTIFICATE: + certificate = tx_body.certificates[r.index] + return Certifying(to_dcert(certificate)) + elif v == pycardano.RedeemerTag.WITHDRAWAL: + withdrawal = sorted(tx_body.withdraws.keys())[r.index] + script_hash = pycardano.Address.from_primitive(withdrawal).staking_part + return Rewarding(to_staking_hash(script_hash)) + else: + raise NotImplementedError() + + +def to_tx_info( + tx: pycardano.Transaction, + resolved_inputs: List[pycardano.TransactionOutput], + resolved_reference_inputs: List[pycardano.TransactionOutput], + posix_from_slot, +): + tx_body = tx.transaction_body + datums = [ + o.datum + for o in tx_body.outputs + resolved_inputs + resolved_reference_inputs + if o.datum is not None + ] + if tx.transaction_witness_set.plutus_data: + datums += tx.transaction_witness_set.plutus_data + + redeemers = ( + tx.transaction_witness_set.redeemer + if tx.transaction_witness_set.redeemer + else [] + ) + return TxInfo( + [to_tx_in_info(i, o) for i, o in zip(tx_body.inputs, resolved_inputs)], + # ( + # [ + # to_tx_in_info(i, o) + # for i, o in zip(tx_body.reference_inputs, resolved_reference_inputs) + # ] + # if tx_body.reference_inputs is not None + # else [] + # ), + [to_tx_out(o) for o in tx_body.outputs], + value_to_value(pycardano.Value(tx_body.fee)), + multiasset_to_value(tx_body.mint), + [to_dcert(c) for c in tx_body.certificates] if tx_body.certificates else [], + to_wdrl(tx_body.withdraws), + to_valid_range(tx_body.validity_start, tx_body.ttl, posix_from_slot), + ( + [to_pubkeyhash(s) for s in tx_body.required_signers] + if tx_body.required_signers + else [] + ), + # ( + # {to_redeemer_purpose(k, tx_body): v.data for k, v in redeemers.items()} + # if isinstance(redeemers, pycardano.RedeemerMap) + # else {to_redeemer_purpose(r, tx_body): r.data for r in redeemers} + # ), + [(pycardano.datum_hash(d).payload, d) for d in datums], + to_tx_id(tx_body.id), + ) + + +def to_spending_script_context( + tx_info_args: Tuple, spending_input: pycardano.TransactionInput +): + return ScriptContext( + to_tx_info(*tx_info_args), Spending(to_tx_out_ref(spending_input)) + ) + + +def to_minting_script_context( + tx_info_args: Tuple, minting_script: pycardano.PlutusV2Script +): + return ScriptContext( + to_tx_info(*tx_info_args), + Minting(pycardano.script_hash(minting_script).payload), + ) + + +def to_certificate_script_context(tx_info_args, certificate): + return ScriptContext(to_tx_info(*tx_info_args), Certifying(to_dcert(certificate))) + + +def to_withdrawal_script_context(tx_info_args, script_hash): + return ScriptContext( + to_tx_info(*tx_info_args), Rewarding(to_staking_hash(script_hash)) + ) diff --git a/plutus_bench/to_script_context_v2.py b/plutus_bench/to_script_context_v2.py index 8d61aad..a3b54e9 100644 --- a/plutus_bench/to_script_context_v2.py +++ b/plutus_bench/to_script_context_v2.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Tuple import pycardano from .ledger.api_v2 import * @@ -43,36 +43,60 @@ def m(k: bytes): return {m(key): val for key, val in wdrl.to_primitive().items()} -def to_valid_range(validity_start: Optional[int], ttl: Optional[int]): +def to_valid_range(validity_start: Optional[int], ttl: Optional[int], posix_from_slot): if validity_start is None: lower_bound = LowerBoundPOSIXTime(NegInfPOSIXTime(), FalseData()) else: - # TODO converting slot number to POSIXTime - lower_bound = LowerBoundPOSIXTime(FinitePOSIXTime(validity_start), TrueData()) + start = posix_from_slot(validity_start) * 1000 + lower_bound = LowerBoundPOSIXTime(FinitePOSIXTime(start), TrueData()) if ttl is None: upper_bound = UpperBoundPOSIXTime(PosInfPOSIXTime(), FalseData()) else: - # TODO converting slot number to POSIXTime - upper_bound = UpperBoundPOSIXTime(FinitePOSIXTime(ttl), TrueData()) + end = posix_from_slot(ttl) * 1000 + upper_bound = UpperBoundPOSIXTime(FinitePOSIXTime(end), TrueData()) return POSIXTimeRange(lower_bound, upper_bound) def to_pubkeyhash(vkh: pycardano.VerificationKeyHash): - return PubKeyHash(vkh.to_primitive()) + return PubKeyHash(vkh.payload) def to_tx_id(tx_id: pycardano.TransactionId): - return TxId(tx_id.to_primitive()) + return TxId(tx_id.payload) def to_dcert(c: pycardano.Certificate) -> DCert: - raise NotImplementedError("Can not convert certificates yet") + if isinstance(c, pycardano.StakeRegistration): + return DCertDelegRegKey(to_staking_hash(c.stake_credential.credential)) + elif isinstance(c, pycardano.StakeDelegation): + return DCertDelegDelegate( + to_staking_hash(c.stake_credential.credential), + PubKeyHash(c.pool_keyhash.payload), + ) + elif isinstance(c, pycardano.StakeDeregistration): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + elif isinstance(c, pycardano.PoolRegistration): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + elif isinstance(c, pycardano.PoolRetirement): + # TODO + raise NotImplementedError( + f"Certificates of type {type(c)} can not be converted yet" + ) + raise NotImplementedError(f"Certificates of type {type(c)} are not implemented") def multiasset_to_value(ma: pycardano.MultiAsset) -> Value: + if ma is None: + return {b"": {b"": 0}} return { PolicyId(policy_id): { - TokenName(asset_name): quantity for asset_name, quantity in asset + TokenName(asset_name): quantity for asset_name, quantity in asset.items() } for policy_id, asset in ma.to_shallow_primitive().items() } @@ -80,7 +104,7 @@ def multiasset_to_value(ma: pycardano.MultiAsset) -> Value: def value_to_value(v: pycardano.Value): ma = multiasset_to_value(v.multi_asset) - ma[b""][b""] = v.coin + ma[b""] = {b"": v.coin} return ma @@ -88,9 +112,9 @@ def to_payment_credential( c: Union[pycardano.VerificationKeyHash, pycardano.ScriptHash] ): if isinstance(c, pycardano.VerificationKeyHash): - return PubKeyCredential(PubKeyHash(c.to_primitive())) + return PubKeyCredential(PubKeyHash(c.payload)) if isinstance(c, pycardano.ScriptHash): - return ScriptCredential(ValidatorHash(c.to_primitive())) + return ScriptCredential(ValidatorHash(c.payload)) raise NotImplementedError(f"Unknown payment key type {type(c)}") @@ -105,13 +129,13 @@ def to_tx_out(o: pycardano.TransactionOutput): if o.datum is not None: output_datum = SomeOutputDatum(o.datum) elif o.datum_hash is not None: - output_datum = SomeOutputDatumHash(o.datum_hash.to_primitive()) + output_datum = SomeOutputDatumHash(o.datum_hash.payload) else: output_datum = NoOutputDatum() if o.script is None: script = NoScriptHash() else: - script = SomeScriptHash(o.script.hash().to_primitive()) + script = SomeScriptHash(pycardano.script_hash(o.script).payload) return TxOut( to_address(o.address), value_to_value(o.amount), @@ -122,7 +146,7 @@ def to_tx_out(o: pycardano.TransactionOutput): def to_tx_out_ref(i: pycardano.TransactionInput): return TxOutRef( - TxId(i.transaction_id.to_primitive()), + TxId(i.transaction_id.payload), i.index, ) @@ -134,34 +158,101 @@ def to_tx_in_info(i: pycardano.TransactionInput, o: pycardano.TransactionOutput) ) +def to_redeemer_purpose( + r: Union[pycardano.RedeemerKey, pycardano.Redeemer], + tx_body: pycardano.TransactionBody, +): + v = r.tag + if v == pycardano.RedeemerTag.SPEND: + spent_input = tx_body.inputs[r.index] + return Spending(to_tx_out_ref(spent_input)) + elif v == pycardano.RedeemerTag.MINT: + minted_id = sorted(tx_body.mint.data.keys())[r.index] + return Minting(PolicyId(minted_id.payload)) + elif v == pycardano.RedeemerTag.CERTIFICATE: + certificate = tx_body.certificates[r.index] + return Certifying(to_dcert(certificate)) + elif v == pycardano.RedeemerTag.WITHDRAWAL: + withdrawal = sorted(tx_body.withdraws.keys())[r.index] + script_hash = pycardano.Address.from_primitive(withdrawal).staking_part + return Rewarding(to_staking_hash(script_hash)) + else: + raise NotImplementedError() + + def to_tx_info( tx: pycardano.Transaction, resolved_inputs: List[pycardano.TransactionOutput], resolved_reference_inputs: List[pycardano.TransactionOutput], + posix_from_slot, ): tx_body = tx.transaction_body + datums = [ + o.datum + for o in tx_body.outputs + resolved_inputs + resolved_reference_inputs + if o.datum is not None + ] + if tx.transaction_witness_set.plutus_data: + datums += tx.transaction_witness_set.plutus_data + + redeemers = ( + tx.transaction_witness_set.redeemer + if tx.transaction_witness_set.redeemer + else [] + ) return TxInfo( [to_tx_in_info(i, o) for i, o in zip(tx_body.inputs, resolved_inputs)], - [ - to_tx_in_info(i, o) - for i, o in zip(tx_body.reference_inputs, resolved_reference_inputs) - ], + ( + [ + to_tx_in_info(i, o) + for i, o in zip(tx_body.reference_inputs, resolved_reference_inputs) + ] + if tx_body.reference_inputs is not None + else [] + ), [to_tx_out(o) for o in tx_body.outputs], value_to_value(pycardano.Value(tx_body.fee)), multiasset_to_value(tx_body.mint), - [to_dcert(c) for c in tx_body.certificates], + [to_dcert(c) for c in tx_body.certificates] if tx_body.certificates else [], to_wdrl(tx_body.withdraws), - to_valid_range(tx_body.validity_start, tx_body.ttl), - [to_pubkeyhash(s) for s in tx_body.required_signers], - { - pycardano.datum_hash(d): d - for d in [ - o.datum - for o in tx_body.outputs + resolved_inputs + resolved_reference_inputs - if o.datum is not None - ] - + tx.transaction_witness_set.plutus_data - }, - {pycardano.todo_datum_hash(r): r for r in tx.transaction_witness_set.redeemer}, + to_valid_range(tx_body.validity_start, tx_body.ttl, posix_from_slot), + ( + [to_pubkeyhash(s) for s in tx_body.required_signers] + if tx_body.required_signers + else [] + ), + ( + {to_redeemer_purpose(k, tx_body): v.data for k, v in redeemers.items()} + if isinstance(redeemers, pycardano.RedeemerMap) + else {to_redeemer_purpose(r, tx_body): r.data for r in redeemers} + ), + {pycardano.datum_hash(d).payload: d for d in datums}, to_tx_id(tx_body.id), ) + + +def to_spending_script_context( + tx_info_args: Tuple, spending_input: pycardano.TransactionInput +): + return ScriptContext( + to_tx_info(*tx_info_args), Spending(to_tx_out_ref(spending_input)) + ) + + +def to_minting_script_context( + tx_info_args: Tuple, minting_script: pycardano.PlutusV2Script +): + return ScriptContext( + to_tx_info(*tx_info_args), + Minting(pycardano.script_hash(minting_script).payload), + ) + + +def to_certificate_script_context(tx_info_args, certificate): + return ScriptContext(to_tx_info(*tx_info_args), Certifying(to_dcert(certificate))) + + +def to_withdrawal_script_context(tx_info_args, script_hash): + return ScriptContext( + to_tx_info(*tx_info_args), Rewarding(to_staking_hash(script_hash)) + ) diff --git a/plutus_bench/tx_tools.py b/plutus_bench/tx_tools.py index 61b552f..3b14353 100644 --- a/plutus_bench/tx_tools.py +++ b/plutus_bench/tx_tools.py @@ -1,5 +1,6 @@ from functools import cache -from typing import Optional +from typing import Optional, Union, List +from dataclasses import dataclass import cbor2 import pycardano @@ -11,238 +12,30 @@ RedeemerTag, plutus_script_hash, datum_hash, + PlutusV1Script, PlutusV2Script, UTxO, ) -from .ledger.api_v2 import * +from pycardano import Datum as Anything, PlutusData +# from .ledger.api_v2 import * +from .ledger.api_v1 import ScriptContext as ScriptContextV1 +from .ledger.api_v2 import ScriptContext as ScriptContextV2 +from .to_script_context_v2 import ( + to_spending_script_context as to_spending_script_context_v2, + to_minting_script_context as to_minting_script_context_v2, + to_certificate_script_context as to_certificate_script_context_v2, + to_withdrawal_script_context as to_withdrawal_script_context_v2, +) +from .to_script_context_v1 import ( + to_spending_script_context as to_spending_script_context_v1, + to_minting_script_context as to_minting_script_context_v1, + to_certificate_script_context as to_certificate_script_context_v1, + to_withdrawal_script_context as to_withdrawal_script_context_v1, +) -def to_staking_credential( - sk: Union[ - pycardano.VerificationKeyHash, - pycardano.ScriptHash, - pycardano.PointerAddress, - None, - ] -): - try: - return SomeStakingCredential(to_staking_hash(sk)) - except NotImplementedError: - return NoStakingCredential() - - -def to_staking_hash( - sk: Union[ - pycardano.VerificationKeyHash, pycardano.ScriptHash, pycardano.PointerAddress - ] -): - if isinstance(sk, pycardano.PointerAddress): - return StakingPtr(sk.slot, sk.tx_index, sk.cert_index) - if isinstance(sk, pycardano.VerificationKeyHash): - return StakingHash(PubKeyCredential(sk.payload)) - if isinstance(sk, pycardano.ScriptHash): - return StakingHash(ScriptCredential(sk.payload)) - raise NotImplementedError(f"Unknown stake key type {type(sk)}") - - -def to_wdrl(wdrl: Optional[pycardano.Withdrawals]) -> Dict[StakingCredential, int]: - if wdrl is None: - return {} - - def m(k: bytes): - sk = pycardano.Address.from_primitive(k).staking_part - return to_staking_hash(sk) - - return {m(key): val for key, val in wdrl.to_primitive().items()} - - -def to_valid_range(validity_start: Optional[int], ttl: Optional[int], posix_from_slot): - if validity_start is None: - lower_bound = LowerBoundPOSIXTime(NegInfPOSIXTime(), FalseData()) - else: - start = posix_from_slot(validity_start) * 1000 - lower_bound = LowerBoundPOSIXTime(FinitePOSIXTime(start), TrueData()) - if ttl is None: - upper_bound = UpperBoundPOSIXTime(PosInfPOSIXTime(), FalseData()) - else: - end = posix_from_slot(ttl) * 1000 - upper_bound = UpperBoundPOSIXTime(FinitePOSIXTime(end), TrueData()) - return POSIXTimeRange(lower_bound, upper_bound) - - -def to_pubkeyhash(vkh: pycardano.VerificationKeyHash): - return PubKeyHash(vkh.payload) - - -def to_tx_id(tx_id: pycardano.TransactionId): - return TxId(tx_id.payload) - - -def to_dcert(c: pycardano.Certificate) -> DCert: - if isinstance(c, pycardano.StakeRegistration): - return DCertDelegRegKey(to_staking_hash(c.stake_credential.credential)) - elif isinstance(c, pycardano.StakeDelegation): - return DCertDelegDelegate( - to_staking_hash(c.stake_credential.credential), - PubKeyHash(c.pool_keyhash.payload), - ) - elif isinstance(c, pycardano.StakeDeregistration): - # TODO - raise NotImplementedError( - f"Certificates of type {type(c)} can not be converted yet" - ) - elif isinstance(c, pycardano.PoolRegistration): - # TODO - raise NotImplementedError( - f"Certificates of type {type(c)} can not be converted yet" - ) - elif isinstance(c, pycardano.PoolRetirement): - # TODO - raise NotImplementedError( - f"Certificates of type {type(c)} can not be converted yet" - ) - raise NotImplementedError(f"Certificates of type {type(c)} are not implemented") - - -def multiasset_to_value(ma: pycardano.MultiAsset) -> Value: - if ma is None: - return {b"": {b"": 0}} - return { - PolicyId(policy_id): { - TokenName(asset_name): quantity for asset_name, quantity in asset.items() - } - for policy_id, asset in ma.to_shallow_primitive().items() - } - - -def value_to_value(v: pycardano.Value): - ma = multiasset_to_value(v.multi_asset) - ma[b""] = {b"": v.coin} - return ma - - -def to_payment_credential( - c: Union[pycardano.VerificationKeyHash, pycardano.ScriptHash] -): - if isinstance(c, pycardano.VerificationKeyHash): - return PubKeyCredential(PubKeyHash(c.payload)) - if isinstance(c, pycardano.ScriptHash): - return ScriptCredential(ValidatorHash(c.payload)) - raise NotImplementedError(f"Unknown payment key type {type(c)}") - - -def to_address(a: pycardano.Address): - return Address( - to_payment_credential(a.payment_part), - to_staking_credential(a.staking_part), - ) - - -def to_tx_out(o: pycardano.TransactionOutput): - if o.datum is not None: - output_datum = SomeOutputDatum(o.datum) - elif o.datum_hash is not None: - output_datum = SomeOutputDatumHash(o.datum_hash.payload) - else: - output_datum = NoOutputDatum() - if o.script is None: - script = NoScriptHash() - else: - script = SomeScriptHash(pycardano.script_hash(o.script).payload) - return TxOut( - to_address(o.address), - value_to_value(o.amount), - output_datum, - script, - ) - - -def to_tx_out_ref(i: pycardano.TransactionInput): - return TxOutRef( - TxId(i.transaction_id.payload), - i.index, - ) - - -def to_tx_in_info(i: pycardano.TransactionInput, o: pycardano.TransactionOutput): - return TxInInfo( - to_tx_out_ref(i), - to_tx_out(o), - ) - - -def to_redeemer_purpose( - r: Union[pycardano.RedeemerKey, pycardano.Redeemer], - tx_body: pycardano.TransactionBody, -): - v = r.tag - if v == pycardano.RedeemerTag.SPEND: - spent_input = tx_body.inputs[r.index] - return Spending(to_tx_out_ref(spent_input)) - elif v == pycardano.RedeemerTag.MINT: - minted_id = sorted(tx_body.mint.data.keys())[r.index] - return Minting(PolicyId(minted_id.payload)) - elif v == pycardano.RedeemerTag.CERTIFICATE: - certificate = tx_body.certificates[r.index] - return Certifying(to_dcert(certificate)) - elif v == pycardano.RedeemerTag.WITHDRAWAL: - withdrawal = sorted(tx_body.withdraws.keys())[r.index] - script_hash = pycardano.Address.from_primitive(withdrawal).staking_part - return Rewarding(to_staking_hash(script_hash)) - else: - raise NotImplementedError() - - -def to_tx_info( - tx: pycardano.Transaction, - resolved_inputs: List[pycardano.TransactionOutput], - resolved_reference_inputs: List[pycardano.TransactionOutput], - posix_from_slot, -): - tx_body = tx.transaction_body - datums = [ - o.datum - for o in tx_body.outputs + resolved_inputs + resolved_reference_inputs - if o.datum is not None - ] - if tx.transaction_witness_set.plutus_data: - datums += tx.transaction_witness_set.plutus_data - - redeemers = ( - tx.transaction_witness_set.redeemer - if tx.transaction_witness_set.redeemer - else [] - ) - return TxInfo( - [to_tx_in_info(i, o) for i, o in zip(tx_body.inputs, resolved_inputs)], - ( - [ - to_tx_in_info(i, o) - for i, o in zip(tx_body.reference_inputs, resolved_reference_inputs) - ] - if tx_body.reference_inputs is not None - else [] - ), - [to_tx_out(o) for o in tx_body.outputs], - value_to_value(pycardano.Value(tx_body.fee)), - multiasset_to_value(tx_body.mint), - [to_dcert(c) for c in tx_body.certificates] if tx_body.certificates else [], - to_wdrl(tx_body.withdraws), - to_valid_range(tx_body.validity_start, tx_body.ttl, posix_from_slot), - ( - [to_pubkeyhash(s) for s in tx_body.required_signers] - if tx_body.required_signers - else [] - ), - ( - {to_redeemer_purpose(k, tx_body): v.data for k, v in redeemers.items()} - if isinstance(redeemers, pycardano.RedeemerMap) - else {to_redeemer_purpose(r, tx_body): r.data for r in redeemers} - ), - {pycardano.datum_hash(d).payload: d for d in datums}, - to_tx_id(tx_body.id), - ) +from .tool import ScriptType @dataclass @@ -250,7 +43,7 @@ class ScriptInvocation: script: pycardano.ScriptType datum: Optional[pycardano.Datum] redeemer: Union[pycardano.Redeemer, pycardano.RedeemerMap] - script_context: ScriptContext + script_context: Union[ScriptContextV1, ScriptContextV2] def generate_script_contexts(tx_builder: pycardano.TransactionBuilder): @@ -298,7 +91,7 @@ def generate_script_contexts_resolved( resolved_reference_inputs: List[UTxO], posix_from_slot, ): - tx_info = to_tx_info( + tx_info_args = ( tx, [i.output for i in resolved_inputs], [i.output for i in resolved_reference_inputs], @@ -323,10 +116,7 @@ def generate_script_contexts_resolved( raise ValueError( f"Missing redeemer for script input {i} (index or tag set incorrectly or missing redeemer)" ) - potential_scripts = tx.transaction_witness_set.plutus_v2_script or [] - for input in resolved_reference_inputs + resolved_inputs: - if input.output.script is not None: - potential_scripts.append(input.output.script) + script_type = None try: spending_script = next( s @@ -334,11 +124,25 @@ def generate_script_contexts_resolved( if plutus_script_hash(PlutusV2Script(s)) == spending_input.output.address.payment_part ) + script_type = ScriptType.PlutusV2 + except (StopIteration, TypeError): - raise NotImplementedError( - "Can not validate spending of non plutus v2 script (or plutus v2 script is not in context)" - ) + try: + spending_script = next( + s + for s in tx.transaction_witness_set.plutus_v1_script + if plutus_script_hash(PlutusV1Script(s)) + == spending_input.output.address.payment_part + ) + script_type = ScriptType.PlutusV1 + except Exception as e: + raise NotImplementedError( + f"Can not validate spending of non plutus v1 or v2 script (or plutus v1 or v2 script is not in context)" + ) if spending_input.output.datum is not None: + assert ( + script_type != ScriptType.PlutusV1 + ), "Only datum hash is supported for plutus v1 scripts" datum = spending_input.output.datum elif spending_input.output.datum_hash is not None: datum_h = spending_input.output.datum_hash @@ -356,12 +160,25 @@ def generate_script_contexts_resolved( raise ValueError( "Spending input is missing an attached datum and can not be spent" ) + + if script_type is ScriptType.PlutusV1: + script_context = to_spending_script_context_v1( + tx_info_args, spending_input.input + ) + elif script_type is ScriptType.PlutusV2: + script_context = to_spending_script_context_v2( + tx_info_args, spending_input.input + ) + else: + raise NotImplementedError() + script_contexts.append( ScriptInvocation( spending_script, datum, spending_redeemer, - ScriptContext(tx_info, Spending(to_tx_out_ref(spending_input.input))), + script_context, + # ScriptContext(tx_info, Spending(to_tx_out_ref(spending_input.input))), ) ) for i, minting_script_hash in enumerate(tx.transaction_body.mint or []): @@ -378,26 +195,41 @@ def generate_script_contexts_resolved( raise ValueError( f"Missing redeemer for mint {i} (index or tag set incorrectly or missing redeemer)" ) - try: - minting_script = next( - s - for s in tx.transaction_witness_set.plutus_v2_script + minting_script, script_type = next( + ( + (s, ScriptType.PlutusV1) + for s in tx.transaction_witness_set.plutus_v1_script or [] if plutus_script_hash(PlutusV2Script(s)) == minting_script_hash + ), + (None, None), + ) + if not minting_script: + minting_script, script_type = ( + next( + ( + (s, ScriptType.PlutusV2) + for s in tx.transaction_witness_set.plutus_v2_script or [] + if plutus_script_hash(PlutusV2Script(s)) == minting_script_hash + ), + (minting_script, script_type), + ) + if not minting_script + else minting_script ) - except StopIteration: - raise NotImplementedError( - "Can not validate spending of non plutus v2 script (or plutus v2 script is not in context)" - ) + + assert ( + minting_script and script_type + ), f"Can not validate spending of non plutus v1 or v2 scripts (or plutus v1 or v2 script is not in context)" + + if script_type == ScriptType.PlutusV1: + script_context = to_minting_script_context_v1(tx_info_args, minting_script) + elif script_type == ScriptType.PlutusV2: + script_context = to_minting_script_context_v2(tx_info_args, minting_script) + else: + raise NotImplementedError() script_contexts.append( - ScriptInvocation( - minting_script, - datum, - minting_redeemer, - ScriptContext( - tx_info, Minting(pycardano.script_hash(minting_script).payload) - ), - ) + ScriptInvocation(minting_script, datum, minting_redeemer, script_context) ) for i, certificate in enumerate(tx.transaction_body.certificates or []): try: @@ -416,24 +248,39 @@ def generate_script_contexts_resolved( raise ValueError( f"Missing redeemer for certificate {i} (index or tag set incorrectly or missing redeemer)" ) - try: - certificate_script = next( - s - for s in tx.transaction_witness_set.plutus_v2_script + + certificate_script, script_type = next( + ( + (s, ScriptType.PlutusV1) + for s in tx.transaction_witness_set.plutus_v1_script or [] + if plutus_script_hash(PlutusV1Script(s)) + == certificate.stake_credential.credential + ), + (None, None), + ) + certificate_script, script_type = next( + ( + (s, ScriptType.PlutusV2) + for s in tx.transaction_witness_set.plutus_v2_script or [] if plutus_script_hash(PlutusV2Script(s)) == certificate.stake_credential.credential - ) - except StopIteration: - raise NotImplementedError( - "Can not validate spending of non plutus v2 script (or plutus v2 script is not in context)" - ) + ), + (certificate_script, script_type), + ) + assert ( + certificate_script and script_type + ), "Can not validate spending of non plutus v1 or v2 scripts (or plutus v1 or v2 script is not in context)" + + if script_type == ScriptType.PlutusV1: + script_context = to_certificate_script_context_v1(tx_info_args, certificate) + elif script_type == ScriptType.PlutusV2: + script_context = to_certificate_script_context_v2(tx_info_args, certificate) + else: + raise NotImplementedError() script_contexts.append( ScriptInvocation( - certificate_script, - datum, - certificate_redeemer, - ScriptContext(tx_info, Certifying(to_dcert(certificate))), + certificate_script, datum, certificate_redeemer, script_context ) ) for i, address in enumerate(sorted(tx.transaction_body.withdraws or {})): @@ -451,23 +298,36 @@ def generate_script_contexts_resolved( f"Missing redeemer for withdrawal {i} (index or tag set incorrectly or missing redeemer)" ) script_hash = pycardano.Address.from_primitive(address).staking_part - try: - withdrawal_script = next( - s - for s in tx.transaction_witness_set.plutus_v2_script + withdrawal_script, script_type = next( + ( + (s, ScriptType.PlutusV1) + for s in tx.transaction_witness_set.plutus_v1_script or [] + if plutus_script_hash(PlutusV1Script(s)) == script_hash + ), + (None, None), + ) + withdrawal_script, script_type = next( + ( + (s, ScriptType.PlutusV2) + for s in tx.transaction_witness_set.plutus_v2_script or [] if plutus_script_hash(PlutusV2Script(s)) == script_hash - ) - except StopIteration: - raise NotImplementedError( - "Can not validate spending of non plutus v2 script (or plutus v2 script is not in context)" - ) + ), + (withdrawal_script, script_type), + ) + assert ( + withdrawal_script and script_type + ), "Can not validate spending of non plutus v1 or v2 scripts (or plutus v1 or v2 script is not in context)" + + if script_type == ScriptType.PlutusV1: + script_context = to_withdrawal_script_context_v1(tx_info_args, script_hash) + elif script_type == ScriptType.PlutusV2: + script_context = to_withdrawal_script_context_v2(tx_info_args, script_hash) + else: + raise NotImplementedError("Only Plutus V1 and V2 scripts are supported.") script_contexts.append( ScriptInvocation( - withdrawal_script, - datum, - withdrawal_redeemer, - ScriptContext(tx_info, Rewarding(to_staking_hash(script_hash))), + withdrawal_script, datum, withdrawal_redeemer, script_context ) ) diff --git a/tests/fortytwo.py b/tests/fortytwo.py new file mode 100644 index 0000000..c6c8d46 --- /dev/null +++ b/tests/fortytwo.py @@ -0,0 +1,61 @@ +import pathlib + +import cbor2 +import pycardano +from pycardano import ChainContext +from plutus_bench.tool import load_contract, ScriptType, address_from_script + + +def give( + payment_key: pycardano.PaymentSigningKey, + script: pycardano.PlutusV1Script, + context: ChainContext, + give_value: int, +): + network = context.network + script_address = pycardano.Address(pycardano.script_hash(script), network=network) + payment_vkey_address = payment_key.to_verification_key().hash() + payment_address = pycardano.Address( + payment_part=payment_vkey_address, network=network + ) + + builder = pycardano.TransactionBuilder(context) + builder.add_input_address(payment_address) + datum = pycardano.Unit() + builder.add_output( + pycardano.TransactionOutput( + script_address, give_value, datum_hash=pycardano.datum_hash(datum) + ) + ) + signed_tx = builder.build_and_sign( + [ + payment_key, + ], + payment_address, + ) + context.submit_tx(signed_tx) + + +def take( + taker_key: pycardano.PaymentSigningKey, + script: pycardano.PlutusV1Script, + redeemer: pycardano.Redeemer, + context: ChainContext, + value: int, +): + network = context.network + script_address = pycardano.Address(pycardano.script_hash(script), network=network) + taker_address = pycardano.Address( + taker_key.to_verification_key().hash(), network=network + ) + + utxo_to_spend = context.utxos(script_address)[0] + datum = pycardano.Unit() + + builder = pycardano.TransactionBuilder(context) + builder.add_input_address(taker_address) + builder.add_script_input(utxo_to_spend, script, datum, redeemer) + builder.add_output(pycardano.TransactionOutput(taker_address, value)) + + signed_tx = builder.build_and_sign([taker_key], taker_address) + context.submit_tx(signed_tx) diff --git a/tests/gift.py b/tests/gift.py index 65139d6..4a122f4 100644 --- a/tests/gift.py +++ b/tests/gift.py @@ -12,9 +12,11 @@ def spend_from_gift_contract( context: ChainContext, enforce_true_owner: bool = True, set_required_signers: bool = True, + redeemer: pycardano.Redeemer = None, + script_type: pycardano.ScriptType = ScriptType.PlutusV2, ): network = context.network - gift_contract = load_contract(gift_contract_path, ScriptType.PlutusV2) + gift_contract = load_contract(gift_contract_path, script_type) script_address = address_from_script(gift_contract, network) payment_vkey_hash = payment_key.to_verification_key().hash() payment_address = pycardano.Address(payment_part=payment_vkey_hash, network=network) @@ -42,7 +44,7 @@ def spend_from_gift_contract( spend_utxo, gift_contract, None, - pycardano.Redeemer(0), + pycardano.Redeemer(0) if not redeemer else redeemer, ) tx = txbuilder.build_and_sign( signing_keys=[payment_key], diff --git a/tests/test_fortytwo_v1.py b/tests/test_fortytwo_v1.py new file mode 100644 index 0000000..6ef2e56 --- /dev/null +++ b/tests/test_fortytwo_v1.py @@ -0,0 +1,36 @@ +import pathlib + +import pycardano +import pytest +from pycardano import TransactionFailedException + +from plutus_bench import MockChainContext, MockUser +from plutus_bench.mock import MockFrostApi + +from tests.fortytwo import * +from plutus_bench.tool import address_from_script, load_contract, ScriptType + +own_path = pathlib.Path(__file__) + + +def test_fortytwo_v1_script(): + api = MockFrostApi() + context = MockChainContext(api=api) + giver = MockUser(api) + giver.fund(100_000_000) + gift_contract_path = own_path.parent / "contracts/fortytwo.cbor" + with open(gift_contract_path, "r") as f: + script_hex = f.read() + forty_two_script = cbor2.loads(bytes.fromhex(script_hex)) + script = pycardano.PlutusV1Script(forty_two_script) + + give(giver.signing_key, script, context, 50_000_000) + + taker = MockUser(api) + taker.fund(14_000_000) # give collateral + + take(taker.signing_key, script, pycardano.Redeemer(42), context, 25_000_000) + + +if __name__ == "__main__": + test_fortytwo_v1_script()