diff --git a/plutus_bench/ledger/api_v1.py b/plutus_bench/ledger/api_v1.py new file mode 100644 index 0000000..ea5eefd --- /dev/null +++ b/plutus_bench/ledger/api_v1.py @@ -0,0 +1,452 @@ +""" +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, Tuple + +from pycardano import Datum as Anything, PlutusData + + +# Plutus V1 +@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 + + +# 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 NoDatumHash(PlutusData): + """ + Indicates that there is no datum associated with an output + """ + + CONSTR_ID = 1 + + +@dataclass(unsafe_hash=True) +class SomeDatumHash(PlutusData): + """ + Indicates that there is an datum associated with an output, which has the given hash + """ + + CONSTR_ID = 0 + datum_hash: DatumHash + + +@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: Union[NoDatumHash, SomeDatumHash] # Changes in V2 to allow inline datum + # reference_script: Union[NoScriptHash, SomeScriptHash] # added in V2 + + +@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 DatumPair(PlutusData): + CONSTR_ID = 0 + datumHash: DatumHash + datum: Datum + + +@dataclass(unsafe_hash=True) +class WdrlPair(PlutusData): + CONSTR_ID = 0 + stakingCredential: StakingCredential + amount: int + + +@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] # Added in V2 + outputs: List[TxOut] + fee: Value + mint: Value + dcert: List[DCert] + wdrl: List[WdrlPair] # Changes to AssocMap in V2 + valid_range: POSIXTimeRange + signatories: List[PubKeyHash] + # redeemers: Dict[ScriptPurpose, Redeemer] # Added in V2 + data: List[DatumPair] # Changes to AssocMap in V2 + 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 diff --git a/plutus_bench/mock.py b/plutus_bench/mock.py index 082c6ac..cf59947 100644 --- a/plutus_bench/mock.py +++ b/plutus_bench/mock.py @@ -228,7 +228,9 @@ def is_witnessed( 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) @@ -308,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..69881dc --- /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 [WdrlPair(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} + # ), + [DatumPair(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/assets/alwayspass.plutus b/tests/assets/alwayspass.plutus new file mode 100644 index 0000000..9dc9cd4 --- /dev/null +++ b/tests/assets/alwayspass.plutus @@ -0,0 +1 @@ +58385836010000322225335333573466ebc008dd4240a82440042440022008264c649319ab9c49010b6578706563746564203432000041200101 \ No newline at end of file diff --git a/tests/assets/fortytwo.plutus b/tests/assets/fortytwo.plutus new file mode 100644 index 0000000..5c3b0f2 --- /dev/null +++ b/tests/assets/fortytwo.plutus @@ -0,0 +1 @@ +59082459082101000033232332233223232333222323332223233333333222222223233322232333322223232332232333222323332223232332233223232333332222233223322332233223322332222223232533530313330063333573466e1cd55ce9baa0054800081248c98d4c120cd5ce0260248238231bad0043333573466e1cd55cea8012400046601264646464646464646464646666ae68cdc39aab9d500a480008cccccccccc05ccd40948c8c8cccd5cd19b8735573aa004900011980e981c1aba15002302a357426ae8940088c98d4c160cd5ce02e02c82b82b09aab9e5001137540026ae854028cd4094098d5d0a804999aa8163ae502b35742a010666aa058eb940acd5d0a80399a8128209aba15006335025335505204a75a6ae854014c8c8c8cccd5cd19b8735573aa0049000119a80f9919191999ab9a3370e6aae7540092000233502733504075a6ae854008c114d5d09aba25002232635305c3357380c00ba0b60b426aae7940044dd50009aba150023232323333573466e1cd55cea80124000466a04a66a080eb4d5d0a80118229aba135744a004464c6a60b866ae7018017416c1684d55cf280089baa001357426ae8940088c98d4c160cd5ce02e02c82b82b09aab9e5001137540026ae854010cd4095d71aba15003335025335505275c40026ae854008c0dcd5d09aba2500223263530543357380b00aa0a60a426ae8940044d5d1280089aba25001135744a00226ae8940044d5d1280089aba25001135744a00226aae7940044dd50009aba150023232323333573466e1d400520062301c3039357426aae79400c8cccd5cd19b875002480108c06cc10cd5d09aab9e500423333573466e1d400d20022301b302e357426aae7940148cccd5cd19b875004480008c078dd71aba135573ca00c464c6a609e66ae7014c14013813413012c1284d55cea80089baa001357426ae8940088c98d4c120cd5ce026024823823082409931a982399ab9c49010350543500048046135573ca00226ea80048848cc00400c0088004888888888848cccccccccc00402c02802402001c01801401000c00880048848cc00400c008800448848cc00400c0084800448848cc00400c0084800448848cc00400c00848004848888c010014848888c00c014848888c008014848888c004014800448c88c008dd6000990009aa81a911999aab9f0012500e233500d30043574200460066ae880080cc8c8c8c8cccd5cd19b8735573aa006900011998039919191999ab9a3370e6aae754009200023300d303135742a00466a02605a6ae84d5d1280111931a981b99ab9c03b038036035135573ca00226ea8004d5d0a801999aa805bae500a35742a00466a01eeb8d5d09aba25002232635303333573806e06806406226ae8940044d55cf280089baa00122212333001004003002200122123300100300220011335500175ceb44488c88c008dd5800990009aa81791191999aab9f0022500923350083355031300635573aa004600a6aae794008c010d5d100181709aba10011122002122122330010040031200112232323333573466e1d400520002350083005357426aae79400c8cccd5cd19b87500248008940208c98d4c0a8cd5ce01701581481401389aab9d500113754002242446004006224400224002464646666ae68cdc39aab9d5002480008cc018c01cd5d0a8011bad357426ae8940088c98d4c090cd5ce01401281181109aab9e50011375400244246600200600440024646666ae68cdc39aab9d5001480008dd71aba135573ca004464c6a604066ae7009008407c0784dd500089119191999ab9a3370ea00290021280391999ab9a3370ea004900111a80518031aba135573ca00846666ae68cdc3a801a40004a014464c6a604666ae7009c09008808408007c4d55cea80089baa00112122230030041122200211222001120012323333573466e1d40052002200623333573466e1d400920002006232635301b33573803e03803403203026aae74dd50008910010910009000919191919191999ab9a3370ea0029006100591999ab9a3370ea0049005100691999ab9a3370ea00690041198059bae35742a00a6eb4d5d09aba2500523333573466e1d4011200623300d375c6ae85401cdd71aba135744a00e46666ae68cdc3a802a400846602460286ae854024dd71aba135744a01246666ae68cdc3a8032400446028602a6ae84d55cf280591999ab9a3370ea00e900011809980b1aba135573ca018464c6a604066ae7009008407c07807407006c0680640604d55cea80209aab9e5003135573ca00426aae7940044dd500090911111118038041109111111198030048041091111111802804091111110020911111100191091111111980100480411091111111980080480410009191919191999ab9a3370ea002900111998041bad35742a0086eb4d5d0a8019bad357426ae89400c8cccd5cd19b875002480008c028c02cd5d09aab9e5006232635301133573802a02402001e01c26aae75400c4d5d1280089aab9e5001137540024244600400644424466600200a0080064002464646666ae68cdc3a800a40044600c6eb8d5d09aab9e500323333573466e1d4009200023008375c6ae84d55cf280211931a980599ab9c00f00c00a009008135573aa00226ea80048488c00800c8488c00400c800444888c8c8cccd5cd19b8735573aa0049000119aa80598031aba150023005357426ae8940088c98d4c020cd5ce00600480380309aab9e5001137540029309000900088910919800801801089000a4903505431001123230010012233003300200200133322222253353004333573466e1c00920540060051006133573892010e77726f6e672072656465656d65720000512200212200120011 \ No newline at end of file diff --git a/tests/gift.plutus b/tests/assets/gift.plutus similarity index 100% rename from tests/gift.plutus rename to tests/assets/gift.plutus diff --git a/tests/assets/vesting_v1.plutus b/tests/assets/vesting_v1.plutus new file mode 100644 index 0000000..8a1614f --- /dev/null +++ b/tests/assets/vesting_v1.plutus @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV1", + "description": "", + "cborHex": "590add590ada01000032323232323232323233223233223232323322323322323232323232323332223232323223232223232533532323232533553353232350022222222222533533355301812001321233001225335002210031001002502925335333573466e3c0300040cc0c84d40ac004540a800c840cc40c4d401488009400440984cd5ce2491f62656e65666963696172792773207369676e6174757265206d697373696e670002515335323232350022235002223500522350022253335333501900b00600215335001153350051333501800b00300710301333501800b00300710301333501800b00300735500322222222220043350143350163502435005220010273350155023027123333333300122333573466e1c0080040a40a0894cd4ccd5cd19b87002001029028101515335333573466e240080040a40a0404c405088ccd5cd19b8800200102902822333573466e240080040a40a088ccd5cd19b8900200102802922333573466e200080040a00a4894cd4ccd5cd19b8900200102902810011002225335333573466e240080040a40a04008400440984cd5ce248114646561646c696e65206e6f742072656163686564000251025135001220023333573466e1cd55cea8022400046644246600200600464646464646464646464646666ae68cdc39aab9d500a480008cccccccccc888888888848cccccccccc00402c02802402001c01801401000c008cd40848c8c8cccd5cd19b8735573aa004900011991091980080180118171aba150023026357426ae8940088c98c80eccd5ce01e81e01c89aab9e5001137540026ae854028cd4084088d5d0a804999aa8123ae502335742a010666aa048eb9408cd5d0a80399a8108191aba15006335021335503503375a6ae854014c8c8c8cccd5cd19b8735573aa0049000119a8111919191999ab9a3370e6aae7540092000233502833503475a6ae854008c0dcd5d09aba2500223263203f33573808208007a26aae7940044dd50009aba150023232323333573466e1cd55cea80124000466a04e66a068eb4d5d0a801181b9aba135744a004464c6407e66ae701041000f44d55cf280089baa001357426ae8940088c98c80eccd5ce01e81e01c89aab9e5001137540026ae854010cd4085d71aba15003335021335503575c40026ae854008c0acd5d09aba2500223263203733573807207006a26ae8940044d5d1280089aba25001135744a00226ae8940044d5d1280089aba25001135744a00226aae7940044dd50009aba150043232323333573466e1d40052006232122223004005302a357426aae79400c8cccd5cd19b875002480108c848888c008014c0b0d5d09aab9e500423333573466e1d400d20022321222230010053024357426aae7940148cccd5cd19b875004480008c848888c00c014dd71aba135573ca00c464c6406466ae700d00cc0c00bc0b80b44d55cea80089baa001357426ae8940108c98c80accd5ce0168160149999ab9a3370e6aae74dd5002a40004056464c6405466ae700b00ac0a0cccd5cd19b8735573aa00c90001199109198008018011bae35742a00c6eb4d5d09aba2500623263202933573805605404e2052264c6405066ae7124010350543500029135573ca00226ea80044d55cf280089baa0013200135502322112225335001135003220012213335005220023004002333553007120010050040011222350022235003225333533350080070040021533500310011020101f102012223232323253335006215333500621533350082130044984c00d261533350072130044984c00d26100d100b1533350072130044984c00d261533350062130044984c00d26100c1533350052100a100b100915333500521533350072130054984c011261533350062130054984c01126100c100a1533350062130054984c011261533350052130054984c01126100b2533350052153335007215333500721333500b00a002001161616100b153335006215333500621333500a009002001161616100a10092533350042153335006215333500621333500a009002001161616100a1533350052153335005213335009008002001161616100910082533350032153335005215333500521333500900800200116161610091533350042153335004213335008007002001161616100810072533350022153335004215333500421333500800700200116161610081533350032153335003213335007006002001161616100710061235001222222220071222003122200212220011221233001003002122123300100300212212330010030021232230023758002640026aa034446666aae7c004940248cd4020c010d5d080118019aba200201a23232323333573466e1cd55cea801a40004666444246660020080060046464646666ae68cdc39aab9d5002480008cc8848cc00400c008c06cd5d0a80119a80700d1aba135744a004464c6403c66ae7008007c0704d55cf280089baa00135742a006666aa00eeb94018d5d0a80119a8053ae357426ae8940088c98c8068cd5ce00e00d80c09aba25001135573ca00226ea80044cd54005d73ad112232230023756002640026aa03044646666aae7c008940208cd401ccd54064c018d55cea80118029aab9e500230043574400603226ae840044488008488488cc00401000c488c8c8cccd5cd19b875001480008d401cc014d5d09aab9e500323333573466e1d400920022500723263201633573803002e02802626aae7540044dd50008909118010018891000919191999ab9a3370e6aae7540092000233221233001003002300535742a0046eb4d5d09aba2500223263201133573802602401e26aae7940044dd50009191999ab9a3370e6aae75400520002375c6ae84d55cf280111931900799ab9c01101000d1375400224464646666ae68cdc3a800a40084a00c46666ae68cdc3a8012400446a010600c6ae84d55cf280211999ab9a3370ea00690001091100111931900919ab9c01401301000f00e135573aa00226ea8004484888c00c010448880048c8cccd5cd19b8750014800880148cccd5cd19b8750024800080148c98c8030cd5ce00700680500489aab9d3754002244004244002464646464646666ae68cdc3a800a401842444444400646666ae68cdc3a8012401442444444400846666ae68cdc3a801a40104664424444444660020120106eb8d5d0a8029bad357426ae8940148cccd5cd19b875004480188cc8848888888cc008024020dd71aba15007375c6ae84d5d1280391999ab9a3370ea00a900211991091111111980300480418061aba15009375c6ae84d5d1280491999ab9a3370ea00c900111909111111180380418069aba135573ca01646666ae68cdc3a803a400046424444444600a010601c6ae84d55cf280611931900919ab9c01401301000f00e00d00c00b00a135573aa00826aae79400c4d55cf280109aab9e5001137540024646464646666ae68cdc3a800a4004466644424466600200a0080066eb4d5d0a8021bad35742a0066eb4d5d09aba2500323333573466e1d4009200023212230020033008357426aae7940188c98c802ccd5ce00680600480409aab9d5003135744a00226aae7940044dd5000919191999ab9a3370ea002900111909118008019bae357426aae79400c8cccd5cd19b875002480008c8488c00800cdd71aba135573ca008464c6401066ae700280240180144d55cea80089baa0011122232323333573466e1cd55cea80124000466aa010600c6ae854008c014d5d09aba2500223263200833573801401200c26aae7940044dd5000a4c22442466002006004240029210350543100112323001001223300330020020011" +} diff --git a/tests/assets/vesting_v2.plutus b/tests/assets/vesting_v2.plutus new file mode 100644 index 0000000..b5fbb8d --- /dev/null +++ b/tests/assets/vesting_v2.plutus @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV2", + "description": "", + "cborHex": "590b30590b2d0100003232323322323233223232323232323233223233223232323232323232333222323232322323222323253353232323253355335323235002222222222222533533355301a12001321233001225335002210031001002502c25335333573466e3c0380040ec0e84d40b8004540b4010840ec40e4d401488009400440b04cd5ce2491f62656e65666963696172792773207369676e6174757265206d697373696e670002b15335323232350022235002223500522350022253335333501900b00600215335001153350051333501800b00300710361333501800b00300710361333501800b00300735500322222222222200533501433501635029350052200102d335015502802d123333333300122333573466e1c0080040bc0b8894cd4ccd5cd19b8700200102f02e101515335333573466e240080040bc0b8404c405088ccd5cd19b8800200102f02e22333573466e240080040bc0b888ccd5cd19b8900200102e02f22333573466e200080040b80bc894cd4ccd5cd19b8900200102f02e10011002225335333573466e240080040bc0b84008400440b04cd5ce248114646561646c696e65206e6f7420726561636865640002b102b135001220023333573466e1cd55cea80224000466442466002006004646464646464646464646464646666ae68cdc39aab9d500c480008cccccccccccc88888888888848cccccccccccc00403403002c02802402001c01801401000c008cd408c090d5d0a80619a8118121aba1500b33502302535742a014666aa04eeb94098d5d0a804999aa813bae502635742a01066a0460606ae85401cccd5409c0c5d69aba150063232323333573466e1cd55cea80124000466a0486464646666ae68cdc39aab9d5002480008cd40a8cd40edd69aba15002303e357426ae8940088c98c8100cd5ce02182101f09aab9e5001137540026ae854008c8c8c8cccd5cd19b8735573aa0049000119a81499a81dbad35742a004607c6ae84d5d1280111931902019ab9c04304203e135573ca00226ea8004d5d09aba2500223263203c33573807e07c07426aae7940044dd50009aba1500533502375c6ae854010ccd5409c0b48004d5d0a801999aa813bae200135742a004605e6ae84d5d1280111931901c19ab9c03b03a036135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d55cf280089baa00135742a008603e6ae84d5d1280211931901519ab9c02d02c0283333573466e1cd55ce9baa0054800080ac8c98c80a4cd5ce0160158139999ab9a3370e6aae7540192000233221233001003002375c6ae854018dd69aba135744a00c464c6405066ae700ac0a809840a44c98c809ccd5ce2490350543500029135573ca00226ea80044d55cf280089baa00132001355023221122253350011350032200122133350052200230040023335530071200100500400112223500222350032253335333500800700400215335003100110261025102612223232323253335006215333500621533350082130044984c00d261533350072130044984c00d26100d100b1533350072130044984c00d261533350062130044984c00d26100c1533350052100a100b100915333500521533350072130054984c011261533350062130054984c01126100c100a1533350062130054984c011261533350052130054984c01126100b2533350052153335007215333500721333500b00a002001161616100b153335006215333500621333500a009002001161616100a10092533350042153335006215333500621333500a009002001161616100a1533350052153335005213335009008002001161616100910082533350032153335005215333500521333500900800200116161610091533350042153335004213335008007002001161616100810072533350022153335004215333500421333500800700200116161610081533350032153335003213335007006002001161616100710061235001222222220071222003122200212220011221233001003002122123300100300212212330010030021232230023758002640026aa034446666aae7c004940288cd4024c010d5d080118019aba200201a232323333573466e1cd55cea80124000466442466002006004601c6ae854008c014d5d09aba2500223263201833573803603402c26aae7940044dd50009191919191999ab9a3370e6aae75401120002333322221233330010050040030023232323333573466e1cd55cea80124000466442466002006004602e6ae854008cd403c058d5d09aba2500223263201d33573804003e03626aae7940044dd50009aba150043335500875ca00e6ae85400cc8c8c8cccd5cd19b875001480108c84888c008010d5d09aab9e500323333573466e1d4009200223212223001004375c6ae84d55cf280211999ab9a3370ea00690001091100191931900f99ab9c02202101d01c01b135573aa00226ea8004d5d0a80119a805bae357426ae8940088c98c8064cd5ce00e00d80b89aba25001135744a00226aae7940044dd5000899aa800bae75a224464460046eac004c8004d5405c88c8cccd55cf80112804119a8039991091980080180118031aab9d5002300535573ca00460086ae8800c0604d5d080088910010910911980080200189119191999ab9a3370ea002900011a80398029aba135573ca00646666ae68cdc3a801240044a00e464c6402866ae7005c0580480444d55cea80089baa0011212230020031122001232323333573466e1d400520062321222230040053007357426aae79400c8cccd5cd19b875002480108c848888c008014c024d5d09aab9e500423333573466e1d400d20022321222230010053007357426aae7940148cccd5cd19b875004480008c848888c00c014dd71aba135573ca00c464c6402466ae7005405004003c0380344d55cea80089baa001232323333573466e1cd55cea80124000466442466002006004600a6ae854008dd69aba135744a004464c6401c66ae700440400304d55cf280089baa0012323333573466e1cd55cea800a400046eb8d5d09aab9e500223263200c33573801e01c01426ea80048c8c8c8c8c8cccd5cd19b8750014803084888888800c8cccd5cd19b875002480288488888880108cccd5cd19b875003480208cc8848888888cc004024020dd71aba15005375a6ae84d5d1280291999ab9a3370ea00890031199109111111198010048041bae35742a00e6eb8d5d09aba2500723333573466e1d40152004233221222222233006009008300c35742a0126eb8d5d09aba2500923333573466e1d40192002232122222223007008300d357426aae79402c8cccd5cd19b875007480008c848888888c014020c038d5d09aab9e500c23263201533573803002e02602402202001e01c01a26aae7540104d55cf280189aab9e5002135573ca00226ea80048c8c8c8c8cccd5cd19b875001480088ccc888488ccc00401401000cdd69aba15004375a6ae85400cdd69aba135744a00646666ae68cdc3a80124000464244600400660106ae84d55cf280311931900719ab9c01101000c00b135573aa00626ae8940044d55cf280089baa001232323333573466e1d400520022321223001003375c6ae84d55cf280191999ab9a3370ea004900011909118010019bae357426aae7940108c98c802ccd5ce00700680480409aab9d50011375400224464646666ae68cdc3a800a40084a00c46666ae68cdc3a8012400446a010600c6ae84d55cf280211999ab9a3370ea00690001091100111931900619ab9c00f00e00a009008135573aa00226ea8004484888c00c010448880048c8cccd5cd19b8750014800880188cccd5cd19b8750024800080188c98c8018cd5ce00480400200189aab9d37540029309100109100089000a490350543100112323001001223300330020020011" +} diff --git a/tests/fortytwo.py b/tests/fortytwo.py new file mode 100644 index 0000000..b37564a --- /dev/null +++ b/tests/fortytwo.py @@ -0,0 +1,68 @@ +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, + datum=pycardano.Unit(), +): + 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) + 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, + datum=pycardano.Unit(), +): + 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] + non_nft_utxo = None + for utxo in context.utxos(taker_address): + if not utxo.output.amount.multi_asset: + non_nft_utxo = utxo + break + + 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)) + + builder.collaterals.append(non_nft_utxo) + + 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..d1b9b2a --- /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 / "assets/fortytwo.plutus" + 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() diff --git a/tests/test_gift.py b/tests/test_gift.py index e6ab737..d96547b 100644 --- a/tests/test_gift.py +++ b/tests/test_gift.py @@ -18,7 +18,7 @@ def test_spend_from_gift_contract(): context = MockChainContext(api=api) payment_key = MockUser(api) payment_key.fund(100_000_000) - gift_contract_path = own_path.parent / "gift.plutus" + gift_contract_path = own_path.parent / "assets/gift.plutus" gift_address = address_from_script( load_contract(gift_contract_path, ScriptType.PlutusV2), network=context.network ) diff --git a/tests/test_stake_blockfrost.py b/tests/test_stake_blockfrost.py new file mode 100644 index 0000000..beb509c --- /dev/null +++ b/tests/test_stake_blockfrost.py @@ -0,0 +1,92 @@ +import pathlib +from multiprocessing import Process +from time import sleep + +import pycardano +import pytest +import uvicorn +from pycardano import TransactionFailedException +from starlette.testclient import TestClient + +from plutus_bench import MockChainContext, MockUser, MockPool +from plutus_bench.mock import MockFrostApi + +from tests.stake import register_and_delegate, withdraw +from pycardano.crypto.bech32 import decode +from opshin import build +from plutus_bench.mockfrost.client import MockFrostClient, MockFrostUser, MockFrostPool +from plutus_bench.mockfrost.server import app + +own_path = pathlib.Path(__file__) + + +def run_server(): + uvicorn.run(app, port=8000) + + +@pytest.fixture +def server(): + proc = Process(target=run_server, args=(), daemon=True) + proc.start() + sleep(1) # Wait for server to start + yield + proc.kill() # Cleanup after test + + +def test_register_and_delegate(server): + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() + staking_user = MockFrostUser(session) + + # api = MockFrostApi() + # context = MockChainContext(api=api) + # staking_user = MockUser(api) + # stake_pool = MockPool(api) + stake_pool = MockFrostPool(session) + + staking_user.fund(100_000_000_000) + script_path = own_path.parent / "contracts/unrealistic_staking.py" + plutus_script = build(script_path, bytes(staking_user.verification_key.hash())) + register_and_delegate( + staking_user.signing_key, plutus_script, stake_pool.pool_id, context + ) + + +def test_withdraw(server): + # api = MockFrostApi() + # context = MockChainContext(api=api) + # staking_user = MockUser(api) + # recipient_user = MockUser(api) + # stake_pool = MockPool(api) + client = MockFrostClient(base_url="http://127.0.0.1:8000") + session = client.create_session() + context = session.chain_context() + staking_user = MockFrostUser(session) + recipient_user = MockFrostUser(session) + stake_pool = MockFrostPool(session) + + staking_user.fund(100_000_000_000) + + script_path = own_path.parent / "contracts/unrealistic_staking.py" + plutus_script = build(script_path, bytes(recipient_user.verification_key.hash())) + stake_info = register_and_delegate( + staking_user.signing_key, plutus_script, stake_pool.pool_id, context + ) + + # Withdraw + withdraw( + recipient_user.address, + 1000000, + staking_user.signing_key, + plutus_script, + context, + ) + + stake_address = stake_info["stake_address"] + script_payment_address = stake_info["script_payment_address"] + + +if __name__ == "__main__": + test_register_and_delegate() + test_withdraw() diff --git a/tests/test_vesting_v1.py b/tests/test_vesting_v1.py new file mode 100644 index 0000000..f5a7bcf --- /dev/null +++ b/tests/test_vesting_v1.py @@ -0,0 +1,79 @@ +import pathlib +from dataclasses import dataclass +import pycardano +import pytest +from pycardano import TransactionFailedException + +from plutus_bench import MockChainContext, MockUser +from plutus_bench.mock import MockFrostApi + +from tests.vesting import * +from plutus_bench.tool import address_from_script, load_contract, ScriptType + +import time + +own_path = pathlib.Path(__file__) + + +@dataclass() +class VestingDatum(pycardano.PlutusData): + CONSTR_ID = 0 + beneficiary: bytes + deadline: int + + +@dataclass() +class VestingRedeemer(pycardano.PlutusData): + CONSTR_ID = 0 + + +def test_vesting_v1_script(): + api = MockFrostApi() + context = MockChainContext(api=api) + giver = MockUser(api) + giver.fund(100_000_000) + + taker = MockUser(api) + taker.fund(5_000_000) # collateral + + vesting_script = load_contract( + own_path.parent / "assets/vesting_v1.plutus", ScriptType.PlutusV1 + ) + script = pycardano.PlutusV1Script(cbor2.loads(vesting_script)) + + current_time = time.time() + + datum = VestingDatum( + beneficiary=bytes(taker.verification_key.hash()), + deadline=int(current_time) * 1000, # must be in milliseconds + ) + + give(giver.signing_key, script, context, 50_000_000, datum) + + redeemer = pycardano.Redeemer(VestingRedeemer()) + param = api.genesis_param + system_start, slot_length = param.system_start, param.slot_length + + # Valid range will be set to last_block_slot-1000 by default + # Should fail with offset = 999 and pass with offset 1000 + offset = 999 + api.set_block_slot(int((current_time - system_start) / slot_length) + offset) + + pytest.raises( + TransactionFailedException, + take, + taker.signing_key, + script, + redeemer, + context, + 25_000_000, + datum=datum, + ) + + offset = 1000 + api.set_block_slot(int((current_time - system_start) / slot_length) + offset) + take(taker.signing_key, script, redeemer, context, 25_000_000, datum=datum) + + +if __name__ == "__main__": + test_vesting_v1_script() diff --git a/tests/test_vesting_v2.py b/tests/test_vesting_v2.py new file mode 100644 index 0000000..42b7f30 --- /dev/null +++ b/tests/test_vesting_v2.py @@ -0,0 +1,78 @@ +import pathlib +from dataclasses import dataclass +import pycardano +import pytest +from pycardano import TransactionFailedException + +from plutus_bench import MockChainContext, MockUser +from plutus_bench.mock import MockFrostApi + +from tests.vesting import * +from plutus_bench.tool import address_from_script, load_contract, ScriptType + +import time + +own_path = pathlib.Path(__file__) + + +@dataclass() +class VestingDatum(pycardano.PlutusData): + CONSTR_ID = 0 + beneficiary: bytes + deadline: int + + +@dataclass() +class VestingRedeemer(pycardano.PlutusData): + CONSTR_ID = 0 + + +def test_vesting_v2_script(): + api = MockFrostApi() + context = MockChainContext(api=api) + giver = MockUser(api) + giver.fund(100_000_000) + + taker = MockUser(api) + taker.fund(5_000_000) # collateral + + vesting_script = load_contract( + own_path.parent / "assets/vesting_v2.plutus", ScriptType.PlutusV2 + ) + script = pycardano.PlutusV2Script(cbor2.loads(vesting_script)) + current_time = time.time() + + datum = VestingDatum( + beneficiary=bytes(taker.verification_key.hash()), + deadline=int(current_time) * 1000, # must be in milliseconds + ) + + give(giver.signing_key, script, context, 50_000_000, datum) + + redeemer = pycardano.Redeemer(VestingRedeemer()) + param = api.genesis_param + system_start, slot_length = param.system_start, param.slot_length + + # Valid range will be set to last_block_slot-1000 by default + # Should fail with offset = 999 and pass with offset 1000 + offset = 999 + api.set_block_slot(int((current_time - system_start) / slot_length) + offset) + + pytest.raises( + TransactionFailedException, + take, + taker.signing_key, + script, + redeemer, + context, + 25_000_000, + datum=datum, + ) + + offset = 1000 + api.set_block_slot(int((current_time - system_start) / slot_length) + offset) + take(taker.signing_key, script, redeemer, context, 25_000_000, datum=datum) + + +if __name__ == "__main__": + test_vesting_v2_script() diff --git a/tests/vesting.py b/tests/vesting.py new file mode 100644 index 0000000..b37564a --- /dev/null +++ b/tests/vesting.py @@ -0,0 +1,68 @@ +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, + datum=pycardano.Unit(), +): + 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) + 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, + datum=pycardano.Unit(), +): + 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] + non_nft_utxo = None + for utxo in context.utxos(taker_address): + if not utxo.output.amount.multi_asset: + non_nft_utxo = utxo + break + + 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)) + + builder.collaterals.append(non_nft_utxo) + + signed_tx = builder.build_and_sign([taker_key], taker_address) + context.submit_tx(signed_tx)