Source code for pymavryk.contract.call

from decimal import Decimal
from pprint import pformat
from typing import Any
from typing import Dict
from typing import Optional
from typing import Union

from deprecation import deprecated  # type: ignore

from pymavryk.context.impl import ExecutionContext
from pymavryk.context.mixin import ContextMixin
from pymavryk.contract.result import ContractCallResult
from pymavryk.jupyter import get_class_docstring
from pymavryk.logging import logger
from pymavryk.michelson.format import micheline_to_michelson
from pymavryk.michelson.repl import Interpreter
from pymavryk.michelson.sections.storage import StorageSection
from pymavryk.operation import DEFAULT_BURN_RESERVE
from pymavryk.operation import DEFAULT_GAS_RESERVE
from pymavryk.operation.content import format_mumav
from pymavryk.operation.content import format_tez
from pymavryk.operation.group import OperationGroup


def skip_nones(**kwargs) -> dict:
    return {k: v for k, v in kwargs.items() if v is not None}


[docs]class ContractCall(ContextMixin): """Proxy class encapsulating a contract call: contract type scheme, contract address, parameters, and amount""" def __init__(self, context: ExecutionContext, parameters: dict, amount: Union[int, Decimal] = 0) -> None: super().__init__(context=context) self.parameters = parameters self.amount = amount def __repr__(self) -> str: res = [ super().__repr__(), f'.amount\t{self.amount}', '\nParameters', pformat(self.parameters), '\nHelpers', get_class_docstring(self.__class__), ] return '\n'.join(res)
[docs] def with_amount(self, amount: Union[int, Decimal]) -> 'ContractCall': """Set amount of funds to send with transaction to the contract. :param amount: amount in microtez (int) or mav (Decimal) :rtype: ContractCall """ return ContractCall( context=self.context, parameters=self.parameters, amount=amount, )
[docs] def as_transaction(self, **kwargs) -> OperationGroup: """Get operation content. :rtype: OperationGroup """ return OperationGroup(context=self._spawn_context()).transaction( destination=self.address, amount=self.amount, parameters=self.parameters, **kwargs )
@property # type: ignore @deprecated(deprecated_in='3.0.0', removed_in='4.0.0', details='use `as_transaction()` instead') def operation_group(self) -> OperationGroup: return self.as_transaction().fill()
[docs] def send( self, gas_reserve: int = DEFAULT_GAS_RESERVE, burn_reserve: int = DEFAULT_BURN_RESERVE, min_confirmations: int = 0, ttl: Optional[int] = None, ) -> 'OperationGroup': """Fill, sign, and broadcast the transaction :param gas_reserve: Add a safe reserve for dynamically calculated gas limit (default is 100). :param burn_reserve: Add a safe reserve for dynamically calculated storage limit (default is 100). :param min_confirmations: number of block injections to wait for before returning (default is 0, i.e. async mode) :param ttl: Number of blocks to wait in the mempool before removal (default is 5 for public network, 60 for sandbox) :return: OperationGroup with hash filled """ return self.as_transaction().send( gas_reserve=gas_reserve, burn_reserve=burn_reserve, min_confirmations=min_confirmations, ttl=ttl, )
[docs] def send_async( self, ttl: int, counter: int, gas_limit: int, storage_limit: int, minimal_nanomav_per_gas_unit: Optional[int] = None, ) -> 'OperationGroup': """ Send operation without simulation or pre-validation :param ttl: Number of blocks to wait in the mempool before removal (default is 5 for public network, 60 for sandbox) :param counter: Set counter value :param gas_limit: Set gas_limit value :param storage_limit: Set storage_limit value :param minimal_nanomav_per_gas_unit: Override minimal_nanomav_per_gas_unit constant :rtype: OperationGroup """ return self.as_transaction().send_async( ttl=ttl, counter=counter, gas_limit=gas_limit, storage_limit=storage_limit, minimal_nanomav_per_gas_unit=minimal_nanomav_per_gas_unit, )
[docs] @deprecated(deprecated_in='3.2.2', removed_in='4.0.0', details='use `send()` instead') def inject(self, _async=True, preapply=True, check_result=True, num_blocks_wait=5) -> OperationGroup: """Send operation to blockchain.""" return ( self.as_transaction() .autofill() .sign() .inject( _async=_async, preapply=preapply, check_result=check_result, num_blocks_wait=num_blocks_wait, ) )
[docs] def cmdline(self) -> str: """Generate command line for octez-client.""" arg = micheline_to_michelson(self.parameters['value'], inline=True) source = self.key.public_key_hash() amount = format_tez(self.amount) entrypoint = self.parameters['entrypoint'] return f'transfer {amount} from {source} to {self.address} --entrypoint \'{entrypoint}\' --arg \'{arg}\''
[docs] def interpret( self, storage=None, source=None, sender=None, amount=None, balance=None, chain_id=None, level=None, now=None, self_address=None, view_results: Optional[Dict[str, Any]] = None, ) -> ContractCallResult: """Run code in the builtin REPL (WARNING! Not recommended for critical tasks). :param storage: initial storage as Python object, leave None if you want to generate a dummy one :param source: patch SOURCE :param sender: patch SENDER :param amount: patch AMOUNT :param balance: patch BALANCE :param chain_id: patch CHAIN_ID :param level: patch LEVEL :param now: patch NOW :param self_address: patch SELF/SELF_ADDRESS :param view_results: patch VIEW calls (keys must be string "address%view", values => Python objects) :rtype: pymavryk.contract.result.ContractCallResult """ storage_ty = StorageSection.match(self.context.storage_expr) if storage is None: initial_storage = storage_ty.dummy(self.context).to_micheline_value(lazy_diff=True) else: initial_storage = storage_ty.from_python_object(storage).to_micheline_value(lazy_diff=True) assert self.context.script operations, storage, lazy_diff, stdout, error = Interpreter.run_code( parameter=self.parameters['value'], entrypoint=self.parameters['entrypoint'], storage=initial_storage, script=self.context.script['code'], source=source, sender=sender or source, amount=amount or self.amount, balance=balance, chain_id=chain_id, level=level, now=now, address=self_address, view_results=view_results, ) if error: logger.debug('\n'.join(stdout)) raise error res = { 'operations': operations, 'storage': storage, 'lazy_storage_diff': lazy_diff, } return ContractCallResult.from_run_code( res, parameters=self.parameters, context=self.context, )
[docs] def run_code( self, storage=None, source=None, sender=None, amount=None, balance=None, chain_id=None, gas_limit=None, ) -> ContractCallResult: """Execute using RPC interpreter. :param storage: initial storage as Python object, leave None if you want to generate a dummy one :param source: patch SOURCE :param sender: patch SENDER :param amount: patch AMOUNT :param balance: patch BALANCE :param chain_id: patch CHAIN_ID :param gas_limit: restrict max consumed gas :rtype: ContractCallResult """ storage_ty = StorageSection.match(self.context.storage_expr) if storage is None: initial_storage = storage_ty.dummy(self.context).to_micheline_value(lazy_diff=True) else: initial_storage = storage_ty.from_python_object(storage).to_micheline_value(lazy_diff=True) script = [self.context.parameter_expr, self.context.storage_expr, self.context.code_expr] query = skip_nones( script=script, storage=initial_storage, entrypoint=self.parameters['entrypoint'], input=self.parameters['value'], amount=format_mumav(amount or self.amount), chain_id=chain_id or self.context.get_chain_id(), source=sender, payer=source, balance=str(balance or 0), gas=str(gas_limit) if gas_limit is not None else None, ) res = self.shell.blocks[self.block_id].helpers.scripts.run_code.post(query) return ContractCallResult.from_run_code(res, parameters=self.parameters, context=self.context)
[docs] def run_operation(self) -> ContractCallResult: """Simulate operation using real context. :rtype: ContractCallResult """ opg_with_metadata = self.as_transaction().fill().run() results = ContractCallResult.from_run_operation(opg_with_metadata, context=self.context) assert len(results) == 1 return results[0]
[docs] @deprecated(deprecated_in='3.0.0', removed_in='4.0.0', details='use either `run_code` or `run_operation`') def result(self, storage=None, source=None, sender=None, gas_limit=None) -> ContractCallResult: """Simulate operation and parse the result. :param storage: Python object only. If storage is specified, `run_code` is called instead of `run_operation`. :param source: Can be specified for unit testing purposes :param sender: Can be specified for unit testing purposes, \ see https://tezos.gitlab.io/whitedoc/michelson.html#operations-on-contracts for the difference :param gas_limit: Specify gas limit (default is gas hard limit) :rtype: ContractCallResult """ if storage or source or sender or gas_limit: return self.run_code(storage=storage, source=source, sender=sender, gas_limit=gas_limit) return self.run_operation()
[docs] def callback_view(self, storage=None): """Get return value of an on-chain callback method. :param storage: initial storage as Python object, if None then the current one will be taken (if context attached), otherwise empty (dummy) :returns: Decoded parameters of a callback """ if storage: storage_ty = StorageSection.match(self.context.storage_expr) initial_storage = storage_ty.from_python_object(storage).to_micheline_value(lazy_diff=True) elif self.address: initial_storage = self.shell.blocks[self.context.block_id].context.contracts[self.address].storage() else: storage_ty = StorageSection.match(self.context.storage_expr) initial_storage = storage_ty.dummy(self.context).to_micheline_value(lazy_diff=True) operations, _, stdout, error = Interpreter.run_callback( parameter=self.parameters['value'], entrypoint=self.parameters['entrypoint'], storage=initial_storage, context=self.context, ) if error: logger.debug('\n'.join(stdout)) raise error if not operations: raise Exception('No operation could be interpreted') if len(operations) > 1: raise Exception('Multiple internal operations, not sure which one to pick') return operations[0]
[docs] @deprecated(deprecated_in='3.0.4', removed_in='4.0.0', details='Use callback_view instead') def view(self): return self.callback_view()