morley-upgradeable-0.3: src/Lorentz/Contracts/Upgradeable/Common/Interface.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
-- TODO: Replace 'Empty' with 'Never' from morley
{-# OPTIONS_GHC -Wno-deprecations #-}
-- | Type-safe interface for constructing contract upgrades.
--
-- Use this module as follows:
-- 1. Construct 'EpwUpgradeParameters';
-- 2. Use one of the respective functions to convert it to an actual upgrade,
-- one-shot or entrypoint-wise, for tests or production.
module Lorentz.Contracts.Upgradeable.Common.Interface
( EpwUpgradeParameters (..)
, fvUpgrade
, makeOneShotUpgradeParameters
, makeOneShotUpgrade
, makeEpwUpgrade
, UpgradeWay (..)
, SimpleUpgradeWay
, integrationalTestUpgrade
) where
import Data.Constraint ((:-)(..), Constraint, Dict(..), (\\))
import Data.Foldable (toList)
import qualified Data.Kind as Kind
import Lorentz
import Prelude (Identity(..), Traversable, Void, absurd, mapM_, maybe, (<$), (<$>))
import Data.Coerce (coerce)
import Unsafe.Coerce (unsafeCoerce)
import Lorentz.Test
import Util.Instances ()
import Util.Named ((.!))
import Util.TypeLits
import Lorentz.Contracts.Upgradeable.Common.Base
import Lorentz.Contracts.Upgradeable.Common.Contract
----------------------------------------------------------------------------
-- Particular pieces updates
----------------------------------------------------------------------------
-- These datatypes are not part of the interface, they only keep
-- information about upgrade and respective invariants
data UContractRouterUpdate curVer newVer where
-- | Do update.
UcrUpdate :: UContractRouter newVer -> UContractRouterUpdate curVer newVer
-- | Retain the same 'UContractRouter'.
UcrRetain :: UContractRouterUpdate curVer newVer
data PermanentImplUpdate curVer newVer where
-- | Do update.
PiUpdate :: PermanentImpl newVer -> PermanentImplUpdate curVer newVer
-- | Retain the same 'PermanentImpl'.
PiRetain :: PermanentImplUpdate curVer newVer
-- Interface conveniences
----------------------------------------------------------------------------
-- | Helps to provide a pleasant interface, it would be inconvenient for
-- user to use 'UcrUpdate' and stuff.
class RecognizeUpgPiece expected given where
recognizeUpgPiece :: given -> expected
instance (newVerE ~ newVerG) =>
RecognizeUpgPiece
(UContractRouterUpdate curVerE newVerE)
(UContractRouter newVerG) where
recognizeUpgPiece = UcrUpdate
instance ( RequireSameVersionStorageParts curVer newVer
"upgradeable part implementation"
, RequireSameVersionInterfaces curVer newVer
, x ~ Void
) =>
RecognizeUpgPiece (UContractRouterUpdate curVer newVer) (Maybe x) where
recognizeUpgPiece = maybe UcrRetain absurd
instance (newVerE ~ newVerG) =>
RecognizeUpgPiece
(PermanentImplUpdate curVerE newVerE)
(PermanentImpl newVerG) where
recognizeUpgPiece = PiUpdate
instance ( RequireSameVersionStorageParts curVer newVer
"permanent part implementation"
, RequireSameVersionPermanents curVer newVer
, x ~ Void
) =>
RecognizeUpgPiece (PermanentImplUpdate curVer newVer) (Maybe x) where
recognizeUpgPiece = maybe PiRetain absurd
-- Type errors
----------------------------------------------------------------------------
type family RequireSameStorageParts curStore newStore desc :: Constraint where
RequireSameStorageParts store store _ = ()
RequireSameStorageParts curStore newStore desc = TypeError
( 'Text "Leaving " ':<>: 'Text desc ':<>:
'Text " unchanged is not safe when storage format changes" ':$$:
'Text "Old storage is `" ':<>: 'ShowType curStore ':<>: 'Text "`" ':$$:
'Text "while new one is `" ':<>: 'ShowType newStore ':<>: 'Text "`"
)
-- Updates which error tells about are not safe because old code may refer
-- to a field which was removed in new version of storage.
type RequireSameVersionStorageParts curVer newVer desc =
RequireSameStorageParts
(VerUStoreTemplate curVer)
(VerUStoreTemplate newVer)
desc
type family RequireSameInterfaces curInterface newInterface :: Constraint where
RequireSameInterfaces interface interface = ()
RequireSameInterfaces curInterface newInterface = TypeError
( 'Text "Need to update interface" ':$$:
'Text "Old interface is `" ':<>: 'ShowType curInterface ':<>: 'Text "`" ':$$:
'Text "while new one is `" ':<>: 'ShowType newInterface ':<>: 'Text "`"
)
type RequireSameVersionInterfaces curVer newVer =
RequireSameInterfaces (VerInterface curVer) (VerInterface newVer)
type family RequireSamePermanents (curPerm :: Kind.Type) (newPerm :: Kind.Type)
:: Kind.Constraint where
RequireSamePermanents perm perm = ()
RequireSamePermanents curPerm Empty =
TypeError
( 'Text "Permanent part of contract version is set to default" ':$$:
'Text "while in existing contract version it is `"
':<>: 'ShowType curPerm ':<>: 'Text "`" ':$$:
'Text "Have you set it in KnownContractVersion instance?"
)
RequireSamePermanents curPerm newPerm =
TypeError
( 'Text "Need to update permanent part implementation" ':$$:
'Text "Parameter of previous version part is of type `"
':<>: 'ShowType curPerm ':<>: 'Text "`" ':$$:
'Text "while in new one it is `" ':<>: 'ShowType newPerm ':<>: 'Text "`"
)
type RequireSameVersionPermanents curVer newVer =
RequireSamePermanents (VerPermanent curVer) (VerPermanent newVer)
----------------------------------------------------------------------------
-- Exposed interface
----------------------------------------------------------------------------
-- | Type-safe upgrade construction.
data EpwUpgradeParameters (t :: Kind.Type -> Kind.Type)
(curVer :: VersionKind)
(newVer :: VersionKind) =
forall code codePerm.
( Traversable t
, KnownContractVersion curVer, KnownContractVersion newVer
, RequireSamePermanents (VerPermanent curVer) (VerPermanent newVer)
, RecognizeUpgPiece (UContractRouterUpdate curVer newVer) code
, RecognizeUpgPiece (PermanentImplUpdate curVer newVer) codePerm
) =>
EpwUpgradeParameters
{ upMigrationScripts :: t (MigrationScript (VerUStoreTemplate curVer) (VerUStoreTemplate newVer))
-- ^ Storage migration script.
-- Supply this field with result of 'migrationToScriptI' or
-- 'migrationToScripts' call.
, upNewCode :: code
-- ^ Updated parameter dispatching logic.
-- Pass 'UContractRouter' or 'Nothing'.
, upNewPermCode :: codePerm
-- ^ Updates implementation of permanent part.
-- Pass 'PermanentImpl' or 'Nothing'.
}
permanentsAreSameEvi
:: RequireSamePermanents (VerPermanent ver1) (VerPermanent ver2)
:- (VerPermanent ver1 ~ VerPermanent ver2)
permanentsAreSameEvi = Sub $
unsafeCoerce $ Dict @(Integer ~ Integer)
-- | New version getter.
upNewVersion
:: forall t curVer newVer.
EpwUpgradeParameters t curVer newVer -> Version
upNewVersion EpwUpgradeParameters{} =
contractVersion (Proxy @newVer)
-- | The current version getter.
upCurVersion
:: forall t curVer newVer.
EpwUpgradeParameters t curVer newVer -> Version
upCurVersion EpwUpgradeParameters{} =
contractVersion (Proxy @curVer)
-- | New 'UContractRouter' getter.
upNewCode'
:: forall curVer newVer t.
EpwUpgradeParameters t curVer newVer -> Maybe (UContractRouter newVer)
upNewCode' EpwUpgradeParameters{..} =
case recognizeUpgPiece @(UContractRouterUpdate curVer newVer) upNewCode of
UcrUpdate code -> Just code
UcrRetain -> Nothing
-- | New 'PermanentImpl' getter.
upNewPermCode'
:: forall curVer newVer t.
EpwUpgradeParameters t curVer newVer -> Maybe (PermanentImpl newVer)
upNewPermCode' EpwUpgradeParameters{..} =
case recognizeUpgPiece @(PermanentImplUpdate curVer newVer) upNewPermCode of
PiUpdate code -> Just code
PiRetain -> Nothing
-- | Make up a "fixed version" upgrade.
-- As argument you supply result of 'migrationToScriptI' or 'migrationToScripts'
-- and entrypoint-wise migration will be used inside.
--
-- Use this method in case you need to authoritatively perform arbitrary
-- modifications of contract storage.
fvUpgrade
:: forall ver t.
(KnownContractVersion ver, Traversable t)
=> t (MigrationScript (VerUStoreTemplate ver) (VerUStoreTemplate ver))
-> EpwUpgradeParameters t ver ver
fvUpgrade migrationScripts = EpwUpgradeParameters
{ upMigrationScripts = migrationScripts
, upNewCode = Nothing
, upNewPermCode = Nothing
}
-- | Construct 'OneShotUpgradeParameters'.
--
-- Naturally, you can construct this kind of upgrade only if your migration
-- has exactly one stage; for batched migrations use 'makeEpwUpgrade'.
makeOneShotUpgradeParameters
:: forall curVer newVer.
EpwUpgradeParameters Identity curVer newVer
-> OneShotUpgradeParameters curVer
makeOneShotUpgradeParameters epw@EpwUpgradeParameters{} =
( #currentVersion .!
upCurVersion epw
, #newVersion .!
upNewVersion epw
, #migrationScript .!
checkedCoerce (runIdentity $ upMigrationScripts epw)
, #newCode .! (coerceUContractRouter <$> upNewCode' epw)
, #newPermCode .! (checkedCoerce <$> upNewPermCode' epw)
\\ permanentsAreSameEvi @curVer @newVer
)
-- | Construct a call which should be performed in order to perform migration.
makeOneShotUpgrade
:: forall oldVer newVer.
(EpwUpgradeParameters Identity oldVer newVer)
-> Parameter oldVer
makeOneShotUpgrade =
Upgrade . makeOneShotUpgradeParameters
-- | Construct calls which should be performed in order to perform full
-- entrypoint-wise migration.
makeEpwUpgrade
:: forall curVer newVer t.
(EpwUpgradeParameters t curVer newVer)
-> [Parameter curVer]
makeEpwUpgrade epw@EpwUpgradeParameters{} =
mconcat
[ [EpwBeginUpgrade (#current .! upCurVersion epw, #new .! upNewVersion epw)]
, EpwApplyMigration . checkedCoerce <$> toList (upMigrationScripts epw)
, [EpwSetCode $ coerceUContractRouter code
| Just code <- pure $ upNewCode' epw
]
, [EpwSetPermCode $ checkedCoerce code
| Just code <- pure $ upNewPermCode' epw
]
\\ permanentsAreSameEvi @curVer @newVer
, [EpwFinishUpgrade]
]
-- | Way of performing an upgrade.
data UpgradeWay (t :: Kind.Type -> Kind.Type) where
-- | Perform upgrade in a single transaction.
-- This, naturally, cannot be used with batched migrations.
UpgOneShot :: UpgradeWay Identity
-- | Perform upgrade calling one entrypoint per transaction.
UpgEntrypointWise :: UpgradeWay t
deriving stock instance Show (UpgradeWay t)
-- | 'UpgradeWay' which can be used with simple (non-batched) migrations.
type SimpleUpgradeWay = UpgradeWay Identity
-- | Perform a contract upgrade in an integrational test scenario.
integrationalTestUpgrade
:: (PermConstraint curVer)
=> EpwUpgradeParameters t curVer newVer
-> UpgradeWay t
-> UTAddress curVer
-> IntegrationalScenarioM (UTAddress newVer)
integrationalTestUpgrade upgParams way addr =
coerce addr <$ case way of
UpgOneShot -> lCallDef addr (makeOneShotUpgrade upgParams)
UpgEntrypointWise -> mapM_ (lCallDef addr) (makeEpwUpgrade upgParams)