packages feed

morley-upgradeable-0.3: src/Lorentz/Contracts/Upgradeable/EntrypointWise.hs

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

module Lorentz.Contracts.Upgradeable.EntrypointWise
  ( EntrypointImpl
  , EpwFallback
  , EpwContract (..)
  , EpwCaseClause (..)
  , mkEpwContract
  , mkEpwContractT
  , epwFallbackFail
  , (/==>)
  , removeEndpoint
  , EpwDocumented (..)
  , epwContractDoc
  ) where

import Lorentz
import Prelude (Typeable, fmap)

import Lorentz.Contracts.Upgradeable.Common
import Lorentz.UStore
import Michelson.Text
import Util.TypeLits
import Util.TypeTuple

-- | This data type represents the new contract code and migrations necessary
--   to upgrade the contract endpoints to the new version.
data EpwContract ver = EpwContract
  { epwServe :: UContractRouter ver
  -- ^ `epwServe` does  the dispatching logic and is assumed to be used for
  --   the `code` lambda of the upgradeable contract.

  , epwCodeMigrations :: forall oldStore. [MigrationScript oldStore (VerUStoreTemplate ver)]
  -- ^ `epwCodeMigrations` is a list of packed migrations the client ought to
  --   pass to the `EpwUpgrade` method in order to upgrade the implementation.
  }


-- | Creates the EpwContract data structure from a Rec of case clauses
mkEpwContract
  :: forall (ver :: VersionKind) (interface :: [EntrypointKind]) store.
  ( interface ~ VerInterface ver, store ~ VerUStoreTemplate ver
  , CodeMigrations interface
  , HasUStore "code" MText (EntrypointImpl store) store
  , HasUField "fallback" (EpwFallback store) store
  , Typeable store
  )
  => Rec (EpwCaseClause store) interface
  -> EpwFallback store
  -> EpwContract ver
