morley-0.1.0.0: src/Morley/Runtime/GState.hs
-- | Global blockchain state (emulated).
module Morley.Runtime.GState
(
-- * Auxiliary types
ContractState (..)
, AddressState (..)
, asBalance
-- * GState
, GState (..)
, genesisAddress
, genesisAddressText
, genesisKeyHash
, initGState
, readGState
, writeGState
-- * Operations on GState
, GStateUpdate (..)
, GStateUpdateError (..)
, applyUpdate
, applyUpdates
) where
import Control.Lens (at)
import qualified Data.Aeson as Aeson
import Data.Aeson.Options (defaultOptions)
import Data.Aeson.TH (deriveJSON)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map.Strict as Map
import Fmt (genericF, pretty, (+|), (|+))
import Formatting.Buildable (Buildable(build))
import System.IO.Error (IOError, isDoesNotExistError)
import Morley.Aliases (UntypedContract, UntypedValue)
import Morley.Types ()
import Tezos.Address (Address(..))
import Tezos.Core (Mutez)
import Tezos.Crypto (KeyHash, parseKeyHash)
-- | State of a contract with code.
data ContractState = ContractState
{ csBalance :: !Mutez
-- ^ Amount of mutez owned by this contract.
, csStorage :: !UntypedValue
-- ^ Storage value associated with this contract.
, csContract :: !UntypedContract
-- ^ Contract itself (untyped).
} deriving (Show, Generic, Eq)
instance Buildable ContractState where
build = genericF
deriveJSON defaultOptions ''ContractState
-- | State of an arbitrary address.
data AddressState
= ASSimple !Mutez
-- ^ For contracts without code we store only its balance.
| ASContract !ContractState
-- ^ For contracts with code we store more state represented by
-- 'ContractState'.
deriving (Show, Generic, Eq)
instance Buildable AddressState where
build =
\case
ASSimple balance -> "Balance = " +| balance |+ ""
ASContract cs -> build cs
deriveJSON defaultOptions ''AddressState
-- | Extract balance from 'AddressState'.
asBalance :: AddressState -> Mutez
asBalance =
\case
ASSimple b -> b
ASContract cs -> csBalance cs
-- | Persistent data passed to Morley contracts which can be updated
-- as result of contract execution.
data GState = GState
{ gsAddresses :: Map Address AddressState
-- ^ All known addresses and their state.
} deriving Show
deriveJSON defaultOptions ''GState
-- | Initially this address has a lot of money.
genesisAddressText :: Text
genesisAddressText = "tz1Yz3VPaCNB5FjhdEVnSoN8Xv3ZM8g2LYhw"
-- | KeyHash of genesis address.
genesisKeyHash :: KeyHash
genesisKeyHash =
either (error . mappend "genesisKeyHash: " . pretty) id $
parseKeyHash genesisAddressText
-- | Initially this address has a lot of money.
genesisAddress :: Address
genesisAddress = KeyAddress genesisKeyHash
-- | Initial 'GState'. It's supposed to be used if no 'GState' is
-- provided. For now it's empty, but we can hardcode some dummy data
-- in the future.
initGState :: GState
initGState =
GState
{ gsAddresses = Map.fromList
[ (genesisAddress, ASSimple maxBound)
]
}
data GStateParseError =
GStateParseError String
deriving Show
instance Exception GStateParseError where
displayException (GStateParseError str) = "Failed to parse GState: " <> str
-- | Read 'GState' from a file.
readGState :: FilePath -> IO GState
readGState fp = (LBS.readFile fp >>= parseFile) `catch` onExc
where
parseFile :: LByteString -> IO GState
parseFile = either (throwM . GStateParseError) pure . Aeson.eitherDecode'
onExc :: IOError -> IO GState
onExc exc
| isDoesNotExistError exc = pure initGState
| otherwise = throwM exc
-- | Write 'GState' to a file.
writeGState :: FilePath -> GState -> IO ()
writeGState fp gs = LBS.writeFile fp (Aeson.encode gs)
-- | Updates that can be applied to 'GState'.
data GStateUpdate
= GSAddAddress !Address
!AddressState
| GSSetStorageValue !Address
!UntypedValue
| GSSetBalance !Address
!Mutez
deriving (Show, Eq)
instance Buildable GStateUpdate where
build =
\case
GSAddAddress addr st ->
"Add address " +| addr |+ " with state " +| st |+ ""
GSSetStorageValue addr val ->
"Set storage value of address " +| addr |+ " to " +| val |+ ""
GSSetBalance addr balance ->
"Set balance of address " +| addr |+ " to " +| balance |+ ""
data GStateUpdateError
= GStateAddressExists !Address
| GStateUnknownAddress !Address
| GStateNotContract !Address
deriving (Show)
instance Buildable GStateUpdateError where
build =
\case
GStateAddressExists addr -> "Address already exists: " <> build addr
GStateUnknownAddress addr -> "Unknown address: " <> build addr
GStateNotContract addr -> "Address doesn't have contract: " <> build addr
-- | Apply 'GStateUpdate' to 'GState'.
applyUpdate :: GStateUpdate -> GState -> Either GStateUpdateError GState
applyUpdate =
\case
GSAddAddress addr st ->
maybeToRight (GStateAddressExists addr) . addAddress addr st
GSSetStorageValue addr newValue -> setStorageValue addr newValue
GSSetBalance addr newBalance -> setBalance addr newBalance
-- | Apply a list of 'GStateUpdate's to 'GState'.
applyUpdates :: [GStateUpdate] -> GState -> Either GStateUpdateError GState
applyUpdates = flip (foldM (flip applyUpdate))
-- | Add an address if it hasn't been added before.
addAddress :: Address -> AddressState -> GState -> Maybe GState
addAddress addr st gs
| addr `Map.member` accounts = Nothing
| otherwise = Just (gs {gsAddresses = accounts & at addr .~ Just st})
where
accounts = gsAddresses gs
-- | Updare storage value associated with given address.
setStorageValue ::
Address -> UntypedValue -> GState -> Either GStateUpdateError GState
setStorageValue addr newValue = updateAddressState addr modifier
where
modifier (ASSimple _) = Left (GStateNotContract addr)
modifier (ASContract cs) = Right $ ASContract $ cs { csStorage = newValue }
-- | Updare storage value associated with given address.
setBalance :: Address -> Mutez -> GState -> Either GStateUpdateError GState
setBalance addr newBalance = updateAddressState addr (Right . modifier)
where
modifier (ASSimple _) = ASSimple newBalance
modifier (ASContract cs) = ASContract (cs {csBalance = newBalance})
updateAddressState ::
Address
-> (AddressState -> Either GStateUpdateError AddressState)
-> GState
-> Either GStateUpdateError GState
updateAddressState addr f gs =
case addresses ^. at addr of
Nothing -> Left (GStateUnknownAddress addr)
Just as -> do
newState <- f as
return $ gs { gsAddresses = addresses & at addr .~ Just newState }
where
addresses = gsAddresses gs