packages feed

cleveland-0.2.0: test/TestSuite/Cleveland/RunCode.hs

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

-- Disable the warning generated by the `L.source` instruction.
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}

module TestSuite.Cleveland.RunCode
  ( test_runs_code
  , test_emulator_state_is_not_modified
  , test_other_operations_are_not_executed
  , test_handles_bigmap
  , test_handles_bigmap_ids
  , test_env_vars_can_be_observed
  , test_can_observe_existing_onchain_contracts_and_accounts
  , test_self_address_does_not_exist_onchain
  ) where

import Control.Lens (at)
import Debug qualified (show)
import Test.Tasty (TestTree)

import Lorentz (HasAnnotation)
import Lorentz qualified as L
import Lorentz.Base
import Lorentz.Value
import Morley.Michelson.Runtime.GState (gsAddressesL)
import Morley.Tezos.Core (Timestamp(..))
import Test.Cleveland
import Test.Cleveland.Internal.Pure (psGState)
import TestSuite.Util (idContract)

test_runs_code :: TestTree
test_runs_code =
  testScenario "runs the given contract's code" $ scenario do
    runCode RunCode
      { rcContract = increment
      , rcStorage = NotRPC 5
      , rcParameter = NotRPC 3
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      } @@== 8
  where
    increment :: Contract Integer Integer ()
    increment = L.defaultContract $
      L.unpair
      # L.add
      # L.nil # L.pair

test_emulator_state_is_not_modified :: TestTree
test_emulator_state_is_not_modified =
  testScenarioOnEmulator "the emulator's state is not modified" $ scenario do
    before <- get
    runCode RunCode
      { rcContract = transferToSender
      , rcStorage = NotRPC ()
      , rcParameter = NotRPC ()
      , rcAmount = 900
      , rcBalance = 900
      , rcSource = Nothing
      , rcNow = Just (Timestamp 1337)
      , rcLevel = Just 42
      }
    after <- get
    checkComparesWith Debug.show before (==) Debug.show after
  where
    transferToSender :: Contract () () ()
    transferToSender = L.defaultContract $
      L.drop
      # L.sender
      # L.contract @()
      # L.assertSome @MText "Address's parameter is not of type unit."
      # L.push @Mutez 10
      # L.push ()
      # L.transferTokens
      # L.dip (L.nil) # L.cons
      # L.dip (L.push ()) # L.pair

test_other_operations_are_not_executed :: TestTree
test_other_operations_are_not_executed =
  testScenario "other operations are not executed" $ scenario do
    -- If we originated the contract and called it, it would loop
    -- until it ran out of gas.

    -- But because `runCode` is not supposed to execute operations emitted by the first call,
    -- then the counter should only be increased to 1 and then stop.
    runCode RunCode
      { rcContract = unboundRecursion
      , rcStorage = NotRPC 0
      , rcParameter = NotRPC ()
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      } @@== 1
  where
    -- | A contract that calls itself forever, increment the counter in its storage by 1 on every call.
    unboundRecursion :: Contract () Integer ()
    unboundRecursion = L.defaultContract $
      L.cdr
      # L.push @Integer 1 # L.add
      # L.self @()
      # L.push @Mutez 0
      # L.push ()
      # L.transferTokens
      # L.dip L.nil # L.cons
      # L.pair

test_handles_bigmap :: TestTree
test_handles_bigmap =
  testScenario "handles bigmaps in parameter/storage" $ scenario do
    (total, bigMapId) <- runCode RunCode
      { rcContract = lookupBigMapKey
      , rcStorage = NotRPC ((0, 0), one ("key", 50))
      , rcParameter = NotRPC ("key", one ("key", 20))
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      }

    total @== (50, 20)
    -- Check that the bigmap was not actually created on-chain.
    getBigMapValueMaybe bigMapId "key" @@== Nothing

test_handles_bigmap_ids :: TestTree
test_handles_bigmap_ids =
  testScenario "handles bigmap IDs in parameter/storage" $ scenario do
    -- Originate a contract with a big_map in its storage
    -- in order to actually create a valid on-chain bigmap ID.
    handle <- originateSimple @() @(BigMap MText Integer) @() "bigmap" (one ("key", 30)) idContract
    bigMapId <- getStorage handle

    (total, _) <- runCode RunCode
      { rcContract = lookupBigMapKey
      , rcStorage = IsRPC ((0, 0), bigMapId)
      , rcParameter = IsRPC ("key", bigMapId)
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      }

    total @== (30, 30)

