packages feed

morley-1.8.0: src/Michelson/Untyped/Entrypoints.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

module Michelson.Untyped.Entrypoints
  ( EpName (..)
  , pattern DefEpName
  , isDefEpName
  , epNameFromParamAnn
  , epNameToParamAnn
  , epNameFromRefAnn
  , epNameFromSelfAnn
  , epNameToRefAnn
  , EpNameFromRefAnnError (..)
  , buildEpName
  , unsafeBuildEpName
  , mkEntrypointsMap
  ) where

import Data.Aeson.TH (deriveJSON)
import qualified Data.Map as Map
import Fmt (Buildable(..), pretty, (+|), (|+))

import Michelson.Untyped.Annotation
import Michelson.Untyped.Type
import Util.Aeson
import Util.CLI

-- | Entrypoint name.
--
-- There are two properties we care about:
--
-- 1. Special treatment of the @default@ entrypoint name.
-- @default@ is prohibited in the @CONTRACT@ instruction and in
-- values of @address@ and @contract@ types.
-- However, it is not prohibited in the @SELF@ instruction.
-- Hence, the value inside @EpName@ __can__ be @"default"@, so that
-- we can distinguish @SELF@ and @SELF %default@. It is important
-- to distinguish them because their binary representation that is
-- inserted into blockchain is different. For example, typechecking
-- @SELF %default@ consumes more gas than @SELF@.
-- In this module, we provide several smart constructors with different
-- handling of @default@, please use the appropriate one for your use case.
-- 2. The set of permitted characters. Intuitively, an entrypoint name should
-- be valid only if it is a valid annotation (because entrypoints are defined
-- using field annotations). However, it is not enforced in Tezos.
-- It is not clear whether this behavior is intended. There is an upstream
-- [issue](https://gitlab.com/tezos/tezos/-/issues/851) which received @bug@
-- label, so probably it is considered a bug. Currently we treat it as a bug
-- and deviate from upstream implementation by probiting entrypoint names that
-- are not valid annotations. If Tezos developers fix it soon, we will be happy.
-- If they don't, we should (maybe temporarily) remove this limitation from our
-- code. There is an
-- [issue](https://gitlab.com/morley-framework/morley/-/issues/275) in our
-- repo as well.
newtype EpName = EpNameUnsafe { unEpName :: Text }
  deriving stock (Show, Eq, Ord, Generic)

instance NFData EpName

deriveJSON morleyAesonOptions ''EpName

-- | This is a bidirectional pattern that can be used for two purposes:
--
-- 1. Construct an 'EpName' referring to the default entrypoint.
-- 2. Use it in pattern-matching or in equality comparison to check whether
-- 'EpName' refers to the default entrypoint. This is trickier because there
-- are two possible 'EpName' values referring to the default entrypoints.
-- 'DefEpName' will match only the most common one (no entrypoint).
-- However, there is a special case: @SELF@ instruction can have explicit
-- @%default@ reference. For this reason, it is recommended to use
-- 'isDefEpName' instead. Pattern-matching on 'DefEpName' is still permitted
-- for backwards compatibility and for the cases when you are sure that
-- 'EpName' does not come from the @SELF@ instruction.
pattern DefEpName :: EpName
pattern DefEpName = EpNameUnsafe ""

-- | Check whether given 'EpName' refers to the default entrypoint.
-- Unlike 'DefEpName' pattern, this function correctly handles all cases,
-- including the @SELF@ instruction.
isDefEpName :: EpName -> Bool
isDefEpName epName = epName == DefEpName || epName == EpNameUnsafe "default"

instance Buildable EpName where
  build = \case
    DefEpName -> "<default>"
    EpNameUnsafe name -> build name

-- | Make up 'EpName' from annotation in parameter type declaration.
--
-- Returns 'Nothing' if no entrypoint is assigned here.
epNameFromParamAnn :: FieldAnn -> Maybe EpName
epNameFromParamAnn an@(Annotation a)
  | an == noAnn = Nothing
  | a == "default" = Just (EpNameUnsafe "")
  | otherwise = Just $ EpNameUnsafe a

-- | Turn entrypoint name into annotation for contract parameter declaration.
epNameToParamAnn :: EpName -> FieldAnn
epNameToParamAnn (EpNameUnsafe name)
  | name == "" = ann "default"
  | otherwise = ann name

data EpNameFromRefAnnError
  = InEpNameBadAnnotation FieldAnn
  deriving stock (Show, Eq, Generic)

instance NFData EpNameFromRefAnnError

instance Buildable EpNameFromRefAnnError where
  build = \case
    InEpNameBadAnnotation (Annotation an) ->
      "Invalid entrypoint reference `" +| an |+ "`"

-- | Make up 'EpName' from annotation which is reference to an entrypoint.
-- Note that it's more common for Michelson to prohibit explicit @default@
-- entrypoint reference.
--
-- Specifically, @%default@ annotation is probitited in values of @address@
-- and @contract@ types. It's also prohibited in the @CONTRACT@ instruction.
-- However, there is an exception: @SELF %default@ is a perfectly valid
-- instruction. Hence, when you construct an 'EpName' from an annotation
-- that's part of @SELF@, you should use 'epNameFromSelfAnn' instead.
epNameFromRefAnn :: FieldAnn -> Either EpNameFromRefAnnError EpName
epNameFromRefAnn an@(Annotation a)
  | a == "default" = Left $ InEpNameBadAnnotation an
  | otherwise = Right $ EpNameUnsafe a

-- | Make up an 'EpName' from an annotation which is part of the
-- @SELF@ instruction.
epNameFromSelfAnn :: FieldAnn -> EpName
epNameFromSelfAnn (Annotation a) = EpNameUnsafe a

-- | Turn entrypoint name into annotation used as reference to entrypoint.
epNameToRefAnn :: EpName -> FieldAnn
epNameToRefAnn (EpNameUnsafe name) = ann name

-- | Make a valid entrypoint name from an arbitrary text. This
-- function prohibits explicit @default@ entrypoint name which is
-- permitted by Michelson inside the @SELF@ instruction. This
-- limitation shouldn't be restrictive because @SELF@ is equivalent to
-- @SELF %default@.
buildEpName :: Text -> Either String EpName
buildEpName txt = do
  annotation <-
    mkAnnotation txt
    & first (mappend "Failed to parse entrypoint: " . pretty)
  epNameFromRefAnn annotation
    & first pretty

-- | Partial version of 'buildEpName'.
unsafeBuildEpName :: HasCallStack => Text -> EpName
unsafeBuildEpName = either (error . pretty) id . buildEpName

instance HasCLReader EpName where
  getReader = eitherReader (buildEpName . toText)
  getMetavar = "ENTRYPOINT"

-- | Given an untyped parameter type, extract a map that maps entrypoint names
-- to the their parameter types. If there are duplicate entrypoints in the
-- given Type then the duplicate entrypoints at a deeper nesting level will get
-- overwritten with the ones that are on top.
mkEntrypointsMap :: ParameterType -> Map EpName Type
mkEntrypointsMap (ParameterType ty rootAnn) = mkEntrypointsMapRec rootAnn ty

-- | Version of 'mkEntrypointMaps' for plain untyped type.
mkEntrypointsMapRec :: FieldAnn -> Type -> Map EpName Type
mkEntrypointsMapRec curRootAnn ty =
  accountRoot curRootAnn <> accountTree ty
  where
    accountRoot rootAnn = Map.fromList $ do
      Just rootEp <- pure $ epNameFromParamAnn rootAnn
      return (rootEp, ty)

    accountTree (Type t _) = case t of
      -- We are only interested in `Or` branches to extract entrypoint
      -- annotations.
      TOr f1 f2 t1 t2 -> mkEntrypointsMapRec f1 t1 <> mkEntrypointsMapRec f2 t2
      _ -> mempty