-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
-- | Module with various helpers that are used in morley-client fake tests.
module Test.Util
( chainOperationHandlers
, dumbContractState
, dumbImplicitContractState
, dumbManagerKey
, mapToContractStateBigMap
, handleGetBigMapValue
-- * Internals
, handleRunOperationInternal
, assertHeadBlockId
) where
import Control.Exception.Safe (throwString)
import Control.Lens (at, (?~))
import Data.Aeson (encode)
import Data.ByteArray (ScrubbedBytes)
import Data.ByteString.Lazy qualified as LBS (toStrict)
import Data.Map as Map (elems, insert, lookup, toList)
import Data.Singletons (demote)
import Fmt ((+|), (|+))
import Network.HTTP.Types.Status (status404)
import Network.HTTP.Types.Version (http20)
import Servant.Client.Core
(BaseUrl(..), ClientError(..), RequestF(..), ResponseF(..), Scheme(..), defaultRequest)
import Text.Hex (encodeHex)
import Lorentz as L (compileLorentz, drop)
import Lorentz.Constraints
import Lorentz.Pack
import Morley.Client.RPC.Types
import Morley.Client.Types
import Morley.Micheline
import Morley.Michelson.Typed
import Morley.Tezos.Address
import Morley.Tezos.Address.Alias (AddressOrAlias(..), Alias(..))
import Morley.Tezos.Core
import Morley.Tezos.Crypto
import Morley.Tezos.Crypto.Ed25519 qualified as Ed25519
import Morley.Util.ByteString
import TestM
-- | Function to convert given map to big map representation
-- used in fake state.
mapToContractStateBigMap
:: forall k v. (NicePackedValue k, NicePackedValue v)
=> BigMapId k v -> Map k v -> ContractStateBigMap
mapToContractStateBigMap (BigMapId bigMapId) map' = ContractStateBigMap
{ csbmKeyType = toExpression $ demote @(ToT k)
, csbmValueType = toExpression $ demote @(ToT v)
, csbmMap = fromList $
map (bimap (encodeBase58Check . valueToScriptExpr) lEncodeValue) $
Map.toList map'
, csbmId = bigMapId
}
-- | Initial simple contract fake state.
dumbContractState :: ContractState
dumbContractState = ContractState
{ csCounter = 100500
, csAlias = "genesis2"
, csContractData = ContractData
OriginationScript
{ osCode = toExpression $ compileLorentz L.drop
, osStorage = toExpression $ toVal ()
}
Nothing
}
dumbImplicitContractState :: ContractState
dumbImplicitContractState = ContractState
{ csCounter = 100500
, csAlias = "genesis1"
, csContractData = ImplicitContractData Nothing
}
-- | Fake handlers used for transaction sending and contract origination.
chainOperationHandlers :: Monad m => Handlers (TestT m)
chainOperationHandlers = defaultHandlers
{ hGetBlockHash = handleGetBlockHash
, hGetCounter = handleGetCounter
, hGetBlockConstants = handleGetBlockConstants
, hGetProtocolParameters = handleGetProtocolParameters
, hRunOperation = handleRunOperation
, hPreApplyOperations = mapM . handlePreApplyOperation
, hForgeOperation = handleForgeOperation
, hInjectOperation = pure . OperationHash . (<> "_injected") . encodeHex . unHexJSONByteString
, hGetContractScript = handleGetContractScript
, hSignBytes =
\_ _ -> pure . SignatureEd25519 . Ed25519.sign testSecretKey
, hWaitForOperation = id
, hGetAlias = handleGetAlias
, hResolveAddressMaybe = handleResolveAddressMaybe
, hRememberContract = handleRememberContract
, hCalcTransferFee = \_ _ _ _ -> pure $ [TezosMutez [tz|100500u|]]
, hCalcOriginationFee = \_ -> pure $ TezosMutez [tz|100500u|]
, hGetKeyPassword = \_ -> pure Nothing
, hGenKey = handleGenKey
, hGetManagerKey = handleGetManagerKey
, hRevealKey = handleRevealKey
}
where
testSecretKey :: Ed25519.SecretKey
testSecretKey = Ed25519.detSecretKey "\001\002\003\004"
mkRunOperationResult :: [Address] -> RunOperationResult
mkRunOperationResult originatedContracts = RunOperationResult
{ rrOperationContents =
one $ OperationContent $ RunMetadata
{ rmOperationResult = OperationApplied $
AppliedResult 100500 100500 100500 originatedContracts 0
, rmInternalOperationResults = []
}
}
handleGetBlockHash :: Monad m => BlockId -> TestT m BlockHash
handleGetBlockHash blkId = do
unless (blkId == FinalHeadId) do
throwString "Expected `getBlockHash` to be called with `head~2`."
FakeState{..} <- get
pure fsFinalHeadBlock
handleGetCounter
:: ( MonadState FakeState m
, MonadThrow m
)
=> BlockId -> Address -> m TezosInt64
handleGetCounter blk addr = do
assertHeadBlockId blk
FakeState{..} <- get
case lookup addr fsContracts of
Nothing -> throwM $ UnknownContract $ AddressResolved addr
Just ContractState{..} -> pure $ csCounter
handleGetBlockConstants
:: MonadState FakeState m
=> BlockId -> m BlockConstants
handleGetBlockConstants blkId = do
FakeState{..} <- get
pure $ fsBlockConstants blkId
handleGetProtocolParameters
:: (MonadState FakeState m, MonadThrow m)
=> BlockId -> m ProtocolParameters
handleGetProtocolParameters blk = do
assertHeadBlockId blk
FakeState{..} <- get
pure $ fsProtocolParameters
handleRunOperation :: Monad m => BlockId -> RunOperation -> TestT m RunOperationResult
handleRunOperation blk RunOperation{..} = do
assertHeadBlockId blk
FakeState{..} <- get
-- Ensure that passed chain id matches with one that fake state has
unless (roChainId == bcChainId (fsBlockConstants blk)) (throwM $ InvalidChainId)
-- As of release of the ithaca protocol, the "branch" field should be "head~2".
-- https://web.archive.org/web/20220305165609/https://tezos.gitlab.io/protocols/tenderbake.html
unless (roiBranch roOperation == fsFinalHeadBlock) do
throwM $ InvalidBranch $ roiBranch roOperation
originatedContracts <- handleRunOperationInternal roOperation
pure $ mkRunOperationResult originatedContracts
handlePreApplyOperation :: Monad m => BlockId -> PreApplyOperation -> TestT m RunOperationResult
handlePreApplyOperation blk PreApplyOperation{..} = do
assertHeadBlockId blk
FakeState{..} <- get
-- Ensure that passed protocol matches with one that mock state has
unless (paoProtocol == bcProtocol (fsBlockConstants blk)) $
throwM InvalidProtocol
-- As of release of the ithaca protocol, the "branch" field should be "head~2".
-- https://web.archive.org/web/20220305165609/https://tezos.gitlab.io/protocols/tenderbake.html
unless (paoBranch == fsFinalHeadBlock) do
throwM $ InvalidBranch paoBranch
originatedContracts <- concatMapM handleTransactionOrOrigination paoContents
pure $ mkRunOperationResult originatedContracts
handleForgeOperation :: Monad m => BlockId -> ForgeOperation -> TestT m HexJSONByteString
handleForgeOperation blkId op = do
assertHeadBlockId blkId
ms <- get
-- As of release of the ithaca protocol, the "branch" field should be "head~2".
-- https://web.archive.org/web/20220305165609/https://tezos.gitlab.io/protocols/tenderbake.html
unless (foBranch op == fsFinalHeadBlock ms) do
throwM $ InvalidBranch $ foBranch op
pure . HexJSONByteString . LBS.toStrict . encode $ op
handleRunOperationInternal :: Monad m => RunOperationInternal -> TestT m [Address]
handleRunOperationInternal RunOperationInternal{..} = do
concatMapM handleTransactionOrOrigination roiContents
handleTransactionOrOrigination
:: Monad m => OperationInput -> TestT m [Address]
handleTransactionOrOrigination op = do
FakeState{..} <- get
case wcoCustom op of
-- Ensure that transaction sender exists
OpTransfer TransactionOperation{..} -> case lookup codSource fsContracts of
Nothing -> throwM $ UnknownContract $ AddressResolved codSource
Just ContractState{..} -> do
-- Ensure that sender counter matches
unless (csCounter + 1 == codCounter) (throwM CounterMismatch)
case lookup toDestination fsContracts of
Nothing -> throwM $ UnknownContract $ AddressResolved toDestination
Just _ -> pure []
-- Ensure that originator exists
OpOriginate _ -> case lookup codSource fsContracts of
Nothing -> throwM $ UnknownContract $ AddressResolved codSource
Just ContractState{..} -> do
-- Ensure that originator counter matches
unless (csCounter + 1 == codCounter) (throwM CounterMismatch)
pure [dummyContractAddr]
where
dummyContractAddr = unsafe $ parseAddress "KT1LZwEZqbqtLYhwzaidBp6So9LgYDpkpEv7"
OpReveal _ ->
-- We do not care about reveals at the moment
return []
where
CommonOperationData{..} = wcoCommon op
-- | In most places, @morley-client@ executes operations against the @head@ block.
assertHeadBlockId :: (HasCallStack, MonadThrow m) => BlockId -> m ()
assertHeadBlockId blockId = unless (blockId == HeadId) $
throwString "Accessing non-head block is not supported in tests"
handleGetContractScript
:: ( MonadState FakeState m
, MonadThrow m
)
=> BlockId
-> Address
-> m OriginationScript
handleGetContractScript blockId addr = do
assertHeadBlockId blockId
FakeState{..} <- get
case lookup addr fsContracts of
Nothing -> throwM $ err404 path
Just ContractState{..} -> case csContractData of
ImplicitContractData _ -> throwM $ UnexpectedImplicitContract addr
ContractData script _ -> pure script
where
path = "/chains/main/blocks/head/context/contracts/" <> formatAddress addr <> "/script"
handleGetBigMapValue :: Monad m => BlockId -> Natural -> Text -> TestT m Expression
handleGetBigMapValue blockId bigMapId scriptExpr = do
assertHeadBlockId blockId
st <- get
let allBigMaps :: [ContractStateBigMap] =
catMaybes $
Map.elems (fsContracts st) <&> \cs -> case (csContractData cs) of
ContractData _ bigMapMaybe -> bigMapMaybe
ImplicitContractData _ -> Nothing
-- Check if a big_map with the given ID exists and, if so, check
-- whether the giv en key exists.
case find (\bigMap -> csbmId bigMap == bigMapId) allBigMaps of
Nothing -> throwM $ err404 path
Just bigMap ->
case lookup scriptExpr (csbmMap bigMap ) of
Nothing -> throwM $ err404 path
Just serializedValue -> pure $ decodeExpression serializedValue
where
path = "/chains/main/blocks/head/context/big_maps/" <> show bigMapId <> "/" <> scriptExpr
handleRememberContract :: Monad m => Bool -> Address -> Alias -> TestT m ()
handleRememberContract replaceExisting addr alias = do
let
cs = dumbContractState { csAlias = alias }
remember addr' cs' FakeState{..} =
modify $ \s -> s { fsContracts = insert addr' cs' fsContracts }
st@FakeState{..} <- get
case lookup addr fsContracts of
Nothing -> remember addr cs st
_ -> bool pass (remember addr cs st) replaceExisting
handleGenKey :: Monad m => Alias -> TestT m Address
handleGenKey alias = do
let
addr = detGenKeyAddress (encodeUtf8 $ unAlias alias)
newContractState = dumbImplicitContractState { csAlias = alias }
modify $ \s ->
s & fsContractsL . at addr ?~ newContractState
pure addr
handleGetAlias :: Monad m => AddressOrAlias -> TestT m Alias
handleGetAlias = \case
AddressAlias alias -> pure alias
AddressResolved addr -> do
FakeState{..} <- get
case lookup addr fsContracts of
Nothing -> throwM $ UnknownContract $ AddressResolved addr
Just ContractState{..} -> pure $ csAlias
handleGetManagerKey :: (Monad m) => BlockId -> Address -> TestT m (Maybe PublicKey)
handleGetManagerKey blk addr = do
assertHeadBlockId blk
s <- get
let mbCs = s ^. fsContractsL . at addr
case mbCs of
Just ContractState{..} -> case csContractData of
ImplicitContractData mbManagerKey -> pure mbManagerKey
ContractData _ _ -> throwString "Only implicit account can have a manager key"
Nothing -> throwM $ UnknownContract $ AddressResolved addr
-- In scenarios where the system under test checks for 404 errors, we
-- use this function to fake and simulate those errors.
err404 :: Text -> ClientError
err404 path = FailureResponse
(defaultRequest { requestBody = Nothing, requestPath = (baseUrl , "") })
response
where
baseUrl = BaseUrl
{ baseUrlScheme = Http
, baseUrlHost = "localhost"
, baseUrlPort = 8732
, baseUrlPath = toString path
}
response = Response
{ responseStatusCode = status404
, responseHeaders = mempty
, responseHttpVersion = http20
, responseBody = "Contract with given address not found"
}
handleResolveAddressMaybe :: Monad m => AddressOrAlias -> TestT m (Maybe Address)
handleResolveAddressMaybe = \case
AddressResolved addr -> pure (Just addr)
AddressAlias alias -> do
FakeState{..} <- get
case find checkAlias $ Map.toList fsContracts of
Just (addr, _) -> pure (Just addr)
Nothing -> pure Nothing
where
checkAlias (_, ContractState { csAlias = alias' }) = alias == alias'
handleRevealKey :: Monad m => Alias -> Maybe ScrubbedBytes -> TestT m ()
handleRevealKey alias _ = do
contracts <- gets (Map.toList . fsContracts)
let contracts' = filter (\(_, ContractState{..}) -> csAlias == alias) contracts
case contracts' of
[] -> throwM $ UnknownContract $ AddressAlias alias
[(addr, cs@ContractState{..})] ->
case (addr, csContractData) of
(ContractAddress _, ContractData _ _) ->
throwM $ CantRevealContract addr
(KeyAddress _, ImplicitContractData (Just _)) ->
throwM $ AlreadyRevealed addr
(KeyAddress _, ImplicitContractData Nothing) ->
-- We don't care about the public key itself, but only its presence.
let newContractState = cs { csContractData = ImplicitContractData $ Just dumbManagerKey }
in modify $ \s -> s & fsContractsL . at addr ?~ newContractState
_ -> error "Inconsitent fake state. This most likely a bug in tests."
_ ->
error $ "Multiple contracts have alias '" +| alias |+
"'. This is most likely a bug in tests."
-- | Dummy public key used in fake tests.
dumbManagerKey :: PublicKey
dumbManagerKey = fromRight (error "impossible") $ parsePublicKey
"edpkuwTWKgQNnhR5v17H2DYHbfcxYepARyrPGbf1tbMoGQAj8Ljr3V"