packages feed

cleveland-0.3.2: src/Test/Cleveland/Internal/Actions/View.hs

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

{-# OPTIONS_HADDOCK not-home #-}

-- | On-chain view utilities.
module Test.Cleveland.Internal.Actions.View
  ( module Test.Cleveland.Internal.Actions.View
  ) where

import Data.Constraint (pattern Dict)
import Fmt ((+|), (|+))

import Lorentz (Contract(..), HasView, Label, NiceViewable, toVal)
import Lorentz qualified as L
import Morley.AsRPC
import Morley.Client.Types (awaAddress)
import Morley.Michelson.Typed (pattern (:#))
import Morley.Michelson.Typed qualified as T
import Morley.Tezos.Address
import Morley.Util.TypeLits (KnownSymbol, symbolValT')
import Test.Cleveland.Internal.Abstract
import Test.Cleveland.Internal.Actions.Assertions (failure)
import Test.Cleveland.Internal.Actions.Misc
import Test.Cleveland.Lorentz.Types

-- | Call an on-chain view by name. The existence of the view is checked at
-- compile time. If you don't have compile-time information about views, see
-- 'unsafeCallView'.
--
-- Example:
--
-- > callView contract #sum (123, -321)
callView
  :: forall name arg ret cp st vd m caps.
    ( MonadCleveland caps m, HasView vd name arg ret
    , NiceParameter arg, NiceViewable ret, NiceStorage ret
    , KnownSymbol name, HasRPCRepr ret
    , T.IsoValue (AsRPC ret))
  => ContractHandle cp st vd -- ^ Contract to call.
  -> Label name -- ^ View name. Use @OverloadedLabels@ syntax.
  -> arg -- ^ Parameter to pass to the view.
  -> m (AsRPC ret)
callView = unsafeCallView @ret
  where _ = Dict @(HasView vd name arg ret) -- silence redundant constraint warning

-- | Version of 'callView' that doesn't check if the view exists in the type.
-- You'll have to specify the return type. You can use @TypeApplications@ syntax
-- for that.
--
-- If the view doesn't exist or has incorrect type, a test failure will be
-- thrown.
--
-- Note that first type argument is return type, the second is parameter type.
-- The reason for this inversion is you often only need to specify the return
-- type, while the parameter type can be either inferred or explicitly specified
-- with a type annotation on the parameter argument value.
--
-- Examples:
--
-- > unsafeCallView @() contract #id ()
-- Calls view @id@ with argument @unit@ and return type @unit@.
--
-- > unsafeCallView @(Integer, MText) contract #query [mt|hello|]
-- Calls view @query@ with argument @string@ and return type @pair int string@.
--
-- > unsafeCallView @Integer contract #sum (123 :: Natural, -321 :: Integer)
-- Calls view @sum@ with argument @pair nat int@ and return type @int@. Type
-- annotations are required due to polymorphic numeric literals.
--
-- This last example could also be written as
--
-- > unsafeCallView @Integer @(Natural, Integer) contract #sum (123, -321)
unsafeCallView
  :: forall ret arg name addr m caps.
    ( MonadCleveland caps m, NiceParameter arg, NiceViewable ret
    , NiceStorage ret, KnownSymbol name
    , HasRPCRepr ret, T.IsoValue (AsRPC ret)
    , ToContractAddress addr)
  => addr -- ^ Contract to call.
  -> Label name -- ^ View name. Use @OverloadedLabels@ syntax.
  -> arg -- ^ Parameter to pass to the view.
  -> m (AsRPC ret)
unsafeCallView (toContractAddress -> ch) _ arg = do
  Sender sender' <- Prelude.view senderL
  contractBalance <- getBalance ch
  let ct = runnerContract @name @arg @ret ch
      callFail = failure $ "Failed to call view " +| symbolValT' @name |+
        ": either it doesn't exist or types don't match."
  res <- runCode RunCode
    { rcAmount = 0
    , rcBalance = contractBalance
    , rcSource = Just $ awaAddress sender'
    , rcContract = ct
    , rcParameter = T.untypeValue $ T.toVal arg
    , rcStorage = T.untypeValue $ T.toVal (Nothing :: Maybe ret)
    , rcLevel = Nothing
    , rcNow = Nothing
    }
  maybe callFail pure res

-- | Contract that calls a view and saves the result to storage.
runnerContract
  :: forall name arg ret. (NiceParameter arg, NiceViewable ret, NiceStorage ret, KnownSymbol name)
  => ContractAddress -> Contract arg (Maybe ret) ()
runnerContract ch = flip Contract fakeDoc $ T.defaultContract $
  -- NB: It's not a Lorentz contract to avoid requiring NiceParameterFull and
  -- NiceStorageFull on arg and ret respectively.
  T.CAR :# T.DIP (T.PUSH $ toVal $ toAddress ch)
    :# T.VIEW (T.UnsafeViewName $ symbolValT' @name)
    :# T.NIL :# T.PAIR
  where
    fakeDoc = L.ContractCode $ L.fakeCoercing L.nop