packages feed

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

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

-- | Contracts based on storage-driven upgrages.
--
-- Here entrypoints are declared as part of UStore template, then
-- we automatically derive public API of the contract from it.
--
-- Migration mechanism for storage (see 'Lorentz.UStore.Migration') also applies
-- to these entrypoints.
--
-- This approach differs from one in "Lorentz.Contracts.Upgradeable.EntrypointWise"
-- in the following points:
-- 1. Storage migrations are not handled here, only 'UContractRouter' creation.
--    The former is comprehensively handled by 'Lorentz.UStore.Migration'.
-- 2. Contract interface is declared via storage - 'UStoreEntrypoint' entries
--    in storage define which public entrypoints (those which are callable via
--    passing 'UParam') the contract will have.
-- 3. Parameter dispatch fallback is made part of 'UContractRouter',
--    not storing it in storage here for simplicity.
--    The user can still decide to keep fallback implementation in storage if it
--    is big and then refer to it in 'SduFallback'.
module Lorentz.Contracts.Upgradeable.StorageDriven
  ( UStoreEntrypoint
  , UMarkerEntrypoint
  , SduEntrypoint
  , pattern UStoreEntrypoint
  , mkSduEntrypoint
  , mkUStoreEntrypoint
  , SduFallback
  , UStoreEpInterface
  , mkSduContract
  , callUStoreEntrypoint
  , sduFallbackFail

    -- * Documentation
  , sduDocument
  , SduDocumentTW
  , sduAddEntrypointDoc
  , SduAddEntrypointDocTW
  , sduContractDoc
  ) where

import Lorentz
import Prelude (Const(..), Identity(..), Typeable, id)

import qualified Data.Kind as Kind

import Lorentz.Contracts.Upgradeable.Common
import qualified Lorentz.Instr as L
import Lorentz.UStore
import Lorentz.UStore.Doc
import Lorentz.UStore.Traversal
import Util.TypeLits

----------------------------------------------------------------------------
-- Types
----------------------------------------------------------------------------

type SduEntrypointUntyped store =
  Lambda (ByteString, UStore store) ([Operation], UStore store)

-- | An entrypoint which is assumed to be kept in 'UStore'.
--   It accepts a packed argument.
newtype SduEntrypoint (store :: Kind.Type) (arg :: Kind.Type) = SduEntrypoint
  { unSduEntrypoint :: SduEntrypointUntyped store
  } deriving stock (Eq, Generic)
    deriving anyclass (IsoValue, Wrappable)

instance ( Typeable store, Typeable arg
         , TypeHasDoc (UStore store), TypeHasDoc arg
         ) =>
         TypeHasDoc (SduEntrypoint store arg) where
  typeDocMdDescription =
    "Public upgradeable entrypoint of a contract."
  typeDocMdReference tp =
    customTypeDocMdReference ("SduEntrypoint", DType tp) [DType (Proxy @arg)]
  typeDocHaskellRep =
    concreteTypeDocHaskellRep @(SduEntrypoint () Integer)
  typeDocMichelsonRep =
    concreteTypeDocMichelsonRep @(SduEntrypoint () Integer)
  typeDocDependencies p = mconcat
    [ [SomeDocDefinitionItem $ DUStoreTemplate $ Proxy @()]
        --- ^ for example of repr
    , genericTypeDocDependencies p
    ]

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

-- | Public entrypoint of a contract kept in 'UStore'.
--
-- These are mere 'UStore' fields but treated specially by 'mkSduContract'
-- function which produces 'UContractRouter' capable of calling these
-- entrypoints.
--
-- This type is not intended for keeping internal code, in such case consider
-- using 'UStoreField' instead.
type UStoreEntrypoint store arg =
  UStoreFieldExt UMarkerEntrypoint (SduEntrypoint store arg)
data UMarkerEntrypoint :: UStoreMarkerType

-- | Access code of 'UStoreEntrypoint'.
pattern UStoreEntrypoint :: SduEntrypointUntyped store -> UStoreEntrypoint store arg
pattern UStoreEntrypoint code = UStoreField (SduEntrypoint code)

type UStoreEpKey = (Lambda () (), MText)

instance KnownUStoreMarker UMarkerEntrypoint where
  mkFieldMarkerUKey field =
    -- Using special encoding to avoid finding non-entrypoints in parameter
    -- dispatch.
    -- Packing an empty lambda is quite cheap, and seems to fit semantically
    -- best.
    lPackValueRaw @UStoreEpKey (L.nop, field)

  type ShowUStoreField UMarkerEntrypoint (SduEntrypoint store arg) =
    'Text "entrypoint with argument " ':<>: 'ShowType arg ':<>:
    'Text " over storage " ':<>: 'ShowType store

