packages feed

morley-1.6.0: src/Michelson/Typed/Entrypoints.hs

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

-- | Utilities for lightweight entrypoints support.
module Michelson.Typed.Entrypoints
  ( EpAddress (..)
  , ParseEpAddressError (..)
  , formatEpAddress
  , mformatEpAddress
  , parseEpAddress
  , unsafeParseEpAddress
  , parseEpAddressRaw
  , unsafeParseEpAddressRaw
  , ParamNotes (..)
  , pattern ParamNotes
  , starParamNotes
  , ArmCoord (..)
  , ArmCoords
  , ParamEpError (..)
  , mkParamNotes

  , EpLiftSequence (..)
  , EntrypointCallT (..)
  , epcPrimitive
  , epcCallRootUnsafe
  , SomeEntrypointCallT (..)
  , sepcCallRootUnsafe
  , sepcPrimitive
  , sepcName
  , ForbidOr
  , MkEntrypointCallRes (..)
  , mkEntrypointCall

  , tyImplicitAccountParam
  , flattenEntrypoints

    -- * Re-exports
  , EpName (..)
  , pattern DefEpName
  , epNameFromParamAnn
  , epNameToParamAnn
  , epNameFromRefAnn
  , epNameToRefAnn
  , EpNameFromRefAnnError (..)
  ) where

import Control.Monad.Except (throwError)
import qualified Data.ByteString as BS
import Data.Constraint (Dict(..))
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import Fmt (Buildable(..), hexF, pretty, (+|), (|+))
import Test.QuickCheck (Arbitrary(..))

import Michelson.Text
import Michelson.Typed.Annotation
import Michelson.Typed.Extract
import Michelson.Typed.Scope
import Michelson.Typed.Sing
import Michelson.Typed.T
import qualified Michelson.Untyped as U
import Michelson.Untyped.Annotation
import Michelson.Untyped.Entrypoints
import Tezos.Address
import Tezos.Crypto (keyHashLengthBytes)
import Util.TH
import Util.Typeable
import Util.TypeLits

----------------------------------------------------------------------------
-- Primitives
----------------------------------------------------------------------------
--
-- EpAddress
----------------------------------------------------------------------------

-- | Address with optional entrypoint name attached to it.
-- TODO: come up with better name?
data EpAddress = EpAddress
  { eaAddress :: Address
    -- ^ Address itself
  , eaEntrypoint :: EpName
    -- ^ Entrypoint name (might be empty)
  } deriving stock (Show, Eq, Ord, Generic)

instance Buildable EpAddress where
  build = build . formatEpAddress

instance NFData EpAddress

formatEpAddress :: EpAddress -> Text
formatEpAddress (EpAddress addr ep)
  | isDefEpName ep = formatAddress addr
  | otherwise = formatAddress addr <> "%" <> pretty ep

mformatEpAddress :: EpAddress -> MText
mformatEpAddress ea =
  let t = formatEpAddress ea
     -- Should be safe because set of characters allowed in annotations
     -- (and thus in 'EpName') is subset of characters allowed in Michelson strings.
  in mkMTextUnsafe t

data ParseEpAddressError
  = ParseEpAddressBadAddress ParseAddressError
  | ParseEpAddressRawBadAddress ParseAddressRawError
  | ParseEpAddressBadEntryopint ByteString UnicodeException
  | ParseEpAddressBadRefAnn Text
  | ParseEpAddressRefAnnError EpNameFromRefAnnError
  | ParseEpAddressInvalidLength Int
  deriving stock (Show, Eq, Generic)

instance NFData ParseEpAddressError

instance Buildable ParseEpAddressError where
  build = \case
    ParseEpAddressBadAddress err -> build err
    ParseEpAddressRawBadAddress err -> build err
    ParseEpAddressBadEntryopint addr exception ->
      "Invalid entrypoint given for raw adddress " <> hexF addr <>
      " and failed with " <> build (show @Text exception)
    ParseEpAddressBadRefAnn txt -> pretty $ "Invalid reference annotation: " <> txt
    ParseEpAddressRefAnnError err -> build err
    ParseEpAddressInvalidLength len ->
      "Given raw entrypoint address has invalid length: " <> build len

