packages feed

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)