mkEpwContract entries fallback = EpwContract
  { epwServe = mkUContractRouter $
      caseUParamUnsafe' @store @interface
  , epwCodeMigrations =
      fmap (MigrationScript . checkedCoercing_ @UStore_ @(UStore store)) $
        (push fallback # ustoreSetField #fallback) : mkMigrations entries
  }

-- | Like 'mkEpwContract', but accepts a tuple of clauses, not a 'Rec'.
mkEpwContractT
  :: forall clauses ver (interface :: [EntrypointKind]) store.
  ( interface ~ VerInterface ver, store ~ VerUStoreTemplate ver
  , clauses ~ Rec (EpwCaseClause store) interface
  , RecFromTuple clauses
  , CodeMigrations interface
  , HasUStore "code" MText (EntrypointImpl store) store
  , HasUField "fallback" (EpwFallback store) store
  , Typeable store
  )
  => IsoRecTuple clauses
  -> EpwFallback store
  -> EpwContract ver
mkEpwContractT clauses fallback = mkEpwContract (recFromTuple clauses) fallback

-- | A helper type that defines an entrypoint that receives
--   an unpacked argument
type TypedEntrypointImpl arg store =
  Lambda (arg, UStore store) ([Operation], UStore store)

-- | A helper type that defines an entrypoint that receives
--   a packed argument, i.e. it's basically an unpack instruction
--   followed by a TypedEntrypoint code
type EntrypointImpl store =
  Lambda (ByteString, UStore store) ([Operation], UStore store)

-- | A helper type that defines a function being called in case
--   no implementation matches the requested entrypoint
type EpwFallback store =
  Lambda ((MText, ByteString), UStore store) ([Operation], UStore store)

-- | A data type representing a full case clause with the name
--   and implementation of an entrypoint.
data EpwCaseClause store (entry :: EntrypointKind) where
  EpwCaseClause
    :: TypedEntrypointImpl arg store
    -> EpwCaseClause store '(name, arg)

(/==>)
  :: Label name
  -> Lambda (arg, UStore store) ([Operation], UStore store)
  -> EpwCaseClause store '(name, arg)
(/==>) _ = EpwCaseClause
infixr 0 /==>

-- | A greatly simplified version of UParam lookup code.
--
--   While it does not provide the same safety guarantees as UParam's lookup,
--   it does a map search instead of a linear search, and thus it may consume
--   less gas in practice.
caseUParamUnsafe'
  :: forall store (entries :: [EntrypointKind]).
  ( HasUStore "code" MText (EntrypointImpl store) store
  , HasUField "fallback" (EpwFallback store) store
  )
  => '[UParam entries, UStore store] :-> '[([Operation], UStore store)]
caseUParamUnsafe' = do
  dup
  unwrapUParam
  unpair
  dip (duupX @3)
  ustoreGet #code
  if IsSome
  then dip (dip (drop) # pair) # swap # exec
  else do
    drop
    dip (ustoreGetField #fallback # swap)
    unwrapUParam
    pair
    exec

-- | Default implementation for 'EpwFallback' reports an error just like its
--   UParam counterpart
epwFallbackFail :: EpwFallback store
epwFallbackFail =
  car # car # failCustom #uparamNoSuchEntrypoint

-- | These functions create the code blocks one has to supply in order
--   upgrade a contract. These code blocks write the code of the contract
--   to a submap of UStore. Code migrations _do not delete_ the old code
--   blocks from UStore, so would still be possible to call the old entry
--   points manually after applying migrations.
class CodeMigrations (entries :: [EntrypointKind]) where
  mkMigrations
    :: forall store.
    ( Typeable store
    , GetUStoreKey store "code" ~ MText
    , GetUStoreValue store "code" ~ EntrypointImpl store
    )
    =>  Rec (EpwCaseClause store) entries
    -> ['[UStore store] :-> '[UStore store]]

instance
  ( CodeMigrations entries
  , KnownSymbol name
  , NiceUnpackedValue arg
  )
  => CodeMigrations ((name ?: arg) ': entries) where
    mkMigrations (EpwCaseClause impl :& clauses) =
      (push untypedLambda # push (symbolToMText @name) # ustoreInsert #code)
      : mkMigrations clauses
      where
        untypedLambda = do
          unpair
          unpackRaw @arg
          ifSome nop $ failCustom_ #uparamArgumentUnpackFailed
          pair
          impl

instance CodeMigrations '[] where
  mkMigrations _ = []

-- | Removes an endpoint from the #code submap
removeEndpoint
  :: forall store name s.
     GetUStoreKey store "code" ~ MText
  => Label name
  -> UStore store ': s :-> UStore store ': s
removeEndpoint Label = do
  push $ symbolToMText @name
  ustoreDelete #code

-- | Helper for documenting entrypoints with EPW interface.
class EpwDocumented (entries :: [EntrypointKind]) where
  -- | Make up documentation for given entry points.
  --
  -- As result you get a fake contract from which you can later build desired
  -- documentation. Although, you may want to add contract name and
  -- description first.
  epwDocument
    :: Rec (EpwCaseClause store) entries
    -> Lambda () ()

instance EpwDocumented '[] where
  epwDocument RNil = nop

instance (KnownSymbol name, EpwDocumented es) =>
         EpwDocumented ('(name, a) ': es) where
  epwDocument (EpwCaseClause code :& es) =
    let documentedCode = clarifyParamBuildingSteps (pbsUParam @name) code
    in cutLorentzNonDoc documentedCode # epwDocument es

-- | By given list of entrypoints make up a fake contract which contains
-- documentation for the body of given upgradeable contract.
epwContractDoc
  :: forall ver.
     ( NiceVersion ver
     , KnownContractVersion ver
     , EpwDocumented (VerInterface ver)
     , PermConstraint ver
     )
  => Rec (EpwCaseClause (VerUStoreTemplate ver)) (VerInterface ver)
  -> PermanentImpl ver
  -> Lambda () ()
epwContractDoc upgImpl permImpl =
  fakeCoercing . finalizeParamCallingDoc @(Parameter ver) $ do
    doc $ DVersion (contractVersion (Proxy @ver))
    fakeCoercing $
      cCode $ upgradeableContract @ver
    fakeCoercing $
      -- We have to put this part (which describes actual logic of our contract)
      -- separately, because this is not directly part of @Run@ entrypoint of
      -- 'upgradeableContract', and also because Markdown editors usually do not
      -- render deeply nested headers well.
      clarifyParamBuildingSteps pbsContainedInRun $
        epwDocument upgImpl
    fakeCoercing $
      clarifyParamBuildingSteps pbsContainedInRunPerm $
        unPermanentImpl permImpl