packages feed

morley-client-0.2.0: src/Morley/Client/RPC/Getters.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | Some read-only actions (wrappers over RPC calls).

module Morley.Client.RPC.Getters
  ( ValueDecodeFailure (..)
  , ValueNotFound (..)

  , readAllBigMapValues
  , readAllBigMapValuesMaybe
  , readContractBigMapValue
  , readBigMapValueMaybe
  , readBigMapValue
  , getContract
  , getImplicitContractCounter
  , getContractsParameterTypes
  , getContractStorage
  , getScriptSize
  , getBigMapValue
  , getBigMapValues
  , getHeadBlock
  , getCounter
  , getProtocolParameters
  , runOperation
  , preApplyOperations
  , forgeOperation
  , getContractScript
  , getContractBigMap
  , getBalance
  , getDelegate
  , runCode
  , getManagerKey
  ) where

import Data.Map as Map (fromList)
import Data.Singletons (demote)
import Fmt (Buildable(..), pretty, (+|), (|+))
import Network.HTTP.Types.Status (statusCode)
import Servant.Client (ClientError(..), responseStatusCode)

import Lorentz (NicePackedValue, NiceUnpackedValue, niceUnpackedValueEvi, valueToScriptExpr)
import Lorentz.Value
import Morley.Micheline
import Morley.Michelson.TypeCheck.TypeCheck
  (SomeParamType(..), TcOriginatedContracts, mkSomeParamType)
import Morley.Michelson.Typed
import Morley.Michelson.Untyped qualified as U
import Morley.Tezos.Address
import Morley.Tezos.Crypto (encodeBase58Check)
import Morley.Util.ByteString
import Morley.Util.Exception (throwLeft)

import Morley.Client.RPC.Class
import Morley.Client.RPC.Types

data ContractGetCounterAttempt = ContractGetCounterAttempt Address
  deriving stock (Show)
instance Exception ContractGetCounterAttempt
instance Buildable ContractGetCounterAttempt where
  build (ContractGetCounterAttempt addr) =
    "Failed to get counter of contract '" <> build addr <> "', " <>
    "this operation is allowed only for implicit contracts"

-- | Failed to decode received value to the given type.
data ValueDecodeFailure = ValueDecodeFailure Text T
  deriving stock (Show)
instance Exception ValueDecodeFailure
instance Buildable ValueDecodeFailure where
  build (ValueDecodeFailure desc ty) =
    "Failed to decode value with expected type " <> build ty <> " \
    \for '" <> build desc <> "'"

data ValueNotFound = ValueNotFound
  deriving stock (Show)
instance Exception ValueNotFound
instance Buildable ValueNotFound where
  build ValueNotFound =
    "Value with such coordinates is not found in contract big maps"

-- | Read big_map value of given contract by key.
--
-- If the contract contains several @big_map@s with given key type, only one
-- of them will be considered.
readContractBigMapValue
  :: forall k v m.
     (PackedValScope k, HasTezosRpc m, SingI v)
  => Address -> Value k -> m (Value v)
readContractBigMapValue contract key = do
  let
    req = GetBigMap
      { bmKey = toExpression key
      , bmType = toExpression (demote @k)
      }
  res <- getContractBigMap contract req >>= \case
    GetBigMapResult res -> pure res
    GetBigMapNotFound -> throwM ValueNotFound
  fromExpression res
    & either (const $ throwM $ ValueDecodeFailure "big map value" (demote @k)) pure

-- | Read big_map value, given it's ID and a key.
-- If the value is not of the expected type, a 'ValueDecodeFailure' will be thrown.
--
-- Returns 'Nothing' if a big_map with the given ID does not exist,
-- or it does exist but does not contain the given key.
readBigMapValueMaybe
  :: forall v k m.
     (NicePackedValue k, NiceUnpackedValue v, HasTezosRpc m)
  => BigMapId k v -> k -> m (Maybe v)
readBigMapValueMaybe bigMapId key =
  handleStatusCode 404
    (pure Nothing)
    (Just <$> readBigMapValue bigMapId key)

-- | Read big_map value, given it's ID and a key.
-- If the value is not of the expected type, a 'ValueDecodeFailure' will be thrown.
readBigMapValue
  :: forall v k m.
     (NicePackedValue k, NiceUnpackedValue v, HasTezosRpc m)
  => BigMapId k v -> k -> m v
readBigMapValue (BigMapId bigMapId) key =
  getBigMapValue bigMapId scriptExpr >>= \expr ->
    withDict (niceUnpackedValueEvi @v) $
      case fromVal <$> fromExpression expr of
        Right v -> pure v
        Left _ -> throwM $ ValueDecodeFailure "big map value" (demote @(ToT k))
  where
    scriptExpr = encodeBase58Check $ valueToScriptExpr key

-- | Read all big_map values, given it's ID.
-- If the values are not of the expected type, a 'ValueDecodeFailure' will be thrown.
--
-- Returns 'Nothing' if a big_map with the given ID does not exist.
readAllBigMapValuesMaybe
  :: forall v k m.
     (NiceUnpackedValue v, HasTezosRpc m)
  => BigMapId k v -> m (Maybe [v])
readAllBigMapValuesMaybe bigMapId =
  handleStatusCode 404
    (pure Nothing)
    (Just <$> readAllBigMapValues bigMapId)

-- | Read all big_map values, given it's ID.
-- If the values are not of the expected type, a 'ValueDecodeFailure' will be thrown.
readAllBigMapValues
  :: forall v k m.
     (NiceUnpackedValue v, HasTezosRpc m)
  => BigMapId k v -> m [v]
