packages feed

cleveland-0.2.1: 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 #-}

{-# LANGUAGE OverloadedLists #-}

module TestSuite.Cleveland.RunCode
  ( test_runs_code
  , test_emulator_state_is_not_modified
  , test_other_operations_are_not_executed
  , test_handles_mix_of_bigmaps_and_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 (gsContractAddressesL)
import Morley.Michelson.Typed (untypeValue)
import Morley.Michelson.Untyped (Elt(..), Value'(..))
import Morley.Tezos.Address
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 = untypeValue $ toVal @Integer 5
      , rcParameter = untypeValue $ toVal @Integer 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 = untypeValue $ toVal ()
      , rcParameter = untypeValue $ toVal ()
      , 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 = untypeValue $ toVal @Integer 0
      , rcParameter = untypeValue $ toVal ()
      , 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_mix_of_bigmaps_and_bigmap_ids :: TestTree
test_handles_mix_of_bigmaps_and_bigmap_ids =
  testScenario "handles a mix of bigmaps and 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 <- originate "bigmap" (one ("key", 20)) $ idContract @() @(BigMap MText Integer)
    bigMapId <- getStorage handle

    (_, results) <- runCode RunCode
      { rcContract = lookupBigMapKey
      , rcParameter =
          -- a list with a big_map ID and a big_map value
          ValueSeq
            [ untypeValue $ toVal bigMapId
            , ValueMap [Elt (ValueString "key") (ValueInt 30)]
            ]
      , rcStorage =
          ValuePair
            (ValueSeq
              [ untypeValue $ toVal bigMapId
              , ValueMap [Elt (ValueString "key") (ValueInt 40)]
              ]
            )
            (ValueNil)
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      }

    results @== [40, 20, 30, 20]

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 = untypeValue $ toVal $ Nothing @EnvVars
      , rcParameter = untypeValue $ toVal ()
      , 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 @== toAddress sender
    envSource envVars @== toAddress 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 "contract" () (idContract @Integer @()) [tz|123u|]
    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 = untypeValue $ toVal $ Nothing @Address
      , rcParameter = untypeValue $ toVal ()
      , 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

    MkAddress selfAddr@ContractAddress{} <- runCode RunCode
      { rcContract = returnSelf
      , rcStorage = untypeValue $ toVal $ Nothing @Address
      , rcParameter = untypeValue $ toVal @Integer 0
      , rcAmount = 0
      , rcBalance = 0
      , rcSource = Nothing
      , rcNow = Nothing
      , rcLevel = Nothing
      } >>= evalJust "Expected contract to return a `Some`"

    emulatorState ^. gsContractAddressesL . at selfAddr @== Nothing

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

-- | This contracts has a list of big_maps in its storage, and takes an additional
-- list of big_maps in its parameter.
--
-- When called, it looks up the key "key" in all big_maps, and stores
-- all results in a @[Integer]@ in the storage.
lookupBigMapKey
  :: Contract [BigMap MText Integer]
              ([BigMap MText Integer], [Integer])
              ()
lookupBigMapKey = L.mkContractWith L.intactCompilationOptions $ L.mkContractCode $
  -- Unpair everything
  L.unpair
  # L.dip L.unpair
  # L.stackType @'[[BigMap MText Integer], [BigMap MText Integer], [Integer]]

  -- Discard the `[Integer]` in the storage
  # L.dipN @2 L.drop
  # L.stackType @'[[BigMap MText Integer], [BigMap MText Integer]]

  -- Get the value at key "key" from all big_maps
  # L.nil @Integer
  # L.swap
  # getValues
  # L.stackType @'[[Integer], [BigMap MText Integer]]
  # L.swap
  # getValues
  # L.stackType @'[[Integer]]

  # L.nil
  # L.pair
  # L.nil @Operation # L.pair
  where
    -- Get the value at key "key" from all big_maps
    getValues :: [BigMap MText Integer] ': [Integer] ': s :-> [Integer] ': s
    getValues =
      L.iter
        (
          L.push @MText "key"
          # L.get
          # L.assertSome @MText "Expected Some, found None."
          # L.cons
        )