packages feed

morley-client-0.2.0: src/Morley/Client/Util.hs

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

module Morley.Client.Util
  ( epNameToTezosEp
  , extractAddressesFromValue
  , disableAlphanetWarning
  -- * @runContract@
  , runContract
  , RunContractParameters(..)
  , runContractParameters
  , withBalance
  , withAmount
  , withSender
  , withSource
  , withLevel
  , withNow

  -- * @tezos-client@ password-related helpers
  , scrubbedBytesToString
  , readScrubbedBytes
  ) where

import Control.Lens (makeLensesFor)
import Data.ByteArray (ScrubbedBytes, convert)
import Data.ByteString qualified as BS (getLine)
import Data.Constraint ((\\))
import Generics.SYB (everything, mkQ)
import System.Environment (setEnv)

import Morley.AsRPC (AsRPC, MaybeRPC, rpcStorageScopeEvi)
import Morley.Client.RPC.Class
import Morley.Client.RPC.Getters
import Morley.Client.RPC.Types
import Morley.Micheline
import Morley.Michelson.Text
import Morley.Michelson.Typed qualified as T
import Morley.Michelson.Typed.Entrypoints (EpAddress(..), parseEpAddress)
import Morley.Michelson.Untyped (InternalByteString(..), Value, Value'(..))
import Morley.Michelson.Untyped.Entrypoints (EpName(..), pattern DefEpName)
import Morley.Tezos.Address
import Morley.Tezos.Core (Mutez, Timestamp(..), zeroMutez)
import Morley.Util.Exception as E (throwLeft)

-- | Sets the environment variable for disabling tezos-client
-- "not a mainnet" warning
disableAlphanetWarning :: IO ()
disableAlphanetWarning = setEnv "TEZOS_CLIENT_UNSAFE_DISABLE_DISCLAIMER" "YES"

-- | Convert 'EpName' to the textual representation used by RPC and tezos-client.
epNameToTezosEp :: EpName -> Text
epNameToTezosEp = \case
  DefEpName -> "default"
  epName -> unEpName epName

-- | Extract all addresses value from given untyped 'Value'.
--
-- Note that it returns all values that can be used as an address.
-- However, some of fetched values can never be used as an address.
extractAddressesFromValue :: Value -> [Address]
extractAddressesFromValue val =
  everything (<>) (mkQ [] fetchAddress) val
  where
    fetchAddress :: Value -> [Address]
    fetchAddress = \case
      ValueString s -> case parseEpAddress (unMText s) of
        Right addr -> [eaAddress addr]
        Left _ -> []
      ValueBytes (InternalByteString b) -> case parseAddressRaw b of
        Right addr -> [addr]
        Left _ -> []
      _ -> []

-- | A structure with all the parameters for 'runContract'
data RunContractParameters cp st = RunContractParameters
  { rcpContract :: T.Contract cp st
  , rcpParameter :: MaybeRPC (T.Value cp)
  , rcpStorage :: MaybeRPC (T.Value st)
  , rcpBalance :: Mutez
  , rcpNow :: Maybe Timestamp
  , rcpLevel :: Maybe Natural
  , rcpAmount :: Mutez
  , rcpSender :: Maybe Address
  , rcpSource :: Maybe Address
  }

-- | Initializes the parameters for `runContract` with sensible defaults.
--
-- Use the @with*@ lenses to set any optional parameters.
runContractParameters
  :: T.Contract cp st -> MaybeRPC (T.Value cp) -> MaybeRPC (T.Value st)
  -> RunContractParameters cp st
runContractParameters contract cp st =
  RunContractParameters
    { rcpContract = contract
    , rcpParameter = cp
    , rcpStorage = st
    , rcpBalance = zeroMutez
    , rcpAmount = zeroMutez
    , rcpNow = Nothing
    , rcpLevel = Nothing
    , rcpSender = Nothing
    , rcpSource = Nothing
    }

makeLensesFor
  [ ("rcpBalance", "withBalance")
  , ("rcpAmount", "withAmount")
  , ("rcpSender", "withSender")
  , ("rcpSource", "withSource")
  , ("rcpLevel", "withLevel")
  , ("rcpNow", "withNow")
  ]
  ''RunContractParameters

-- | Run contract with given parameter and storage and get new storage without
-- injecting anything to the chain.
runContract
  :: forall cp st m. (HasTezosRpc m, T.ParameterScope cp, T.StorageScope st)
  => RunContractParameters cp st -> m (AsRPC (T.Value st))
runContract RunContractParameters{..} = do
  headConstants <- getBlockConstants HeadId
  let args = RunCode
        { rcScript = toExpression rcpContract
        , rcStorage = toExpression rcpStorage
        , rcInput = toExpression rcpParameter
        , rcAmount = TezosMutez rcpAmount
        , rcBalance = TezosMutez rcpBalance
        , rcChainId = bcChainId headConstants
        , rcNow = rcpNow <&> StringEncode . round . unTimestamp
        , rcLevel = StringEncode <$> rcpLevel
        -- Note: assigning source=sender and payer=source may seem like a bug, but it's not.
        -- For some reason, the /run_code uses a different naming scheme.
        -- What this endpoint calls 'source' is actually the address that will be returned by the `SENDER` instruction.
        -- See details here: https://gitlab.com/tezos/tezos/-/issues/710
        , rcSource = rcpSender
        , rcPayer = rcpSource
        }
  res <- runCode args
  throwLeft @_ @FromExpressionError $ pure $
    fromExpression @(AsRPC (T.Value st)) (rcrStorage res)
      \\ rpcStorageScopeEvi @st

-- | Function for relatively safe getting password from stdin.
-- After reading bytes are converted to @ScrubbedBytes@, thus it's harder
-- to accidentally leak them.
readScrubbedBytes :: MonadIO m => m ScrubbedBytes
readScrubbedBytes = convert <$> liftIO BS.getLine

-- | Convert @ScrubbedBytes@ to @String@, so that it can be passed to @tezos-client@
-- as a stdin
scrubbedBytesToString :: ScrubbedBytes -> String
scrubbedBytesToString = decodeUtf8 . convert @ScrubbedBytes @ByteString