readAllBigMapValues (BigMapId bigMapId) =
  getBigMapValues bigMapId Nothing Nothing >>= \expr ->
    withDict (niceUnpackedValueEvi @v) $
      case fromVal <$> fromExpression expr of
        Right v -> pure v
        Left _ -> throwM $ ValueDecodeFailure "big map value " (demote @(ToT v))

data ContractNotFound = ContractNotFound Address
  deriving stock Show

instance Buildable ContractNotFound where
  build (ContractNotFound addr) =
    "Smart contract " +| addr |+ " was not found"

instance Exception ContractNotFound where
  displayException = pretty

-- | Get originated t'U.Contract' for some address.
getContract :: (HasTezosRpc m) => Address -> m U.Contract
getContract addr =
  handleStatusCode 404 (throwM $ ContractNotFound addr) $
  throwLeft $ fromExpression . osCode <$> getContractScript addr

-- | Get counter value for given address.
--
-- Throws an error if given address is a contract address.
getImplicitContractCounter :: (HasTezosRpc m) => Address -> m TezosInt64
getImplicitContractCounter addr = case addr of
  KeyAddress _      -> getCounter addr
  ContractAddress _ -> throwM $ ContractGetCounterAttempt addr
  TransactionRollupAddress _ -> throwM $ ContractGetCounterAttempt addr

handleStatusCode :: MonadCatch m => Int -> m a -> m a -> m a
handleStatusCode code onError action = action `catch`
  \case FailureResponse _ resp
          | statusCode (responseStatusCode resp) == code -> onError
        e -> throwM e

-- | Extract parameter types for all smart contracts' addresses and return mapping
-- from their hashes to their parameter types
getContractsParameterTypes
  :: HasTezosRpc m => [Address] -> m TcOriginatedContracts
getContractsParameterTypes addrs =
  Map.fromList <$> concatMapM (fmap maybeToList . extractParameterType) addrs
  where
    extractParameterType
      :: HasTezosRpc m => Address
      -> m (Maybe (ContractHash, SomeParamType))
    extractParameterType addr = case addr of
      KeyAddress _ -> return Nothing
      TransactionRollupAddress _ -> return Nothing
      ContractAddress ch ->
        handleStatusCode 404 (return Nothing) $ do
          params <- fmap (U.contractParameter) . throwLeft $
              fromExpression @U.Contract . osCode <$> getContractScript addr
          (paramNotes :: SomeParamType) <- throwLeft $ pure $ mkSomeParamType params
          pure $ Just (ch, paramNotes)

-- | 'getContractStorageAtBlock' applied to the head block.
getContractStorage :: HasTezosRpc m => Address -> m Expression
getContractStorage = getContractStorageAtBlock HeadId

-- | 'getBigMapValueAtBlock' applied to the head block.
getBigMapValue :: HasTezosRpc m => Natural -> Text -> m Expression
getBigMapValue = getBigMapValueAtBlock HeadId

-- | 'getBigMapValuesAtBlock' applied to the head block.
getBigMapValues :: HasTezosRpc m => Natural -> Maybe Natural -> Maybe Natural -> m Expression
getBigMapValues = getBigMapValuesAtBlock HeadId

-- | Get hash of the current head block, this head hash is used in other
-- RPC calls.
getHeadBlock :: HasTezosRpc m => m BlockHash
getHeadBlock = getBlockHash HeadId

-- | 'getCounterAtBlock' applied to the head block.
getCounter :: HasTezosRpc m => Address -> m TezosInt64
getCounter = getCounterAtBlock HeadId

-- | 'getProtocolParametersAtBlock' applied to the head block.
getProtocolParameters :: HasTezosRpc m => m ProtocolParameters
getProtocolParameters = getProtocolParametersAtBlock HeadId

-- | 'runOperationAtBlock' applied to the head block.
runOperation :: HasTezosRpc m => RunOperation -> m RunOperationResult
runOperation = runOperationAtBlock HeadId

-- | 'preApplyOperationsAtBlock' applied to the head block.
preApplyOperations :: HasTezosRpc m => [PreApplyOperation] -> m [RunOperationResult]
preApplyOperations = preApplyOperationsAtBlock HeadId

-- | 'forgeOperationAtBlock' applied to the head block.
forgeOperation :: HasTezosRpc m => ForgeOperation -> m HexJSONByteString
forgeOperation = forgeOperationAtBlock HeadId

-- | 'getContractScriptAtBlock' applied to the head block.
getContractScript :: HasTezosRpc m => Address -> m OriginationScript
getContractScript = getContractScriptAtBlock HeadId

-- | 'getContractBigMapAtBlock' applied to the head block.
getContractBigMap :: HasTezosRpc m => Address -> GetBigMap -> m GetBigMapResult
getContractBigMap = getContractBigMapAtBlock HeadId

-- | 'getBalanceAtBlock' applied to the head block.
getBalance :: HasTezosRpc m => Address -> m Mutez
getBalance = getBalanceAtBlock HeadId

-- | 'getScriptSizeAtBlock' applied to the head block.
getScriptSize :: HasTezosRpc m => CalcSize -> m ScriptSize
getScriptSize = getScriptSizeAtBlock HeadId

-- | 'getDelegateAtBlock' applied to the head block.
getDelegate :: HasTezosRpc m => Address -> m (Maybe KeyHash)
getDelegate = getDelegateAtBlock HeadId

-- | 'runCodeAtBlock' applied to the head block.
runCode :: HasTezosRpc m => RunCode -> m RunCodeResult
runCode = runCodeAtBlock HeadId

getManagerKey :: HasTezosRpc m => Address -> m (Maybe PublicKey)
getManagerKey = getManagerKeyAtBlock HeadId