packages feed

morley-1.19.1: src/Morley/Michelson/Runtime/RunCode.hs

-- SPDX-FileCopyrightText: 2022 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | Emulation of @run_code@.

module Morley.Michelson.Runtime.RunCode
  ( runCode
  , RunCodeParameters(..)
  , runCodeParameters
  , resolveRunCodeBigMaps
  ) where

import Data.Default (def)
import Data.Map qualified as Map

import Morley.Michelson.Interpret
import Morley.Michelson.Runtime.Dummy
import Morley.Michelson.Runtime.GState
import Morley.Michelson.TypeCheck
import Morley.Michelson.Typed qualified as T
import Morley.Michelson.Typed.Operation
import Morley.Michelson.Untyped qualified as U
import Morley.Tezos.Address
import Morley.Tezos.Core (ChainId, Mutez, Timestamp(..), dummyChainId, zeroMutez)
import Morley.Tezos.Crypto (KeyHash)

----------------------------------------------------------------------------
-- Auxiliary types
----------------------------------------------------------------------------

-- | Data required for calling 'runCode'.
type RunCodeParameters :: T.T -> T.T -> T.T -> Type
data RunCodeParameters cp epArg st = RunCodeParameters
  { rcScript :: T.Contract cp st
    -- ^ Contract code to run
  , rcStorage :: T.Value st
    -- ^ Initial contract storage
  , rcInput :: T.Value epArg
    -- ^ Parameter to call the contract
  , rcEntryPoint :: T.EntrypointCallT cp epArg
    -- ^ Entrypoint to call. Use 'T.mkEntrypointCall' to construct.
  , rcAmount :: Mutez
    -- ^ Transfer amount
  , rcBalance :: Mutez
    -- ^ Contract initial balance
  , rcChainId :: ChainId
    -- ^ Chain id
  , rcNow :: Timestamp
    -- ^ The result of @NOW@ instruction
  , rcLevel :: Natural
    -- ^ The result of @LEVEL@ instruction
  , rcMinBlockTime :: Natural
    -- ^ The result of @MIN_BLOCK_TIME@ instruction
  , rcSource :: L1Address
    -- ^ Transfer source
  , rcSender :: L1Address
    -- ^ Transfer sender
  , rcKnownContracts :: Map ContractAddress ContractState
    -- ^ Known contracts and their state. If you only know parameter types and
    -- don't need to run contract's views, you can use 'dummyContractState' to
    -- construct the state.
  , rcSelf :: Maybe ContractAddress
    -- ^ Address returned by the @SELF@ instruction, will be auto-generated if
    -- 'Nothing'
  , rcDelegate :: Maybe KeyHash
    -- ^ Contract's delegate
  , rcVotingPowers :: VotingPowers
    -- ^ Voting powers
  }

-- | Construct 'RunCodeParameters' with some reasonable defaults.
--
-- Prepare untyped storage and parameter with 'resolveRunCodeBigMaps'.
--
-- Use 'T.mkEntrypointCall' or 'T.mkDefEntrypointCall' to construct the entrypoint
-- call specification.
runCodeParameters
  :: T.Contract cp st -- ^ Contract to run
  -> T.Value st -- ^ Contract storage
  -> T.EntrypointCallT cp epArg -- ^ Entrypoint call specification
  -> T.Value epArg -- ^ Entrypoint argument
  -> RunCodeParameters cp epArg st
runCodeParameters rcScript rcStorage rcEntryPoint rcInput = RunCodeParameters
  { rcAmount = zeroMutez
  , rcBalance = zeroMutez
  , rcChainId = dummyChainId
  , rcNow = dummyNow
  , rcLevel = dummyLevel
  , rcMinBlockTime = dummyMinBlockTime
  , rcSource = Constrained genesisAddress
  , rcSender = Constrained genesisAddress
  , rcKnownContracts = mempty
  , rcSelf = Nothing
  , rcDelegate = Nothing
  , rcVotingPowers = dummyVotingPowers
  , ..
  }

-- | Emulate @run_code@ RPC endpoint to an extent. Unlike @runContract@, runs
-- the contract through the emulator directly, without doing any operations.
-- This includes not doing the origination operation, and not applying the
-- operations produced by the transfer.
runCode
  :: RunCodeParameters cp epArg st
  -> Either (InterpretError Void) ([T.Operation], T.Value st)
runCode (RunCodeParameters
  -- NB: explicit match to ensure all fields are consumed; ugly, but there are no real alternatives
  rcScript@T.Contract{}
  rcStorage
  rcInput
  rcEntryPoint
  rcAmount
  rcBalance
  rcChainId
  rcNow
  rcLevel
  rcMinBlockTime
  rcSource
  rcSender
  rcKnownContracts
  rcSelf
  rcDelegate
  rcVotingPowers
  ) = fmap (extractValOps . rslResult) . handleReturn $
    interpret rcScript rcEntryPoint input storage dummyGlobalCounter bigMapCtr contractEnv
  where
    selfState = ContractState
      { csBalance = rcBalance
      , csContract = rcScript
      , csStorage = rcStorage
      , csDelegate = rcDelegate
      }
    dummyOriginationHash = mkOriginationOperationHash $
      dummyOrigination rcStorage rcScript dummyGlobalCounter
    self = fromMaybe
      (mkContractAddress dummyOriginationHash dummyGlobalCounter)
      rcSelf
    ((input, storage), bigMapCtr) = usingState dummyBigMapCounter $
      (,) <$> assignBigMapIds False rcInput <*> assignBigMapIds False rcStorage
    contractMap = Map.insert self selfState rcKnownContracts
    contractEnv = ContractEnv
      { ceNow = rcNow
      , ceBalance = rcBalance
      , ceSelf = self
      , ceAmount = rcAmount
      , ceMinBlockTime = rcMinBlockTime
      , ceContracts = pure . flip Map.lookup contractMap
      , ceMaxSteps = dummyMaxSteps
      , ceSource = rcSource
      , ceSender = rcSender
      , ceChainId = rcChainId
      , ceOperationHash = Nothing
      , ceLevel = rcLevel
      , ceErrorSrcPos = def
      , ceVotingPowers = rcVotingPowers
      , ceMetaWrapper = id
      }

-- | Given an untyped value, possibly containing @big_map@ ids, typecheck it,
-- resolving ids to the corresponding @big_map@s.
--
-- 'BigMapFinder' can be constructed using 'Morley.Michelson.Runtime.mkBigMapFinder'.
resolveRunCodeBigMaps
  :: T.SingI t => BigMapFinder -> U.Value -> Either TcError (T.Value t)
resolveRunCodeBigMaps = typeCheckingWith def{tcStrict=False} ... typeCheckValueRunCodeCompat