-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA -- | An alternative implementation of @morley-client@ that does not require -- @tezos-client@ and has some limitations because of that (not all methods -- are implemented). module Morley.Client.OnlyRPC ( MorleyOnlyRpcEnv (..) , mkMorleyOnlyRpcEnv , MorleyOnlyRpcM (..) , runMorleyOnlyRpcM ) where import Colog (HasLog(..), Message) import Control.Lens (at) import Data.Map.Strict qualified as Map import Fmt ((+|), (|+)) import Servant.Client (BaseUrl, ClientEnv) import Servant.Client.Core (RunClient(..)) import UnliftIO (MonadUnliftIO) import Morley.Client.App import Morley.Client.Init import Morley.Client.Logging (ClientLogAction) import Morley.Client.RPC.Class (HasTezosRpc(..)) import Morley.Client.RPC.HttpClient (newClientEnv) import Morley.Client.TezosClient.Class (HasTezosClient(..)) import Morley.Client.TezosClient.Types (AddressOrAlias(..), mkAlias) import Morley.Tezos.Address (Address, mkKeyAddress) import Morley.Tezos.Crypto (SecretKey, sign, toPublic) ---------------- -- Environment ---------------- -- | Environment used by 'MorleyOnlyRpcM'. data MorleyOnlyRpcEnv = MorleyOnlyRpcEnv { moreLogAction :: ClientLogAction MorleyOnlyRpcM -- ^ Action used to log messages. , moreClientEnv :: ClientEnv -- ^ Environment necessary to make HTTP calls. , moreSecretKeys :: Map Address SecretKey -- ^ In-memory secret keys that can be used for signing. } -- | Construct 'MorleyOnlyRpcEnv'. -- -- * Full 'MorleyClientConfig' is not passed because we need just 2 things from it. -- * Log action is built the same way as for t'Morley.Client.MorleyClientEnv'. -- * All secret keys are passed as an argument. mkMorleyOnlyRpcEnv :: [SecretKey] -> BaseUrl -> Word -> IO MorleyOnlyRpcEnv mkMorleyOnlyRpcEnv secretKeys endpoint verbosity = do clientEnv <- newClientEnv endpoint pure MorleyOnlyRpcEnv { moreLogAction = mkLogAction verbosity , moreClientEnv = clientEnv , moreSecretKeys = Map.fromList $ map (\sk -> (mkKeyAddress (toPublic sk), sk)) secretKeys } ---------------- -- Monad ---------------- -- | Monad that implements 'HasTezosClient' and 'HasTezosRpc' classes and -- can be used for high-level actions as an alternative to t'Morley.Client.MorleyClientM'. newtype MorleyOnlyRpcM a = MorleyOnlyRpcM { unMorleyOnlyRpcM :: ReaderT MorleyOnlyRpcEnv IO a } deriving newtype ( Functor, Applicative, Monad, MonadReader MorleyOnlyRpcEnv , MonadIO, MonadThrow, MonadCatch, MonadMask, MonadUnliftIO ) -- | Run 'MorleyOnlyRpcM' action within given 'MorleyOnlyRpcEnv'. Retry action -- in case of invalid counter error. runMorleyOnlyRpcM :: MorleyOnlyRpcEnv -> MorleyOnlyRpcM a -> IO a runMorleyOnlyRpcM env action = runReaderT (unMorleyOnlyRpcM (retryInvalidCounter action)) env where retryInvalidCounter a = a `catch` handleInvalidCounterRpc retryAction where retryAction = waitBeforeRetry >> retryInvalidCounter action ---------------- -- Exceptions ---------------- -- | This exception is thrown in methods that are completely unsupported. data UnsupportedByOnlyRPC = UnsupportedByOnlyRPC Text deriving stock (Show, Eq) instance Exception UnsupportedByOnlyRPC where displayException (UnsupportedByOnlyRPC method) = toString $ "Method '" <> method <> "' is not supported in only-RPC mode" -- | This exception is thrown when something goes wrong in supported methods. data MorleyOnlyRpcException = UnknownSecretKeyFor Address deriving stock (Show, Eq) instance Exception MorleyOnlyRpcException where displayException = \case UnknownSecretKeyFor addr -> "Secret key is unknown for " +| addr |+ "" ---------------- -- Instances (implementation) ---------------- instance HasLog MorleyOnlyRpcEnv Message MorleyOnlyRpcM where getLogAction = moreLogAction setLogAction action mce = mce { moreLogAction = action } -- [#652] We may implement more methods here if the need arises. instance HasTezosClient MorleyOnlyRpcM where signBytes sender _password opHash = case sender of AddressAlias {} -> throwM $ UnsupportedByOnlyRPC "signBytes (AddressAlias _)" AddressResolved address -> do env <- ask case moreSecretKeys env ^. at address of Nothing -> throwM $ UnknownSecretKeyFor address Just secretKey -> liftIO $ sign secretKey opHash -- In RPC-only mode we only use unencrypted in-memory passwords. getKeyPassword _ = pure Nothing -- This method can be implemented if necessary by manually checking whether -- the operation is confirmed. For now we simply don't wait for confirmation -- in RPC-only mode. waitForOperation = \_ -> pass -- Stateful actions that simply do nothing because there is no persistent state. rememberContract = \_ _ _ -> pass -- We return a dummy value here, because this function is used in a lot of -- places and with an exception here it's not possible to send transactions. -- So be aware of this and do not rely on this value! -- TODO #652: consider using a `Map` instead getAlias _ = pure (mkAlias "MorleyOnlyRpc") -- Actions that are not supported and simply throw exceptions. genKey _ = throwM $ UnsupportedByOnlyRPC "genKey" genFreshKey _ = throwM $ UnsupportedByOnlyRPC "genFreshKey" importKey _ _ _ = throwM $ UnsupportedByOnlyRPC "importKey" revealKey _ _ = throwM $ UnsupportedByOnlyRPC "revealKey" resolveAddressMaybe _ = throwM $ UnsupportedByOnlyRPC "resolveAddressMaybe" getPublicKey _ = throwM $ UnsupportedByOnlyRPC "getPublicKey" registerDelegate _ _ = throwM $ UnsupportedByOnlyRPC "registerDelegate" getTezosClientConfig = throwM $ UnsupportedByOnlyRPC "getTezosClientConfig" calcTransferFee _ _ _ _ = throwM $ UnsupportedByOnlyRPC "calcTransferFee" calcOriginationFee _ = throwM $ UnsupportedByOnlyRPC "calcOriginationFee" instance RunClient MorleyOnlyRpcM where runRequestAcceptStatus statuses req = do env <- moreClientEnv <$> ask runRequestAcceptStatusImpl env statuses req throwClientError = throwClientErrorImpl instance HasTezosRpc MorleyOnlyRpcM where getBlockHash = getBlockHashImpl getCounterAtBlock = getCounterImpl getBlockHeader = getBlockHeaderImpl getBlockConstants = getBlockConstantsImpl getBlockOperations = getBlockOperationsImpl getProtocolParametersAtBlock = getProtocolParametersImpl runOperationAtBlock = runOperationImpl preApplyOperationsAtBlock = preApplyOperationsImpl forgeOperationAtBlock = forgeOperationImpl injectOperation = injectOperationImpl getContractScriptAtBlock = getContractScriptImpl getContractStorageAtBlock = getContractStorageAtBlockImpl getContractBigMapAtBlock = getContractBigMapImpl getBigMapValueAtBlock = getBigMapValueAtBlockImpl getBigMapValuesAtBlock = getBigMapValuesAtBlockImpl getBalanceAtBlock = getBalanceImpl getDelegateAtBlock = getDelegateImpl runCodeAtBlock = runCodeImpl getChainId = getChainIdImpl getManagerKeyAtBlock = getManagerKeyImpl