-- | Parse an address which can be suffixed with entrypoint name
-- (e.g. "tz1faswCTDciRzE4oJ9jn2Vm2dvjeyA9fUzU%entrypoint").
parseEpAddress :: Text -> Either ParseEpAddressError EpAddress
parseEpAddress txt =
  let (addrTxt, mannotTxt) = T.breakOn "%" txt
  in case mannotTxt of
    "" -> do
      addr <- first ParseEpAddressBadAddress $ parseAddress addrTxt
      return $ EpAddress addr DefEpName
    annotTxt' -> do
      addr <- first ParseEpAddressBadAddress $ parseAddress addrTxt
      annot <- first ParseEpAddressBadRefAnn $ case T.stripPrefix "%" annotTxt' of
        Nothing -> error "impossible"
        Just a -> mkAnnotation a
      epName <- first ParseEpAddressRefAnnError $ epNameFromRefAnn annot
      return $ EpAddress addr epName

unsafeParseEpAddress :: HasCallStack => Text -> EpAddress
unsafeParseEpAddress = either (error . pretty) id . parseEpAddress

instance Arbitrary FieldAnn => Arbitrary EpAddress where
  arbitrary = EpAddress <$> arbitrary <*> arbitrary

-- | Parses byte representation of entrypoint address.
--
-- For every address
--
-- @
-- KT1QbdJ7M7uAQZwLpvzerUyk7LYkJWDL7eDh%foo%bar
-- @
--
-- we get the following byte representation
--
-- @
-- 01afab866e7f1e74f9bba388d66b246276ce50bf4700666f6f25626172
-- \________________________________________/\/\____/\/\____/
--               address                     %   ep1  % ep2
-- @
--
parseEpAddressRaw :: ByteString -> Either ParseEpAddressError EpAddress
parseEpAddressRaw raw = do
  let (bytes, eps) = BS.splitAt (keyHashLengthBytes + 2) raw
  eaAddress <- first ParseEpAddressRawBadAddress $ parseAddressRaw bytes
  decodedEntrypoint <- first (ParseEpAddressBadEntryopint raw) $ decodeUtf8' eps
  decodedAnnotation <- first ParseEpAddressBadRefAnn $ mkAnnotation decodedEntrypoint
  eaEntrypoint <- first ParseEpAddressRefAnnError $ epNameFromRefAnn decodedAnnotation
  pure $ EpAddress {..}

unsafeParseEpAddressRaw :: ByteString -> EpAddress
unsafeParseEpAddressRaw = either (error . pretty) id . parseEpAddressRaw

-- ParamNotes
----------------------------------------------------------------------------

-- | Annotations for contract parameter declaration.
--
-- Following the Michelson specification, this type has the following invariants:
-- 1. No entrypoint name is duplicated.
-- 2. If @default@ entrypoint is explicitly assigned, no "arm" remains uncallable.
data ParamNotes (t :: T) = ParamNotesUnsafe
  { pnNotes   :: Notes t
  , pnRootAnn :: RootAnn
  } deriving stock (Show, Eq, Generic)
    deriving anyclass (NFData)