test_env_vars_can_be_observed :: TestTree
test_env_vars_can_be_observed =
  testScenario "environment variables can be observed" $ scenario do
    sender <- newFreshAddress auto
    source <- newFreshAddress auto

    envVars <- withSender sender $ runCode RunCode
      { rcContract = readEnvVars
      , rcStorage = NotRPC Nothing
      , rcParameter = NotRPC ()
      , rcAmount = 456
      , rcBalance = 123
      , rcSource = Just source
      , rcNow = Just (Timestamp 8)
      , rcLevel = Just 29
      } >>= evalJust "Expected contract to return a `Some`"

    envLevel envVars @== 29
    envNow envVars @== Timestamp 8
    getChainId @@== envChainId envVars
    envSender envVars @== sender
    envSource envVars @== source
    envAmount envVars @== 456
    envBalance envVars @== 123
  where
    readEnvVars :: Contract () (Maybe EnvVars) ()
    readEnvVars = L.defaultContract $
      L.drop
      # L.constructT @EnvVars
            ( L.fieldCtor $ L.level
            , L.fieldCtor $ L.now
            , L.fieldCtor $ L.chainId
            , L.fieldCtor $ L.sender
            , L.fieldCtor $ L.source
            , L.fieldCtor $ L.amount
            , L.fieldCtor $ L.balance
            )
      # L.some # L.nil # L.pair

data EnvVars = EnvVars
  { envLevel :: Natural
  , envNow :: Timestamp
  , envChainId :: ChainId
  , envSender :: Address
  , envSource :: Address
  , envAmount :: Mutez
  , envBalance :: Mutez
  }
  deriving stock (Generic)
  deriving anyclass (IsoValue, HasAnnotation)

instance HasRPCRepr EnvVars where type AsRPC EnvVars = EnvVars

test_can_observe_existing_onchain_contracts_and_accounts :: TestTree
test_can_observe_existing_onchain_contracts_and_accounts =
  testScenario "existing onchain contracts/acounts can be observed" $ scenario do
    contractAddress <- toAddress <$> originate OriginateData
      { odName = "contract"
      , odBalance = 123
      , odStorage = ()
      , odContract = idContract @Integer @()
      }

    let
      checkContractExists :: Contract () (Maybe Address) ()
      checkContractExists = L.defaultContract $
        L.drop
        -- Assert that the given address exists and belongs to a contract.
        # L.push contractAddress
        # L.contract @Integer
        # L.assertSome @MText "Expected to find a contract with parameter `Integer`."
        # L.address
        # L.some # L.nil # L.pair

    runCode RunCode
      { rcContract = checkContractExists
      , rcStorage = IsRPC Nothing
      , rcParameter = IsRPC ()
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      } @@== Just contractAddress

test_self_address_does_not_exist_onchain :: TestTree
test_self_address_does_not_exist_onchain =
  testScenarioOnEmulator "SELF address does not exist onchain" $ scenario do
    emulatorState <- use psGState

    let
      returnSelf :: Contract Integer (Maybe Address) ()
      returnSelf = L.defaultContract $
        L.drop
        -- Check `SELF` instruction succeeds
        # L.self @Integer
        # L.drop
        # L.selfAddress # L.contract @Integer
        # L.assertSome @MText "Expected to find a contract with parameter `Integer`."
        # L.address # L.some # L.nil # L.pair

    selfAddr <- runCode RunCode
      { rcContract = returnSelf
      , rcStorage = IsRPC Nothing
      , rcParameter = IsRPC 0
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      } >>= evalJust "Expected contract to return a `Some`"

    emulatorState ^. gsAddressesL . at selfAddr @== Nothing

----------------------------------------------------------------------------
-- Helpers
----------------------------------------------------------------------------

-- | Looks up an integer in:
--
-- * The big_map given in the parameter
-- * The big_map in the storage
--
-- ... and stores both results in the storage.
lookupBigMapKey :: Contract (MText, BigMap MText Integer) ((Integer, Integer), BigMap MText Integer) ()
lookupBigMapKey = L.mkContractWith L.intactCompilationOptions $ L.mkContractCode $
  L.unpair
  # L.dip L.cdr
  # L.unpair
  # L.stackType @'[MText, BigMap MText Integer, BigMap MText Integer]

  # L.dup
  # L.dip L.swap
  # L.stackType @'[MText, BigMap MText Integer, MText, BigMap MText Integer]

  -- Make a copy of the bigmap in the storage
  # L.dupN @4
  # L.dug @4
  # L.stackType @'[MText, BigMap MText Integer, MText, BigMap MText Integer, BigMap MText Integer]

  -- Retrieve keys
  # L.get
  # L.assertSome @MText "Expected Some, found None."
  # L.dug @2
  # L.get
  # L.assertSome @MText "Expected Some, found None."

  # L.pair
  # L.pair
  # L.nil @Operation # L.pair