morley-upgradeable-0.3: src/Lorentz/Contracts/Upgradeable/Common/Base.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- TODO: Replace 'Empty' with 'Never' from morley
{-# OPTIONS_GHC -Wno-deprecations #-}
module Lorentz.Contracts.Upgradeable.Common.Base
( Version (..)
, VersionKind
, KnownContractVersion (..)
, VerParam
, VerUStore
, EmptyContractVersion
, SomeContractVersion
, UStore_
, MigrationScript (..)
, MigrationScriptFrom
, MigrationScriptTo
, UContractRouter (..)
, SomeUContractRouter
, UpgradeableEntrypointsKind
, PermanentImpl (..)
, SomePermanentImpl
, PermanentEntrypointsKind
, mkUContractRouter
, coerceUContractRouter
, emptyPermanentImpl
, mkSmallPermanentImpl
-- * Re-exports
, Nat
, Empty
, absurd_
) where
import Prelude (KnownNat, Num, Typeable, natVal)
import qualified Data.Kind as Kind
import Fmt (Buildable(..))
import GHC.TypeNats (Nat)
import Util.TypeTuple
import Lorentz
import Lorentz.Contracts.Upgradeable.Common.Doc (UpgradeableEntrypointsKind)
import Lorentz.UStore
import Lorentz.UStore.Migration
import Michelson.Typed.Arith
-- Versioning
----------------------------------------------------------------------------
-- | Version of a contract.
--
-- Our current versioning suggests that this type is a term-level reflection
-- of types which have 'KnownContractVersion' instance, so this version item
-- should uniquely identify storage structure and entrypoints set for a given
-- contract for all of its instances.
--
-- The old semantics of this type was that it counts number of given contract
-- instance upgrades, so different contract instances, being upgraded to the
-- recent version, could have different 'Version's. For old contracts we have
-- to follow this behaviour.
newtype Version = Version { unVersion :: Natural }
deriving stock (Show, Eq, Ord, Generic)
deriving newtype (Num, IsoValue)
deriving anyclass HasAnnotation
instance ArithOpHs Add Natural Version where
type ArithResHs Add Natural Version = Version
instance Buildable Version where
build (Version v) = "v" <> build v
instance TypeHasDoc Version where
typeDocMdDescription = "Contract version."
instance ParameterHasEntrypoints Version where
type ParameterEntrypointsDerivation Version = EpdNone
-- | Kind of type-level contract version.
type VersionKind =
-- Defining it like this as it is the simplest way to have a custom open kind
ContractVersionTag -> Kind.Type
data ContractVersionTag
-- | Declare given type as contract version identifier.
--
-- Instances of this typeclass (versions) uniquely identify contract storage
-- scheme and code. Normally the opposite should also hold, i.e.
-- @contract version <-> (contract storage scheme, code)@ relation is a bijection.
--
-- If as part of migration you need to update contract storage without modifying
-- its structure, then contract version should not change, and you should
-- perform an upgrade to the same version as the current one.
--
-- We allow upgrades between arbitrary two versions, so one can not only upgrade
-- to the next adjacent version, but also upgrade a new contract from V0 to the
-- recent version immediately, or leave version the same (as a versatile way
-- to change storage).
class KnownContractVersion (v :: VersionKind) where
-- | List of entrypoints of given contract version.
type VerInterface v :: [EntrypointKind]
-- | Storage template of given contract version.
type VerUStoreTemplate v :: Kind.Type
-- | Set of permanent entrypoints (as a sum type).
--
-- We tie this type to contract version for convenience, in order not to carry
-- one more type argument everywhere.
-- We do not ensure right here that all versions of a contract have the same
-- permanent entrypoints, but if this does not hold, then (ideally) it will
-- not be possible to construct migration between such contract versions.
type VerPermanent v :: Kind.Type
type VerPermanent _ = Empty
-- | Get term-level contract version.
-- Returned value will be stored within the contract designating the current
-- contract version.
contractVersion :: Proxy v -> Version
default contractVersion :: (v ~ cid ver, KnownNat ver) => Proxy v -> Version
contractVersion (_ :: Proxy (cid ver)) = Version $ natVal (Proxy @ver)
type VerParam v = UParam (VerInterface v)
type VerUStore v = UStore (VerUStoreTemplate v)
-- | Contract with empty interface and storage.
data EmptyContractVersion (perm :: Kind.Type) :: VersionKind
instance KnownContractVersion (EmptyContractVersion perm) where
type VerInterface (EmptyContractVersion perm) = '[]
type VerUStoreTemplate (EmptyContractVersion perm) = ()
type VerPermanent (EmptyContractVersion perm) = perm
contractVersion _ = 0
-- | Version which forgets about particular interface/storage.
data SomeContractVersion (perm :: Kind.Type) :: VersionKind
instance KnownContractVersion (SomeContractVersion perm) where
type VerInterface (SomeContractVersion perm) = SomeInterface
type VerUStoreTemplate (SomeContractVersion perm) = SomeUTemplate
type VerPermanent (SomeContractVersion perm) = perm
contractVersion _ = error "Requested version of SomeContractVersion"
-- UParam dispatching
----------------------------------------------------------------------------
-- | Keeps parameter dispatching logic.
newtype UContractRouter (ver :: VersionKind) =
UContractRouter
{ unUContractRouter
:: Lambda (VerParam ver, VerUStore ver)
([Operation], VerUStore ver)
}
deriving stock (Generic, Show)
deriving anyclass (IsoValue, HasAnnotation, Wrappable)
deriving newtype (MapLorentzInstr)
instance ( Typeable ver
, Typeable (VerInterface ver), Typeable (VerUStoreTemplate ver)
, TypeHasDoc (VerUStore ver)
) =>
TypeHasDoc (UContractRouter ver) where
typeDocMdDescription =
"Parameter dispatching logic, main purpose of this code is to pass control \
\to an entrypoint carrying the main logic of the contract."
typeDocMdReference tp = customTypeDocMdReference ("UContractRouter", DType tp) []
typeDocHaskellRep = homomorphicTypeDocHaskellRep
typeDocMichelsonRep = homomorphicTypeDocMichelsonRep
type SomeUContractRouter = UContractRouter (SomeContractVersion ())
mkUContractRouter
:: ([VerParam ver, VerUStore ver] :-> '[([Operation], VerUStore ver)])
-> UContractRouter ver
mkUContractRouter code = UContractRouter $ do
unpair
code
instance ( VerParam ver1 `CanCastTo` VerParam ver2
, VerUStore ver1 `CanCastTo` VerUStore ver2
) =>
UContractRouter ver1 `CanCastTo` UContractRouter ver2 where
castDummy = castDummyG
coerceUContractRouter
:: ( Coercible_ (VerParam s1) (VerParam s2)
, Coercible_ (VerUStore s1) (VerUStore s2)
)
=> UContractRouter s1 -> UContractRouter s2
coerceUContractRouter (UContractRouter code) =
UContractRouter $ checkedCoerce_ # code # checkedCoerce_
-- Permanent entrypoints parameter dispatching
----------------------------------------------------------------------------
-- | Implementation of permanent entrypoints.
--
-- This will be injected into contract storage as one of fields, so make sure
-- that code within does not exceed several instructions; an actual entrypoint
-- logic can be put into 'UStore' and called from within @PermanentImpl@ only
-- when necessary.
--
-- Regarding documentation - this have to provide code pieces wrapped into
-- 'DEntrypoint' with 'PermanentEntrypointsKind', so always use 'entryCase' as
-- implementation of this type /or/ inject documentation of code which does so
-- unless you know what you are doing.
newtype PermanentImpl ver = PermanentImpl
{ unPermanentImpl :: Entrypoint (VerPermanent ver) (VerUStore ver)
}
deriving stock (Generic, Show)
deriving newtype (MapLorentzInstr)
deriving anyclass (Wrappable)
deriving anyclass instance (WellTypedIsoValue (VerPermanent ver)) => IsoValue (PermanentImpl ver)
instance HasAnnotation (VerPermanent ver) => HasAnnotation (PermanentImpl ver)
instance ( Typeable ver, Typeable (VerUStoreTemplate ver)
, TypeHasDoc (VerUStore ver)
, TypeHasDoc (VerPermanent ver), KnownValue (VerPermanent ver)
) =>
TypeHasDoc (PermanentImpl ver) where
typeDocMdDescription =
"Implementation of permanent entrypoints."
typeDocMdReference tp = customTypeDocMdReference ("PermanentImpl", DType tp) []
typeDocHaskellRep = homomorphicTypeDocHaskellRep
typeDocMichelsonRep = homomorphicTypeDocMichelsonRep
type SomePermanentImpl perm = PermanentImpl (SomeContractVersion perm)
instance ( VerPermanent ver1 `CanCastTo` VerPermanent ver2
, VerUStore ver1 `CanCastTo` VerUStore ver2
) =>
PermanentImpl ver1 `CanCastTo` PermanentImpl ver2 where
castDummy = castDummyG
-- | Common implementation of permanent part in case contract has no such.
emptyPermanentImpl :: (VerPermanent ver ~ Empty) => PermanentImpl ver
emptyPermanentImpl = PermanentImpl $
docGroup (DEntrypoint @PermanentEntrypointsKind "<No entrypoints>")
absurd_
-- | Construct implementation of permanent part in a common case;
-- this works similarly to 'entryCase'.
--
-- Use this function only for very small implementations.
mkSmallPermanentImpl
:: forall ver dt out inp clauses.
( CaseTC dt out inp clauses
, DocumentEntrypoints PermanentEntrypointsKind dt
, dt ~ VerPermanent ver, inp ~ '[VerUStore ver]
, out ~ ContractOut (VerUStore ver)
)
=> IsoRecTuple clauses -> PermanentImpl ver
mkSmallPermanentImpl = PermanentImpl . entryCase (Proxy @PermanentEntrypointsKind)
-- | Common marker for permanent entrypoints.
-- Can be used when parameter for permanent entrypoints is flat, i.e. does not
-- have nested subparameters with multiple entrypoints.
data PermanentEntrypointsKind
instance EntrypointKindHasDoc PermanentEntrypointsKind where
entrypointKindPos = 1050
entrypointKindSectionName = "Permanent entrypoints"