pattern ParamNotes :: Notes t -> RootAnn -> ParamNotes t
pattern ParamNotes t f <- ParamNotesUnsafe t f
{-# COMPLETE ParamNotes #-}

-- | Parameter without annotations.
starParamNotes :: SingI t => ParamNotes t
starParamNotes = ParamNotesUnsafe starNotes noAnn

-- | Coordinates of "arm" in Or tree, used solely in error messages.
type ArmCoords = [ArmCoord]
data ArmCoord = AcLeft | AcRight
  deriving stock (Show, Eq, Generic)

instance NFData ArmCoord

instance Buildable ArmCoord where
  build = \case
    AcLeft -> "left"
    AcRight -> "right"

-- | Errors specific to parameter type declaration (entrypoints).
data ParamEpError
  = ParamEpDuplicatedNames (NonEmpty EpName)
  | ParamEpUncallableArm ArmCoords
  deriving stock (Show, Eq, Generic)

instance NFData ParamEpError

instance Buildable ParamEpError where
  build = \case
    ParamEpDuplicatedNames names -> mconcat
      [ "Duplicated entrypoint names: "
      , mconcat . intersperse ", " $ map (surround "'" "'" . build) (toList names)
      ]
    ParamEpUncallableArm arm -> mconcat
      [ "Due to presence of 'default' entrypoint, one of contract \"arms\" \
        \cannot be called: \""
      , mconcat . intersperse " - " $ map build arm
      , "\""
      , if length arm > 1 then " (in top-to-bottom order)" else ""
      ]
    where
    surround pre post builder = pre <> builder <> post

-- | Check whether given notes are valid parameter notes.
verifyParamNotes :: Notes t -> RootAnn -> Either ParamEpError ()
verifyParamNotes notes ra = do
  let allEps = appEndo (gatherEntrypoints notes) []
      duplicatedEps
        = mapMaybe (safeHead . tail)
        . NE.group
        . sort
        $ maybe allEps (: allEps) (epNameFromParamAnn $ convAnn ra)

  whenJust (nonEmpty duplicatedEps) $ \dups ->
    throwError $ ParamEpDuplicatedNames dups

  -- In case contract have explicit root entrypoint, we assume that everything is
  -- callable.
  when (ra == noAnn) $ void
    $ first ParamEpUncallableArm
    $ ensureAllCallable notes
  where
    gatherEntrypoints :: Notes t -> Endo [EpName]
    gatherEntrypoints = \case
      NTOr _ fn1 fn2 l r -> mconcat
        [ Endo $ maybe id (:) (epNameFromParamAnn fn1)
        , Endo $ maybe id (:) (epNameFromParamAnn fn2)
        , gatherEntrypoints l
        , gatherEntrypoints r
        ]
      _ -> mempty

    -- Here we can assume that there is no more than one @default@ entrypoint,
    -- because duplications check occurs earlier.
    --
    -- In case when multiple entrypoints are uncallable, the reference
    -- implementation prefers displaying (in error message) arms which are
    -- closer to the root, but here we don't do this because that would be
    -- some more complex to implement and not sure how much does it worth that.
    ensureAllCallable :: Notes t -> Either ArmCoords Bool
    ensureAllCallable = \case
      NTOr _ fnL fnR l r -> do
        let epNameL = epNameFromParamAnn fnL
        let epNameR = epNameFromParamAnn fnR
        haveDefLL <- first (AcLeft  :) $ ensureAllCallable l
        haveDefRR <- first (AcRight :) $ ensureAllCallable r

        let haveDefL = or [haveDefLL, maybe False isDefEpName epNameL]
        let haveDefR = or [haveDefRR, maybe False isDefEpName epNameR]

        when haveDefL $ first (AcRight :) $ checkAllEpsNamed epNameR r
        when haveDefR $ first (AcLeft  :) $ checkAllEpsNamed epNameL l

        return $ or [haveDefL, haveDefR]

      _ -> return False

    checkAllEpsNamed :: Maybe EpName -> Notes t -> Either ArmCoords ()
    checkAllEpsNamed epNameRoot
      | isJust epNameRoot = \_ -> pass
      | otherwise = \case
          NTOr _ fnL fnR l r -> do
            let epNameL = epNameFromParamAnn fnL
                epNameR = epNameFromParamAnn fnR

            first (AcLeft  :) $ checkAllEpsNamed epNameL l
            first (AcRight :) $ checkAllEpsNamed epNameR r

          _ -> throwError []

-- | Construct 'ParamNotes' performing all necessary checks.
mkParamNotes :: Notes t -> RootAnn -> Either ParamEpError (ParamNotes t)
mkParamNotes nt fa = verifyParamNotes nt fa $> ParamNotesUnsafe nt fa

----------------------------------------------------------------------------
-- Entrypoint logic
----------------------------------------------------------------------------

-- | Describes how to construct full contract parameter from given entrypoint
-- argument.
--
-- This could be just wrapper over @Value arg -> Value param@, but we cannot
-- use @Value@ type in this module easily.
data EpLiftSequence (arg :: T) (param :: T) where
  EplArgHere :: EpLiftSequence arg arg
  EplWrapLeft
    :: (KnownT subparam, KnownT r)
    => EpLiftSequence arg subparam -> EpLiftSequence arg ('TOr subparam r)
  EplWrapRight
    :: (KnownT l, KnownT subparam)
    => EpLiftSequence arg subparam -> EpLiftSequence arg ('TOr l subparam)

deriving stock instance Eq (EpLiftSequence arg param)
deriving stock instance Show (EpLiftSequence arg param)

$(deriveGADTNFData ''EpLiftSequence)

instance Buildable (EpLiftSequence arg param) where
  build = \case
    EplArgHere -> "×"
    EplWrapLeft es -> "Left (" <> build es <> ")"
    EplWrapRight es -> "Right (" <> build es <> ")"

-- | Reference for calling a specific entrypoint of type @arg@.
data EntrypointCallT (param :: T) (arg :: T) =
  ParameterScope arg => EntrypointCall
  { epcName :: EpName
    -- ^ Name of entrypoint.
  , epcParamProxy :: Proxy param
    -- ^ Proxy of parameter, to make parameter type more easily fetchable.
  , epcLiftSequence :: EpLiftSequence arg param
    -- ^ How to call this entrypoint in the corresponding contract.
  }

deriving stock instance Eq (EntrypointCallT param arg)
deriving stock instance Show (EntrypointCallT param arg)
instance NFData (EntrypointCallT param arg) where
  rnf (EntrypointCall name Proxy s) = rnf (name, s)

instance Buildable (EntrypointCallT param arg) where
  build EntrypointCall{..} =
    "Call " +| epcName |+ ": " +| epcLiftSequence |+ ""

-- | Construct 'EntrypointCallT' which calls no entrypoint and assumes that
-- there is no explicit "default" one.
--
-- Validity of such operation is not ensured.
epcCallRootUnsafe :: ParameterScope param => EntrypointCallT param param
epcCallRootUnsafe = EntrypointCall
  { epcName = DefEpName
  , epcParamProxy = Proxy
  , epcLiftSequence = EplArgHere
  }

-- | Call parameter which has no entrypoints, always safe.
epcPrimitive
  :: forall p.
     (ParameterScope p, ForbidOr p)
  => EntrypointCallT p p
epcPrimitive = epcCallRootUnsafe
  where
  _requireNoOr = Dict @(ForbidOr p)

type family ForbidOr (t :: T) :: Constraint where
  ForbidOr ('TOr l r) =
    TypeError
    ('Text "Cannot apply to sum type parameter " ':<>: 'ShowType ('TOr l r))
  ForbidOr _ = ()

-- | 'EntrypointCallT' with hidden parameter type.
--
-- This requires argument to satisfy 'ParameterScope' constraint.
-- Strictly speaking, entrypoint argument may one day start having different
-- set of constraints comparing to ones applied to parameter, but this seems
-- unlikely.
data SomeEntrypointCallT (arg :: T) =
  forall param. (ParameterScope param) =>
  SomeEpc (EntrypointCallT param arg)

instance Eq (SomeEntrypointCallT arg) where
  SomeEpc epc1 == SomeEpc epc2 = isJust @() $ do
    Refl <- eqP (epcParamProxy epc1) (epcParamProxy epc2)
    guard (epc1 == epc2)

deriving stock instance Show (SomeEntrypointCallT arg)
instance NFData (SomeEntrypointCallT arg) where
  rnf (SomeEpc epc) = rnf epc

instance Buildable (SomeEntrypointCallT arg) where
  build (SomeEpc epc) = build epc

-- | Construct 'SomeEntrypointCallT' which calls no entrypoint and assumes that
-- there is no explicit "default" one.
--
-- Validity of such operation is not ensured.
sepcCallRootUnsafe :: ParameterScope param => SomeEntrypointCallT param
sepcCallRootUnsafe = SomeEpc epcCallRootUnsafe

-- | Call parameter which has no entrypoints, always safe.
sepcPrimitive
  :: forall t.
     (ParameterScope t, ForbidOr t)
  => SomeEntrypointCallT t
sepcPrimitive = SomeEpc epcPrimitive

sepcName :: SomeEntrypointCallT arg -> EpName
sepcName (SomeEpc epc) = epcName epc

-- | Build 'EpLiftSequence'.
--
-- Here we accept entrypoint name and type information for the parameter
-- of target contract.
--
-- Returns 'Nothing' if entrypoint is not found.
-- Does not treat default entrypoints specially.
withEpLiftSequence
  :: forall param r.
     (ParameterScope param)
  => EpName
  -> Notes param
  -> (forall arg. (ParameterScope arg) => (Notes arg, EpLiftSequence arg param) -> r)
  -> Maybe r
withEpLiftSequence epName@(epNameToParamAnn -> epAnn) param cont =
  case (sing @param, param) of
    (STOr lSing _, NTOr _ lFieldAnn rFieldAnn lNotes rNotes) ->
      case (checkOpPresence lSing, checkNestedBigMapsPresence lSing) of
        (OpAbsent, NestedBigMapsAbsent) -> asum
          [ guard (lFieldAnn == epAnn) $> cont (lNotes, EplWrapLeft EplArgHere)
          , guard (rFieldAnn == epAnn) $> cont (rNotes, EplWrapRight EplArgHere)
          , withEpLiftSequence epName lNotes (cont . fmap @((,) _) EplWrapLeft)
          , withEpLiftSequence epName rNotes (cont . fmap @((,) _) EplWrapRight)
          ]
    _ -> Nothing

-- Helper datatype for 'mkEntrypointCall'.
data MkEntrypointCallRes param where
  MkEntrypointCallRes
    :: ParameterScope arg
    => Notes arg
    -> EntrypointCallT param arg
    -> MkEntrypointCallRes param

-- | Build 'EntrypointCallT'.
--
-- Here we accept entrypoint name and type information for the parameter
-- of target contract.
--
-- Returns 'Nothing' if entrypoint is not found.
mkEntrypointCall
  :: (ParameterScope param)
  => EpName
  -> ParamNotes param
  -> Maybe (MkEntrypointCallRes param)
mkEntrypointCall epName (ParamNotes paramNotes root) =
  asum
  [ do
      epName' <- epNameFromParamAnn $ convAnn root
      guard (epName == epName')
      return $ MkEntrypointCallRes
        paramNotes
        EntrypointCall
          { epcName = epName
          , epcParamProxy = Proxy
          , epcLiftSequence = EplArgHere
          }
  , withEpLiftSequence epName paramNotes $ \(argInfo, liftSeq) ->
      MkEntrypointCallRes argInfo $ EntrypointCall
        { epcName = epName
        , epcParamProxy = Proxy
        , epcLiftSequence = liftSeq
        }
  , guard (isDefEpName epName) $>
      MkEntrypointCallRes paramNotes EntrypointCall
        { epcName = epName
        , epcParamProxy = Proxy
        , epcLiftSequence = EplArgHere
        }
  ]

-- | "Parameter" type of implicit account.
tyImplicitAccountParam :: ParamNotes 'TUnit
tyImplicitAccountParam = ParamNotesUnsafe starNotes noAnn

-- Misc
----------------------------------------------------------------------------

-- | Flatten a provided list of notes to a map of its entrypoints and its
-- corresponding utype. Please refer to 'mkEntrypointsMap' in regards to how
-- duplicate entrypoints are handled.
flattenEntrypoints :: SingI t => ParamNotes t -> Map EpName U.Type
flattenEntrypoints (pnNotes -> notes) = mkEntrypointsMap (mkUType notes)