-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA -- | Integration with integrational testing engine (pun intended). module Test.Cleveland.Internal.Pure ( PureM(..) , runClevelandT , runEmulatedT -- * Capability implementations , PureState , TestError(..) , emulatedImpl , clevelandOpsImpl , clevelandMiscImpl -- * Initial environment for Emulated tests , initEnv -- * Support functions , failedInsideBranch , moneybagAlias , emptyScenarioBranch -- * Optics , psAliases , psDefaultAliasesCounter , psRefillableAddresses , psNow , psLevel , psGState , psContractsNames ) where import Control.Lens (assign, at, makeLenses, modifying, to, (%=), (.=)) import Control.Monad.Catch.Pure (CatchT, runCatchT) import Control.Monad.Writer (MonadWriter, WriterT, listen, runWriterT, tell) import Data.Constraint (Dict(..), withDict, (\\)) import Data.Default (def) import Data.Map qualified as Map import Data.Monoid (Ap(..)) import Data.Set qualified as Set import Data.Singletons (sing) import Data.Type.Equality (type (:~:)(Refl)) import Fmt (Buildable(..), Builder, build, pretty, unlinesF, (+|), (|+)) import Time (Second, toNum, toUnit) import Lorentz (Mutez, NiceComparable, pattern DefEpName) import Lorentz qualified as L import Lorentz.Entrypoints (HasEntrypointArg, TrustEpName(..), useHasEntrypointArg) import Morley.AsRPC (HasRPCRepr(AsRPC), MaybeRPC(..), notesAsRPC, replaceBigMapIds, rpcStorageScopeEvi, valueAsRPC) import Morley.Client (Alias, OperationInfo(..), mkAlias) import Morley.Client.TezosClient.Types (unsafeCoerceAliasHintToAlias, unsafeGetAliasHintText) import Morley.Michelson.Interpret (InterpretError(..), InterpretResult(..), MichelsonFailed(..), MichelsonFailureWithStack(..)) import Morley.Michelson.Runtime hiding (ExecutorOp(..), transfer) import Morley.Michelson.Runtime qualified as Runtime (ExecutorOp(..)) import Morley.Michelson.Runtime.Dummy (dummyLevel, dummyMaxSteps, dummyNow, dummyOrigination) import Morley.Michelson.Runtime.GState (GState(..), asBalance, genesisAddress, genesisSecretKey, gsAddressesL, gsChainIdL, gsCounterL, gsVotingPowersL, initGState) import Morley.Michelson.TypeCheck (TCError(..), typeCheckContractAndStorage, typeCheckingWith) import Morley.Michelson.Typed (BigMapId(..), IsoValue, SingI, SomeAnnotatedValue(..), ToT, Value, Value'(..), castM, dfsFoldMapValue, fromVal, requireEq, toVal) import Morley.Michelson.Typed qualified as T import Morley.Michelson.Typed.Operation (OriginationOperation(..), TransferOperation(..)) import Morley.Michelson.Untyped qualified as U import Morley.Tezos.Address (Address, detGenKeyAddress) import Morley.Tezos.Core (Timestamp, timestampPlusSeconds, unsafeSubMutez, zeroMutez) import Morley.Tezos.Crypto (SecretKey(..), detSecretKey, sign, toPublic) import Morley.Util.MismatchError import Morley.Util.Named import Test.Cleveland.Internal.Abstract import Test.Cleveland.Internal.Exceptions (addCallStack, catchWithCallStack, throwWithCallStack) import Test.Cleveland.Lorentz import Test.Cleveland.Util (ceilingUnit) data PureState = PureState { _psAliases :: Aliases , _psDefaultAliasesCounter :: DefaultAliasCounter , _psRefillableAddresses :: Set Address , _psNow :: Timestamp , _psLevel :: Natural , _psGState :: GState , _psContractsNames :: Map Address Text -- ^ Map from contracts addresses to human-readable names. } deriving stock Show instance MonadState PureState PureM where get = ask >>= readIORef put = (ask >>=) . flip writeIORef data AddressName = AddressName (Maybe Text) Address deriving stock (Show) newtype PureM a = PureM { unPureM :: ReaderT (IORef PureState) (CatchT (WriterT LogsInfo IO)) a } deriving newtype (Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadReader (IORef PureState), MonadWriter LogsInfo, MonadFail) type Aliases = Map Alias AliasData -- | Datatype to store alias data, we store optional 'SecretKey' in addition -- to 'Address' in order to support bytes signing. data AliasData = AliasData { adAddress :: Address , adMbSecretKey :: Maybe SecretKey } deriving stock Show data TestError = UnexpectedTypeCheckError TCError | UnexpectedStorageType (MismatchError T.T) | UnexpectedBigMapKeyType (MismatchError T.T) | UnexpectedBigMapValueType (MismatchError T.T) | CustomTestError Text deriving stock Show makeLenses ''PureState instance Buildable AddressName where build (AddressName mbName addr) = build addr +| maybe "" (\cName -> " (" +|cName |+ ")") mbName instance Buildable TestError where build (UnexpectedTypeCheckError tcErr) = "Unexpected type check error. Reason: " +| tcErr |+ "" build (UnexpectedStorageType merr) = "Unexpected storage type.\n" +| merr |+ "" build (UnexpectedBigMapKeyType merr) = "Unexpected big map key type.\n" +| merr |+ "" build (UnexpectedBigMapValueType merr) = "Unexpected big map value type.\n" +| merr |+ "" build (CustomTestError msg) = pretty msg instance Exception TestError where displayException = pretty -- In this implementation we do not prefix aliases, so 'Alias' and 'AliasHint' -- are identical and conversions between them are safe. hintToAlias :: AliasHint -> Alias hintToAlias = unsafeCoerceAliasHintToAlias moneybagAlias :: Alias moneybagAlias = mkAlias "moneybag" runEmulatedT :: Alias -> EmulatedT PureM a -> IO a runEmulatedT moneybagAlias' scenario = runClevelandT moneybagAlias' do clevelandCaps <- ask lift . runReaderT scenario $ EmulatedCaps emulatedImpl clevelandCaps runClevelandT :: Alias -> ClevelandT PureM a -> IO a runClevelandT moneybagAlias' scenario = do let caps = ClevelandCaps { ccSender = Sender genesisAddress , ccMoneybag = Moneybag genesisAddress , ccMiscCap = clevelandMiscImpl , ccOpsCap = clevelandOpsImpl } let pureM = runReaderT scenario caps env <- newIORef (initEnv moneybagAlias') (res, _logs) <- runWriterT $ runCatchT $ runReaderT (unPureM pureM) env either throwM pure $ res emulatedImpl :: EmulatedImpl PureM emulatedImpl = EmulatedImpl { eiBranchout = \(scenarios :: [(Text, PureM ())]) -> forM_ scenarios $ \(name, scenario) -> do aliasesState <- get newRef <- newIORef aliasesState local (\_ -> newRef) scenario `catchWithCallStack` \originalCallStackMb err -> maybe throwM throwWithCallStack originalCallStackMb $ failedInsideBranch name err , eiGetStorage = addCallStack . exceptionHandler . getStorageImpl , eiGetMorleyLogs = getMorleyLogsImpl , eiSetVotingPowers = assign (psGState . gsVotingPowersL) } clevelandOpsImpl :: Sender -> ClevelandOpsImpl PureM clevelandOpsImpl (Sender sender) = mapClevelandOpsImplExceptions (addCallStack . exceptionHandler) ClevelandOpsImpl { coiRunOperationBatch = mapM \case OpOriginate UntypedOriginateData{..} -> do ref <- originate uodContract (pretty uodName) uodStorage uodBalance OpOriginate <$> saveAlias uodName (toAddress ref) Nothing OpTransfer TransferData{..} -> do let fromAddr = #from :! sender let toAddr = #to :! toAddress tdTo refillable <- isAddressRefillable sender when refillable $ do balance <- getBalance sender when (balance < tdAmount) $ do let moneybag = #from :! genesisAddress toSender = #to :! sender transfer @() moneybag toSender (unsafeSubMutez tdAmount balance) (TrustEpName DefEpName) () -- Here @toAddr@ is 'Address', so we can not check anything -- about it and assume that entrypoint is correct. We pass -- unit as contract parameter because it won't be checked -- anyway. transfer @() fromAddr toAddr tdAmount (TrustEpName tdEntrypoint) tdParameter return $ OpTransfer () OpReveal{} -> do -- We do not care about reveals in our Morley runtime return $ OpReveal () } clevelandMiscImpl :: ClevelandMiscImpl PureM clevelandMiscImpl = mapClevelandMiscImplExceptions (addCallStack . exceptionHandler) ClevelandMiscImpl { cmiRunIO = \action -> liftIO (try action) >>= \case Right res -> pure res Left (err :: SomeException) -> throwM err , cmiResolveAddress = resolve , cmiSignBytes = \bs addr -> do -- TODO [#248]: make sure this performs fast alias <- getAlias addr aliases <- use psAliases let mbMbSk = Map.lookup alias aliases mbSk <- maybe (unknownAlias alias) (pure . adMbSecretKey) mbMbSk case mbSk of Nothing -> cmiFailure $ "Given address doesn't have known associated secret key: " <> build alias Just sk -> liftIO $ sign sk bs , cmiGenKey = \alias -> do aliasHint <- resolveSpecificOrDefaultAliasHint alias smartGenKey Nothing aliasHint , cmiGenFreshKey = \alias -> do aliasHint <- resolveSpecificOrDefaultAliasHint alias aliases <- use psAliases let mbSk = Map.lookup (hintToAlias aliasHint) aliases smartGenKey (adAddress <$> mbSk) aliasHint , cmiOriginateLargeUntyped = originateUntyped -- Comments are not supported by integrational testing engine (yet). , cmiComment = const pass , cmiGetPublicKey = \addr -> do aliases <- use psAliases let mbAliasInfo = fmap snd $ find (\(_, AliasData addr' _) -> addr == addr') (Map.toList aliases) aliasInfo <- maybe (unknownAddress addr) pure mbAliasInfo case adMbSecretKey aliasInfo of Nothing -> cmiFailure $ "Given address doesn't have known associated public key: " <> build addr Just sk -> pure $ toPublic sk , cmiGetDelegate = \addr -> do ContractState _ _ _ delegate <- contractStorage addr pure delegate , cmiRegisterDelegate = const pass , cmiGetChainId = use $ psGState . gsChainIdL , cmiAdvanceTime = \time -> do modifying psNow . flip timestampPlusSeconds $ toNum @Second @Integer $ ceilingUnit $ toUnit @Second time , cmiAdvanceToLevel = \fn -> -- do not go back in levels modifying psLevel (\cl -> max (fn cl) cl) , cmiGetNow = use psNow , cmiGetLevel = use psLevel , cmiGetApproximateBlockInterval = pure $ sec 1 , cmiAttempt = try , cmiThrow = throwM , cmiMarkAddressRefillable = setAddressRefillable , cmiGetBalance = getBalance , cmiEmulatedImpl = pure $ Just emulatedImpl , .. } where cmiFailure :: forall a. Builder -> PureM a cmiFailure = throwM . CustomTestError . pretty setAddressRefillable addr = psRefillableAddresses %= Set.insert addr originateUntyped :: Sender -> UntypedOriginateData -> PureM Address originateUntyped _ UntypedOriginateData {..} = do ref <- originate uodContract (pretty uodName) uodStorage uodBalance saveAlias uodName (toAddress ref) Nothing cmiGetBigMapValueMaybe :: forall k v. (NiceComparable k, IsoValue v) => BigMapId k v -> k -> PureM (Maybe v) cmiGetBigMapValueMaybe (BigMapId bmId) k = runMaybeT do VBigMap _ bigMap <- findBigMapByIdMaybe @(ToT k) @(ToT v) bmId hoistMaybe $ fromVal @v <$> Map.lookup (toVal k) bigMap cmiGetAllBigMapValuesMaybe :: forall k v. (NiceComparable k, IsoValue v) => BigMapId k v -> PureM (Maybe [v]) cmiGetAllBigMapValuesMaybe (BigMapId bmId) = runMaybeT do VBigMap _ bigMap <- findBigMapByIdMaybe @(ToT k) @(ToT v) bmId pure $ fromVal @v <$> Map.elems bigMap findBigMapById :: forall k v. (SingI v, SingI k) => Natural -> PureM (Value ('T.TBigMap k v)) findBigMapById bigMapId = runMaybeT (findBigMapByIdMaybe @k @v bigMapId) >>= maybe notFound pure where notFound = cmiFailure $ "BigMap with ID " <> build bigMapId <> " not found." -- | Traverse storage values of all contracts and looks for a big_map with the given ID. -- If multiple big_maps with the given ID are found, it fails with error. findBigMapByIdMaybe :: forall k v. (SingI v, SingI k) => Natural -> MaybeT PureM (Value ('T.TBigMap k v)) findBigMapByIdMaybe bigMapId = MaybeT do addresses <- use $ psGState . gsAddressesL . to Map.elems let Ap result = flip foldMap addresses \case ASContract ContractState{csStorage} -> findBigMapInStorage csStorage bigMapId ASSimple {} -> Ap $ Right [] case result of -- The RPC does not distinguish between "the bigmap does not exist" -- and "the bigmap exists, but the key doesn't", so we mimic the RPC's behaviour here. -- We simply return `Nothing` in both cases. Right [] -> pure Nothing Right [bigMap] -> pure $ Just bigMap Right bigMaps -> error $ pretty $ unlinesF @_ @Builder [ "Expected all big_maps to have unique IDs, but found " +| length bigMaps |+ " big_maps with the ID " +| bigMapId |+ "." , "This is most likely a bug." ] Left (err :: TestError) -> throwM err -- | Traverse a storage value and looks for a big_map with the given ID. -- If multiple big_maps with the given ID are found, they'll all be returned. findBigMapInStorage :: forall k v st. (SingI k, SingI v) => Value st -> Natural -> Ap (Either TestError) [Value ('T.TBigMap k v)] findBigMapInStorage storage bigMapId = dfsFoldMapValue (\v -> case v of VBigMap (Just bigMapId') (_ :: Map (Value k') (Value v')) | bigMapId == bigMapId' -> do Refl <- requireEq @k' @k (Ap . Left ... UnexpectedBigMapKeyType) Refl <- requireEq @v' @v (Ap . Left ... UnexpectedBigMapValueType) pure [v] _ -> Ap $ Right [] ) storage -- | In a real chain, when we retrieve a contract's storage via the Tezos RPC, -- the storage expression will have all the big_maps replaced with their respective big_map IDs. -- -- Here, we mimic the RPC's behaviour. -- -- We expect all big_maps in the storage to already have an ID. -- IDs are assigned to big_maps by the interpreter/runtime when: -- * A contract with big_maps in its storage is originated -- * A transfer is made and the parameter contains big_maps -- * A contract's code is run and it calls `EMPTY_BIG_MAP`, `DUP` or `DUP n`. cmiGetSomeStorage :: Address -> PureM SomeAnnotatedValue cmiGetSomeStorage addr = do ContractState _ contract (storage :: Value t) _ <- contractStorage addr pure $ SomeAnnotatedValue (notesAsRPC $ T.cStoreNotes contract) (valueAsRPC storage) \\ rpcStorageScopeEvi @t getAlias :: Address -> PureM Alias getAlias addr = do aliases <- use psAliases let maybeAlias = (fmap fst . find (\(_, AliasData addr' _) -> addr == addr') . Map.toList) aliases maybe (unknownAddress addr) pure maybeAlias -- Generate a fresh address which was never generated for given alias. -- If the address is not saved, we use the alias as its seed. -- Otherwise we concatenate the alias with the saved address. smartGenKey :: Maybe Address -> AliasHint -> PureM Address smartGenKey existingAddr aliasHint@(unsafeGetAliasHintText -> aliasTxt) = let seed = maybe aliasTxt (mappend aliasTxt . pretty) existingAddr sk = detSecretKey (encodeUtf8 seed) addr = detGenKeyAddress (encodeUtf8 seed) in saveAlias aliasHint addr $ Just sk resolveSpecificOrDefaultAliasHint (SpecificAliasHint aliasHint) = return aliasHint resolveSpecificOrDefaultAliasHint (DefaultAliasHint) = do DefaultAliasCounter counter <- use psDefaultAliasesCounter psDefaultAliasesCounter %= \(DefaultAliasCounter i) -> DefaultAliasCounter $ i + 1 return $ mkDefaultAlias counter cmiRunCode :: forall cp st vd. (HasRPCRepr st, T.IsoValue (AsRPC st)) => Sender -> RunCode cp st vd -> PureM (AsRPC st) cmiRunCode (Sender sender) (RunCode rcContract rcStorage rcParameter rcAmount rcBalance rcSource) = do -- Pattern match on the contract constructor to reveal -- a proof of `NiceParameter cp` and `NiceStorage st` L.Contract{} <- pure rcContract param <- maybeRPCToVal rcParameter storage <- maybeRPCToVal rcStorage res <- interpret do counter0 <- use $ esGState . gsCounterL contractAddr <- executeOrigination ! #isGlobalOp True $ (dummyOrigination storage (L.toMichelsonContract rcContract) counter0) { ooBalance = zeroMutez } \\ L.niceStorageEvi @st \\ L.niceParameterEvi @cp esSourceAddress .= rcSource counter1 <- use $ esGState . gsCounterL let overrideContractBalance = Just rcBalance executeTransfer (#isGlobalOp :! False) overrideContractBalance def $ TransferOperation { toDestination = contractAddr , toCounter = counter1 , toTxData = TxData { tdSenderAddress = sender , tdParameter = TxTypedParam param \\ L.niceParameterEvi @cp , tdEntrypoint = DefEpName , tdAmount = rcAmount } } pure contractAddr case res of Left executorError -> throwEE executorError Right (executorRes, contractAddr) -> do -- Find the storage of the contract and return it in its RPC representation. case executorRes ^. erGState . gsAddressesL . at contractAddr of Nothing -> cmiFailure $ "Internal error: failed to find contract: '" +| contractAddr |+ "'" Just (ASSimple {}) -> cmiFailure $ "Internal error: expected address to belong to a contract: '" +| contractAddr |+ "'" Just (ASContract (ContractState _ _ (finalStorage :: Value actualSt) _)) -> do finalStorage' <- castM @actualSt @(ToT st) finalStorage (throwM . UnexpectedStorageType) pure $ fromVal $ valueAsRPC finalStorage' where maybeRPCToVal :: MaybeRPC v -> PureM (Value (ToT v)) maybeRPCToVal = \case NotRPC v -> pure $ toVal v IsRPC v -> replaceBigMapIds findBigMapById sing $ toVal v isAddressRefillable :: Address -> PureM Bool isAddressRefillable addr = Set.member addr <$> use psRefillableAddresses getBalance :: Address -> PureM Mutez getBalance addr = do GState{..} <- use psGState return $ maybe zeroMutez asBalance $ Map.lookup addr gsAddresses saveAlias :: AliasHint -> Address -> Maybe SecretKey -> PureM Address saveAlias name addr mbSk = do psAliases %= Map.insert (hintToAlias name) (AliasData addr mbSk) pure addr exceptionHandler :: PureM a -> PureM a exceptionHandler action = try action >>= \case Left err -> exceptionToTransferFailure err >>= throwM Right res -> return res where exceptionToTransferFailure :: ExecutorError' AddressName -> PureM TransferFailure exceptionToTransferFailure err = case err of EEZeroTransaction addr -> return $ TransferFailure (addrNameToAddr addr) EmptyTransaction EEIllTypedParameter addr _ -> return $ TransferFailure (addrNameToAddr addr) BadParameter EEUnexpectedParameterType addr _ -> return $ TransferFailure (addrNameToAddr addr) BadParameter EEInterpreterFailed addr (InterpretError (MichelsonFailureWithStack{..}, _)) -> case mfwsFailed of MichelsonFailedWith val -> return $ TransferFailure (addrNameToAddr addr) $ FailedWith (EOTVTypedValue val) (Just mfwsInstrCallStack) MichelsonArithError (T.ShiftArithError{}) -> return $ TransferFailure (addrNameToAddr addr) ShiftOverflow MichelsonArithError (T.MutezArithError errType _ _) -> return $ TransferFailure (addrNameToAddr addr) $ MutezArithError errType MichelsonGasExhaustion -> return $ TransferFailure (addrNameToAddr addr) GasExhaustion _ -> throwM err _ -> throwM err getMorleyLogsImpl :: PureM a -> PureM (LogsInfo, a) getMorleyLogsImpl action = swap <$> listen action getStorageImpl :: forall st addr. (ToStorageType st addr) => addr -> PureM st getStorageImpl addr = do withDict (pickNiceStorage @st addr) $ do ContractState _ _ (storage :: Value actualT) _ <- contractStorage (toAddress addr) val <- castM @actualT @(ToT st) storage (throwM . UnexpectedStorageType) pure $ T.fromVal val -- Attempt to retrieve a ContractState given for the given address. Fails if the -- address is unknown or the address is a simple address (contract without -- code and storage). contractStorage :: Address -> PureM ContractState contractStorage addr = do GState{..} <- use psGState case Map.lookup addr gsAddresses of Just (ASContract contractState) -> pure contractState Just (ASSimple {}) -> throwM . CustomTestError $ "Expected address to be contract with storage, but it's a simple address: " <> pretty addr Nothing -> unknownAddress addr resolve :: Alias -> PureM Address resolve name = do aliases <- use psAliases let maybeAddress = Map.lookup name aliases maybe (unknownAlias name) (pure . adAddress) maybeAddress unknownAddress :: Address -> PureM whatever unknownAddress = throwM . CustomTestError . mappend "Unknown address provided: " . pretty unknownAlias :: Alias -> PureM whatever unknownAlias = throwM . CustomTestError . mappend "Unknown address alias: " . pretty -- | Make branch names for a case when we are not within any branch. emptyScenarioBranch :: ScenarioBranchName emptyScenarioBranch = ScenarioBranchName [] -- | Add a new branch element to names provided by inner -- 'Test.Cleveland.branchout' calls. appendScenarioBranch :: Text -> ScenarioBranchName -> ScenarioBranchName appendScenarioBranch brName (ScenarioBranchName branches) = ScenarioBranchName (brName : branches) ---------------------------------------------------------------------------- -- Support functions ---------------------------------------------------------------------------- initAliases :: Alias -> Aliases initAliases alias = one ( alias , AliasData genesisAddress $ Just $ genesisSecretKey ) initEnv :: Alias -> PureState initEnv alias = PureState { _psAliases = initAliases alias , _psDefaultAliasesCounter = DefaultAliasCounter 0 , _psRefillableAddresses = Set.empty , _psNow = dummyNow , _psLevel = dummyLevel , _psGState = initGState , _psContractsNames = Map.empty } failedInsideBranch :: Text -> SomeException -> FailedInBranch failedInsideBranch name err = case fromException @FailedInBranch err of Just (FailedInBranch branch failure) -> FailedInBranch (appendScenarioBranch name branch) failure Nothing -> FailedInBranch (appendScenarioBranch name emptyScenarioBranch) err ---------------------------------------------------------------------------- -- Emulator internals ---------------------------------------------------------------------------- transfer :: forall cp vd epRef epArg addr. (HasEntrypointArg cp epRef epArg, T.IsoValue epArg, L.ToTAddress cp vd addr) => "from" :! Address -> "to" :! addr -> Mutez -> epRef -> epArg -> PureM () transfer (arg #from -> from) (arg #to -> toAddr) money epRef param = let L.TAddress to' = L.toTAddress @cp @vd toAddr in case useHasEntrypointArg @cp @epRef @epArg epRef of (Dict, epName) -> registerInterpretation do executeGlobalOperations def $ one $ Runtime.TransferOp $ TransferOperation { toDestination = to' , toCounter = 0 , toTxData = TxData { tdSenderAddress = from , tdParameter = withDict (T.properParameterEvi @(ToT epArg)) $ TxTypedParam (T.toVal param) , tdEntrypoint = epName , tdAmount = money } } -- | Originate a contract with given initial storage and balance. Its -- address is returned. originate :: U.Contract -> Text -> U.Value -> Mutez -> PureM Address originate uContract contractName uStorage balance = case typeCheckingWith def $ typeCheckContractAndStorage uContract uStorage of Left tcErr -> throwM $ UnexpectedTypeCheckError tcErr Right (T.SomeContractAndStorage contract storage) -> do counter <- gsCounter <$> use psGState address <- registerInterpretation . executeGlobalOrigination $ (dummyOrigination storage contract counter) { ooBalance = balance } psContractsNames %= Map.insert address contractName return address throwEE :: ExecutorError -> PureM a throwEE err = do st <- get throwM $ flip addrToAddrName st <$> err -- | Runs a set of operations and updates the engine's state. registerInterpretation :: ExecutorM a -> PureM a registerInterpretation action = do interpretedResult <- interpret action tell $ extractLogs interpretedResult case interpretedResult of Right (executorRes, res) -> do psGState .= _erGState executorRes pure res Left executorError -> throwEE executorError where extractLogs :: Either ExecutorError (ExecutorRes, a) -> [ScenarioLogs] extractLogs = \case Left (EEInterpreterFailed addr (InterpretError e)) -> [ScenarioLogs addr $ snd e] Right (res, _) -> res ^. erInterpretResults <&> \(addr, InterpretResult{..}) -> ScenarioLogs addr iurMorleyLogs _ -> [] -- | Interpret an action and return the result _without_ updating the engine's state. interpret :: ExecutorM a -> PureM (Either ExecutorError (ExecutorRes, a)) interpret action = do now <- use psNow level <- use psLevel gState <- use psGState pure $ runExecutorM now level dummyMaxSteps gState action addrToAddrName :: Address -> PureState -> AddressName addrToAddrName addr iState = AddressName (Map.lookup addr (iState ^. psContractsNames)) addr addrNameToAddr :: AddressName -> Address addrNameToAddr (AddressName _ addr) = addr