instance UStoreMarkerHasDoc UMarkerEntrypoint where
  ustoreMarkerKeyEncoding k = "pack ({} :: lambda, " <> k <> ")"

----------------------------------------------------------------------------
-- Logic
----------------------------------------------------------------------------

-- | Get the set of entrypoints (i.e. 'UStoreEntrypoint' entries) stored in UStore
-- with given template.
type UStoreEpInterface utemplate =
  ExtractInterface utemplate (PickMarkedFields UMarkerEntrypoint utemplate)

type family ExtractInterface (utemplate :: Kind.Type) (ufields :: [(Symbol, Kind.Type)])
              :: [EntrypointKind] where
  ExtractInterface _ '[] = '[]
  ExtractInterface utemplate (entry ': entries) =
    ExtractEntrypoint utemplate entry ': ExtractInterface utemplate entries

type family ExtractEntrypoint (utemplate :: Kind.Type) (ufields :: (Symbol, Kind.Type))
              :: EntrypointKind where
  ExtractEntrypoint utemplate '(name, SduEntrypoint utemplate arg) =
    name ?: arg
  ExtractEntrypoint _ '(name, SduEntrypoint (UStore _) _) =
    TypeError ('Text "UStore passed to entrypoint, expected UStore template" ':$$:
               'Text "In UStore field " ':<>: 'ShowType name
              )
  ExtractEntrypoint utemplate' '(name, SduEntrypoint utemplate _) =
    TypeError ('Text "Entrypoint polymorphic over foreign storage: UStore " ':<>:
               'ShowType utemplate ':$$:
               'Text "In storage UStore " ':<>: 'ShowType utemplate' ':$$:
               'Text "In field " ':<>: 'ShowType name
              )
  ExtractEntrypoint _ v =
    TypeError ('Text "Field with entrypoint of unknown type " ':<>: 'ShowType v)

-- | Construct 'UContractRouter' which allows calling all entrypoints stored
-- as 'UStoreEntrypoint' entries of 'UStore'.
mkSduContract
  :: (Typeable (VerUStoreTemplate ver))
  => SduFallback (VerUStoreTemplate ver) -> UContractRouter ver
mkSduContract fallback = mkUContractRouter $ do
  dup @(UParam _)
  unwrapUParam; car
  dip (duupX @2)
  -- Further fetching UStore field manually because field name comes from stack
  push nop; pair; packRaw @UStoreEpKey
  get
  if IsSome
  then do
    unpackRaw @(SduEntrypointUntyped _)
    -- This error normally should not occur by construction of @interface@ type
    assertSome [mt|Wrong sdu entrypoint type|]
    dip $ do
      unwrapUParam; cdr
      pair
    swap
    exec
  else do
    unwrapUParam
    pair
    fallback

-- | Construct public entrypoint.
mkSduEntrypoint
  :: NiceUnpackedValue arg
  => Entrypoint arg (UStore store)
  -> SduEntrypoint store arg
mkSduEntrypoint code = SduEntrypoint $ do
  unpair
  unpackRaw
  ifSome nop $ failCustom_ #uparamArgumentUnpackFailed
  code

-- | Construct public entrypoint for 'UStore'.
mkUStoreEntrypoint
  :: NiceUnpackedValue arg
  => Entrypoint arg (UStore store)
  -> UStoreEntrypoint store arg
mkUStoreEntrypoint = UStoreField . mkSduEntrypoint

-- | Call an entrypoint since it appeared on stack.
callSduEntrypoint
  :: NicePackedValue arg
  => arg : SduEntrypoint store arg : UStore store : s
     :-> ([Operation], UStore store) : s
callSduEntrypoint = do
  dip $ coerceUnwrap >> swap
  packRaw
  pair
  exec

-- | Call an entrypoint stored under the given field.
callUStoreEntrypoint
  :: (NicePackedValue arg, HasUField field (SduEntrypoint store arg) store)
  => Label field
  -> arg : UStore store : s :-> ([Operation], UStore store) : s
callUStoreEntrypoint label = do
  dip $ ustoreGetField label
  callSduEntrypoint

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

----------------------------------------------------------------------------
-- Documentation
----------------------------------------------------------------------------

-- | Gather documentation of entrypoints kept in given storage.
-- Unfortunatelly, this seems to be the only place where we can pick the code
-- for documenting it.
--
-- Note: in most cases you want to use this function is couple with
-- 'sduAddEntrypointDoc'.
sduDocument
  :: UStoreTraversable SduDocumentTW template
  => template -> Lambda () ()
sduDocument = foldUStore SduDocumentTW

data SduDocumentTW = SduDocumentTW

instance UStoreTraversalWay SduDocumentTW where
  type UStoreTraversalArgumentWrapper SduDocumentTW = Identity
  type UStoreTraversalMonad SduDocumentTW = Const (Lambda () ())

instance {-# OVERLAPPING #-}
         UStoreTraversalFieldHandler SduDocumentTW
           UMarkerEntrypoint (SduEntrypoint store arg) where
  ustoreTraversalFieldHandler
      SduDocumentTW (Label :: Label fieldName) (Identity (SduEntrypoint ep)) =
    Const $
    cutLorentzNonDoc $ clarifyParamBuildingSteps (pbsUParam @fieldName) ep

instance UStoreTraversalFieldHandler SduDocumentTW marker v where
  ustoreTraversalFieldHandler SduDocumentTW _ _ = Const mempty
instance UStoreTraversalSubmapHandler SduDocumentTW k v where
  ustoreTraversalSubmapHandler SduDocumentTW _ _ = Const mempty

-- | Mark all public code kept in given storage as atomic entrypoints.
--
-- Sometimes you want your 'SduEntrypoint's to contain multiple sub-entrypoints
-- inside, in this case using 'entryCase' function you get documentation for each
-- of sub-entrypoints automatically and calling this function is not necessary.
-- In case when this __does not__ hold and 'SduEntrypoint' keeps exactly one
-- entrypoint, you still need to mark it as such in order for 'sduDocument'
-- to handle it properly. This function does exactly that - it finds all
-- UStore entrypoints and marks them for documentation.
sduAddEntrypointDoc
  :: ( UStoreTraversable SduAddEntrypointDocTW template
     , DocItem (DEntrypoint epKind)
     )
  => Proxy epKind -> template -> template
sduAddEntrypointDoc epKindP = modifyUStore (SduAddEntrypointDocTW epKindP)

data SduAddEntrypointDocTW =
  -- I don't want this type to be polymorphic over @epKind@ because this way
  -- phantom type would appear in 'sduAddEntrypointDoc' signature and any
  -- helper over this function would need to write the respective constraint
  -- with @epKind@. So using existential quantification.
  forall epKind. (DocItem (DEntrypoint epKind)) =>
  SduAddEntrypointDocTW (Proxy epKind)

instance UStoreTraversalWay SduAddEntrypointDocTW where
  type UStoreTraversalArgumentWrapper SduAddEntrypointDocTW = Identity
  type UStoreTraversalMonad SduAddEntrypointDocTW = Identity

instance {-# OVERLAPPING #-}
         ( TypeHasDoc arg, NiceParameterFull arg
         ) =>
         UStoreTraversalFieldHandler SduAddEntrypointDocTW
           UMarkerEntrypoint (SduEntrypoint store arg) where
  ustoreTraversalFieldHandler
      (SduAddEntrypointDocTW (_ :: Proxy epKind))
      (Label :: Label fieldName) (Identity (SduEntrypoint ep)) =
    Identity . SduEntrypoint $
      docGroup (DEntrypoint @epKind (symbolValT' @fieldName))
        (doc (constructDEpArg @arg) # ep)

instance UStoreTraversalFieldHandler SduAddEntrypointDocTW marker v where
  ustoreTraversalFieldHandler _ _ = id
instance UStoreTraversalSubmapHandler SduAddEntrypointDocTW k v where
  ustoreTraversalSubmapHandler _ _ = id

-- | By given storage make up a fake contract which contains
-- documentation of all entrypoints declared by this storage.
--
-- Note: in most cases you want to use this function in couple with
-- 'sduAddEntrypointDoc'.
--
-- Note: we intentionally allow accepted @UStore@ template not to correspond
-- to the contract version storage, this is useful when one does not want to
-- provide the full storage (construction of which may require passing some
-- parameters), rather only part of storage with entrypoints.
sduContractDoc
  :: forall utemplate ver.
     ( NiceVersion ver
     , KnownContractVersion ver
     , UStoreTraversable SduDocumentTW utemplate
     , PermConstraint ver
     )
  => utemplate
  -> PermanentImpl ver
  -> Lambda () ()
sduContractDoc store permImpl = do
    doc $ DVersion (contractVersion (Proxy @ver))
    fakeCoercing $
      cCode $ upgradeableContract @ver
    finalizeParamCallingDoc @(Parameter 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 $
        sduDocument store
    finalizeParamCallingDoc @(Parameter ver) . fakeCoercing $
      clarifyParamBuildingSteps pbsContainedInRunPerm $
        unPermanentImpl permImpl