-- 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
, mapToContractStateBigMap
, handleGetBigMapValue
, genesisState
, revealKeyState
, addrAndAliasFromGenesisState
-- * Internals
, handleRunOperationInternal
, assertHeadBlockId
) where
import Control.Exception.Safe (throwString)
import Control.Lens (at, (?=), (?~))
import Data.Aeson (encode)
import Data.ByteString.Lazy qualified as LBS (toStrict)
import Data.Singletons (demote)
import Fmt (pretty, (+|), (|+))
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.TezosClient (AliasBehavior(..), TezosClientError(DuplicateAlias))
import Morley.Client.Types
import Morley.Micheline
import Morley.Michelson.Runtime.GState
import Morley.Michelson.Typed
import Morley.Tezos.Address
import Morley.Tezos.Address.Alias
import Morley.Tezos.Address.Kinds
import Morley.Tezos.Crypto
import Morley.Tezos.Crypto.Ed25519 qualified as Ed25519
import Morley.Util.ByteString
import Morley.Util.Peano
import Morley.Util.SizedList qualified as Sized
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) $ toPairs map'
, csbmId = bigMapId
}
-- | Initial simple contract fake state.
dumbContractState :: AccountState 'AddressKindContract
dumbContractState = AccountState
{ asCounter = 100500
, asAlias = "genesis2"
, asAccountData = ContractData
OriginationScript
{ osCode = toExpression $ compileLorentz L.drop
, osStorage = toExpression $ toVal ()
}
Nothing
}
dumbImplicitState :: PublicKey -> (ImplicitAddress, AccountState 'AddressKindImplicit)
dumbImplicitState pk = (mkKeyAddress pk, AccountState
{ asCounter = 100500
, asAlias = "genesis1"
, asAccountData = ImplicitData pk 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
, hGetAliasesAndAddresses = handleGetAliasesAndAddresses
, hRememberContract = handleRememberContract
, hGetKeyPassword = \_ -> pure Nothing
, hGenKey = handleGenKey
, hGetManagerKey = handleGetManagerKey
, hGetPublicKey = handleGetPublicKey
}
where
testSecretKey :: Ed25519.SecretKey
testSecretKey = Ed25519.detSecretKey "\001\002\003\004"
mkRunOperationResult :: NonEmpty AppliedResult -> RunOperationResult
mkRunOperationResult results = RunOperationResult
{ rrOperationContents = results <&> \ar ->
OperationContent $ RunMetadata
{ rmOperationResult = OperationApplied ar
, rmInternalOperationResults = []
}
}
mkAppliedResult :: [ContractAddress] -> AppliedResult
mkAppliedResult originatedContracts = AppliedResult 100500 100500 100500 originatedContracts 0
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 -> ImplicitAddress -> m TezosInt64
handleGetCounter blk addr = do
assertHeadBlockId blk
use (fsImplicitsL . at addr) >>= \case
Nothing -> throwM $ UnknownAccount $ Constrained addr
Just AccountState{..} -> pure $ asCounter
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
mkRunOperationResult <$> handleRunOperationInternal roOperation
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
results <- handleOperationInput paoContents
pure $ mkRunOperationResult results
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 (NonEmpty AppliedResult)
handleRunOperationInternal RunOperationInternal{roiContents} = handleOperationInput roiContents
handleOperationInput :: Monad m => NonEmpty OperationInput -> TestT m (NonEmpty AppliedResult)
handleOperationInput (c :| cs) =
(:|) <$> go c 1 <*> zipWithM go cs [2, 3..]
where go = fmap mkAppliedResult ... handleTransactionOrOrigination
handleTransactionOrOrigination
:: forall m. (Monad m, HasCallStack) => OperationInput -> TezosInt64 -> TestT m [ContractAddress]
handleTransactionOrOrigination op n = do
FakeState{..} <- get
AccountState{..} <-
maybe (throwM $ UnknownAccount $ Constrained codSource) pure $ fsImplicits ^. at codSource
unless ((asCounter + n) == codCounter) $ throwM CounterMismatch
let checkContractExists :: Address -> TestT m ()
checkContractExists dest'@(MkAddress dest) = case dest of
ContractAddress{} -> when (isNothing $ fsContracts ^. at dest) $
throwM $ UnknownAccount dest'
ImplicitAddress{} -> when (isNothing $ fsImplicits ^. at dest) $
throwM $ UnknownAccount dest'
SmartRollupAddress{} -> error "smart rollup unsupported"
case wcoCustom op of
OpTransfer TransactionOperation{toDestination} -> do
checkContractExists toDestination
pure []
OpTransferTicket TransferTicketOperation{ttoDestination} -> do
checkContractExists ttoDestination
pure []
OpOriginate{} ->
pure [[ta|KT1LZwEZqbqtLYhwzaidBp6So9LgYDpkpEv7|]]
OpReveal RevealOperation{..} -> do
let addr = mkKeyAddress roPublicKey
modify $ fsImplicitsL . at addr %~ fmap revealKeyState
return []
OpDelegation _ ->
-- We do not care about delegations 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
-> ContractAddress
-> m OriginationScript
handleGetContractScript blockId addr = do
assertHeadBlockId blockId
use (fsContractsL . at addr) >>= \case
Nothing -> throwM $ err404 path
Just AccountState{..} -> case asAccountData of
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 $
toList (fsContracts st) <&> \cs -> case (asAccountData cs) of
ContractData _ bigMapMaybe -> bigMapMaybe
-- 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 csbmMap bigMap ^. at scriptExpr 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 => AliasBehavior -> ContractAddress -> ContractAlias -> TestT m ()
handleRememberContract DontSaveAlias _ _ = pass
handleRememberContract replaceExisting addr alias = do
let
cs = dumbContractState { asAlias = alias }
remember addr' cs' = fsContractsL . at addr' ?= cs'
use (fsContractsL . at addr) >>= \case
Nothing -> remember addr cs
_ -> case replaceExisting of
KeepDuplicateAlias -> pass
OverwriteDuplicateAlias -> remember addr cs
ForbidDuplicateAlias -> throwM $ DuplicateAlias $ unAlias alias
handleGenKey :: Monad m => ImplicitAlias -> TestT m ImplicitAddressWithAlias
handleGenKey alias = do
let
pk = toPublic . detSecretKey . encodeUtf8 $ unAlias alias
(addr, newAccountState) = dumbImplicitState pk
modify $ fsImplicitsL . at addr ?~ newAccountState{ asAlias = alias }
pure $ AddressWithAlias addr alias
handleGetAliasesAndAddresses
:: forall m. Monad m
=> TestT m [(Text, Text)]
handleGetAliasesAndAddresses = do
FakeState{fsContracts, fsImplicits} <- get
pure $ convert fsContracts <> convert fsImplicits
where
convert :: Map (KindedAddress kind) (AccountState kind) -> [(Text, Text)]
convert m =
toPairs m <&> \(addr, AccountState{asAlias}) -> (pretty asAlias, pretty addr)
handleGetManagerKey :: (Monad m) => BlockId -> ImplicitAddress -> TestT m (Maybe PublicKey)
handleGetManagerKey blk addr = do
assertHeadBlockId blk
s <- get
let mbCs = s ^. fsImplicitsL . at addr
case mbCs of
Just AccountState{..} -> pure $ idManagerKey asAccountData
Nothing -> throwM $ UnknownAccount $ Constrained 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"
}
handleGetPublicKey :: Monad m => ImplicitAddressWithAlias -> TestT m PublicKey
handleGetPublicKey aaa@(AddressWithAlias addr alias) = do
accounts <- gets (toPairs . fsImplicits)
let accounts' = filter check accounts
check (addr', AccountState{..}) = addr' == addr && asAlias == alias
case accounts' of
[] -> throwM $ UnknownAccount $ Constrained addr
[(_, AccountState{..})] -> pure $ idPublicKey asAccountData
_ ->
error $ "Multiple accounts found for '" +| aaa |+
"'. This is most likely a bug in tests."
genesisPk :: forall n. (SingIPeano n, ToPeano 10 > ToPeano n ~ 'True) => PublicKey
genesisPk = toPublic $ Sized.index @n genesisSecrets
genesisState
:: forall n. (SingIPeano n, ToPeano 10 > ToPeano n ~ 'True)
=> (ImplicitAddress, AccountState 'AddressKindImplicit)
genesisState = dumbImplicitState $ genesisPk @n
addrAndAliasFromGenesisState
:: forall n. (SingIPeano n, ToPeano 10 > ToPeano n ~ 'True)
=> ImplicitAddressWithAlias
addrAndAliasFromGenesisState =
let (addr, AccountState{asAlias}) = genesisState @n
in AddressWithAlias addr asAlias
revealKeyState :: AccountState 'AddressKindImplicit -> AccountState 'AddressKindImplicit
revealKeyState = asAccountDataL %~ \x@ImplicitData{..} -> x{ idManagerKey = Just idPublicKey }