morley-client-0.2.0: src/Morley/Client/TezosClient/Impl.hs
-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA
-- | Interface to the @tezos-client@ executable expressed in Haskell types.
module Morley.Client.TezosClient.Impl
( TezosClientError (..)
-- * @tezos-client@ api
, signBytes
, rememberContract
, importKey
, genKey
, genFreshKey
, revealKey
, resolveAddressMaybe
, resolveAddress
, getAlias
, getPublicKey
, getSecretKey
, getTezosClientConfig
, calcTransferFee
, calcOriginationFee
, calcRevealFee
, getKeyPassword
, registerDelegate
-- * Internals
, callTezosClient
, callTezosClientStrict
) where
import Unsafe qualified ((!!))
import Colourista (formatWith, red)
import Control.Exception (IOException, throwIO)
import Data.Aeson (eitherDecodeStrict, encode)
import Data.ByteArray (ScrubbedBytes)
import Data.ByteString.Lazy.Char8 qualified as C (unpack)
import Data.Text qualified as T
import Fmt (Buildable(..), pretty, (+|), (|+))
import System.Exit (ExitCode(..))
import System.Process (readProcessWithExitCode)
import Text.Printf (printf)
import UnliftIO.IO (hGetEcho, hSetEcho)
import Lorentz.Value
import Morley.Client.Logging
import Morley.Client.RPC.Types
import Morley.Client.TezosClient.Class qualified as Class (HasTezosClient(resolveAddressMaybe))
import Morley.Client.TezosClient.Parser
import Morley.Client.TezosClient.Types
import Morley.Client.Util (readScrubbedBytes, scrubbedBytesToString)
import Morley.Micheline
import Morley.Michelson.Typed.Scope
import Morley.Tezos.Address
import Morley.Tezos.Address.Alias (AddressOrAlias(..), Alias(..))
import Morley.Tezos.Crypto
import Morley.Util.Peano
import Morley.Util.SizedList.Types
----------------------------------------------------------------------------
-- Errors
----------------------------------------------------------------------------
-- | A data type for all /predicatable/ errors that can happen during
-- @tezos-client@ usage.
data TezosClientError =
UnexpectedClientFailure
-- ^ @tezos-client@ call unexpectedly failed (returned non-zero exit code).
-- The error contains the error code, stdout and stderr contents.
Int -- ^ Exit code
Text -- ^ stdout
Text -- ^ stderr
-- These errors represent specific known scenarios.
| UnknownAddressAlias
-- ^ Could not find an address with given name.
Alias -- ^ Name of address which is eventually used
| UnknownAddress
-- ^ Could not find an address.
Address -- ^ Address that is not present in local tezos cache
| AlreadyRevealed
-- ^ Public key of the given address is already revealed.
Alias -- ^ Address alias that has already revealed its key
| InvalidOperationHash
-- ^ Can't wait for inclusion of operation with given hash because
-- the hash is invalid.
OperationHash
| CounterIsAlreadyUsed
-- ^ Error that indicates when given counter is already used for
-- given contract.
Text -- ^ Raw counter
Text -- ^ Raw address
| EConnreset
-- ^ Network error with which @tezos-client@ fails from time to time.
-- Note: the errors below most likely indicate that something is wrong in our code.
-- Maybe we made a wrong assumption about tezos-client or just didn't consider some case.
-- Another possible reason that a broken tezos-client is used.
| ConfigParseError String
-- ^ A parse error occurred during config parsing.
| TezosClientCryptoParseError Text CryptoParseError
-- ^ @tezos-client@ produced a cryptographic primitive that we can't parse.
| TezosClientParseAddressError Text ParseAddressError
-- ^ @tezos-client@ produced an address that we can't parse.
| TezosClientParseFeeError Text Text
-- ^ @tezos-client@ produced invalid output for parsing baker fee
| TezosClientUnexpectedOutputFormat Text
-- ^ @tezos-client@ printed a string that doesn't match the format we expect.
| CantRevealContract
-- ^ Given alias is a contract and cannot be revealed.
Alias -- ^ Address alias of implicit account
| ContractSender Address Text
-- ^ Given contract is a source of a transfer or origination operation.
| EmptyImplicitContract
-- ^ Given alias is an empty implicit contract.
Alias -- ^ Address alias of implicit contract
| TezosClientUnexpectedSignatureOutput Text
-- ^ @tezos-client sign bytes@ produced unexpected output format
| TezosClientParseEncryptionTypeError Text Text
-- ^ @tezos-client@ produced invalid output for parsing secret key encryption type.
deriving stock (Show, Eq)
instance Exception TezosClientError where
displayException = pretty
instance Buildable TezosClientError where
build = \case
UnexpectedClientFailure errCode output errOutput ->
"tezos-client unexpectedly failed with error code " +| errCode |+
". Stdout:\n" +| output |+ "\nStderr:\n" +| errOutput |+ ""
UnknownAddressAlias name ->
"Could not find an address with name " <> build name <> "."
UnknownAddress uaddr ->
"Could not find an associated name for the given address " <>
build uaddr <> "."
AlreadyRevealed alias ->
"The address alias " <> build alias <> " is already revealed"
InvalidOperationHash hash ->
"Can't wait for inclusion of operation " <> build hash <>
" because this hash is invalid."
CounterIsAlreadyUsed counter addr ->
"Counter " +| counter |+ " already used for " +| addr |+ "."
EConnreset -> "tezos-client call failed with 'Unix.ECONNRESET' error."
ConfigParseError err ->
"A parse error occurred during config parsing: " <> build err
TezosClientCryptoParseError txt err ->
"tezos-client produced a cryptographic primitive that we can't parse: " +|
txt |+ ".\n The error is: " +| err |+ "."
TezosClientParseAddressError txt err ->
"tezos-client produced an address that we can't parse: " +|
txt |+ ".\n The error is: " +| err |+ "."
TezosClientParseFeeError txt err ->
"tezos-client produced invalid output for parsing baker fee: " +|
txt |+ ".\n Parsing error is: " +| err |+ ""
TezosClientUnexpectedOutputFormat txt ->
"tezos-client printed a string that doesn't match the format we expect:\n" <>
build txt
CantRevealContract alias ->
"Contracts (" <> build alias <> ") cannot be revealed"
ContractSender addr opName ->
"Contract (" <> build addr <> ") cannot be source of " +| opName |+ ""
EmptyImplicitContract alias ->
"Empty implicit contract (" <> build alias <> ")"
TezosClientUnexpectedSignatureOutput txt ->
"'tezos-client sign bytes' call returned a signature in format we don't expect:\n" <>
build txt
TezosClientParseEncryptionTypeError txt err ->
"tezos-client produced invalid output for parsing secret key encryption type: " +|
txt |+ ".\n Parsing error is: " +| err |+ ""
----------------------------------------------------------------------------
-- API
----------------------------------------------------------------------------
-- Note: if we try to sign with an unknown alias, tezos-client will
-- report a fatal error (assert failure) to stdout. It's bad. It's
-- reported in two issues: https://gitlab.com/tezos/tezos/-/issues/653
-- and https://gitlab.com/tezos/tezos/-/issues/813.
-- I (@gromak) currently think it's better to wait for it to be resolved upstream.
-- Currently we will throw 'TezosClientUnexpectedOutputFormat' error.
-- | Sign an arbtrary bytestring using @tezos-client@.
-- Secret key of the address corresponding to give 'AddressOrAlias' must be known.
signBytes
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> AddressOrAlias
-> Maybe ScrubbedBytes
-> ByteString
-> m Signature
signBytes signer mbPassword opHash = do
signerAlias <- getAlias signer
logDebug $ "Signing for " <> pretty signer
output <- callTezosClientStrict
["sign", "bytes", toCmdArg opHash, "for", toCmdArg signerAlias] MockupMode mbPassword
liftIO case T.stripPrefix "Signature: " output of
Nothing ->
-- There is additional noise in the stdout in case key is password protected
case T.stripPrefix "Enter password for encrypted key: Signature: " output of
Nothing -> throwM $ TezosClientUnexpectedSignatureOutput output
Just signatureTxt -> txtToSignature signatureTxt
Just signatureTxt -> txtToSignature signatureTxt
where
txtToSignature :: MonadCatch m => Text -> m Signature
txtToSignature signatureTxt = either (throwM . TezosClientCryptoParseError signatureTxt) pure $
parseSignature . T.strip $ signatureTxt
-- | Generate a new secret key and save it with given alias.
-- If an address with given alias already exists, it will be returned
-- and no state will be changed.
genKey
:: ( MonadThrow m, MonadCatch m, WithClientLog env m, HasTezosClientEnv env, MonadIO m
, Class.HasTezosClient m)
=> Alias
-> m Address
genKey name = do
let
isAlreadyExistsError :: Text -> Bool
-- We can do a bit better here using more complex parsing if necessary.
isAlreadyExistsError = T.isInfixOf "already exists."
errHandler _ errOut = pure (isAlreadyExistsError errOut)
_ <-
callTezosClient errHandler
["gen", "keys", toCmdArg name] MockupMode Nothing
resolveAddress (AddressAlias name)
-- | Generate a new secret key and save it with given alias.
-- If an address with given alias already exists, it will be removed
-- and replaced with a fresh one.
genFreshKey
:: ( MonadThrow m, MonadCatch m, WithClientLog env m, HasTezosClientEnv env, MonadIO m
, Class.HasTezosClient m)
=> Alias
-> m Address
genFreshKey name = do
let
isNoAliasError :: Text -> Bool
-- We can do a bit better here using more complex parsing if necessary.
isNoAliasError = T.isInfixOf "no public key hash alias named"
errHandler _ errOutput = pure (isNoAliasError errOutput)
_ <-
callTezosClient errHandler
["forget", "address", toCmdArg name, "--force"] MockupMode Nothing
callTezosClientStrict ["gen", "keys", toCmdArg name] MockupMode Nothing
resolveAddress (AddressAlias name)
-- | Reveal public key corresponding to the given alias.
-- Fails if it's already revealed.
revealKey
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> Alias
-> Maybe ScrubbedBytes
-> m ()
revealKey alias mbPassword = do
logDebug $ "Revealing key for " +| alias |+ ""
let
alreadyRevealed = T.isInfixOf "previously revealed"
revealedImplicitAccount = T.isInfixOf "only implicit accounts can be revealed"
emptyImplicitContract = T.isInfixOf "Empty implicit contract"
errHandler _ errOut =
False <$ do
when (alreadyRevealed errOut) (throwM (AlreadyRevealed alias))
when (revealedImplicitAccount errOut) (throwM (CantRevealContract alias))
when (emptyImplicitContract errOut) (throwM (EmptyImplicitContract alias))
_ <-
callTezosClient errHandler
["reveal", "key", "for", toCmdArg alias] ClientMode mbPassword
logDebug $ "Successfully revealed key for " +| alias |+ ""
-- | Register alias as delegate
registerDelegate
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> Alias
-> Maybe ScrubbedBytes
-> m ()
registerDelegate alias mbPassword = do
logDebug $ "Registering " +| alias |+ " as delegate"
let
emptyImplicitContract = T.isInfixOf "Empty implicit contract"
errHandler _ errOut =
False <$ do
when (emptyImplicitContract errOut) (throwM (EmptyImplicitContract alias))
_ <-
callTezosClient errHandler
["register", "key", toCmdArg alias, "as", "delegate"] ClientMode mbPassword
logDebug $ "Successfully registered " +| alias |+ " as delegate"
-- | Return 'Address' corresponding to given 'AddressOrAlias', covered in @Maybe@.
-- Return @Nothing@ if address alias is unknown
resolveAddressMaybe
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> AddressOrAlias
-> m (Maybe Address)
resolveAddressMaybe addressOrAlias = case addressOrAlias of
AddressResolved addr -> (pure . Just) addr
AddressAlias originatorName -> do
logDebug $ "Resolving " +| originatorName |+ ""
output <- callTezosClientStrict ["list", "known", "contracts"] MockupMode Nothing
let parse = T.stripPrefix (unAlias originatorName <> ": ")
liftIO case safeHead . mapMaybe parse . lines $ output of
Nothing -> pure Nothing
Just addrText ->
either (throwM . TezosClientParseAddressError addrText) (pure . Just) $
parseAddress addrText
-- | Return 'Alias' corresponding to given 'AddressOrAlias'.
getAlias
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> AddressOrAlias
-> m Alias
getAlias = \case
AddressAlias alias -> pure alias
AddressResolved senderAddress -> do
logDebug $ "Getting an alias for " <> pretty senderAddress
output <- callTezosClientStrict ["list", "known", "contracts"] MockupMode Nothing
let parse = T.stripSuffix (": " <> pretty senderAddress)
liftIO case safeHead . mapMaybe parse . lines $ output of
Nothing -> throwM $ UnknownAddress senderAddress
Just alias -> pure (Alias alias)
-- | Return 'PublicKey' corresponding to given 'AddressOrAlias'.
getPublicKey
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> AddressOrAlias
-> m PublicKey
getPublicKey addrOrAlias = do
alias <- getAlias addrOrAlias
logDebug $ "Getting " +| alias |+ " public key"
output <- callTezosClientStrict ["show", "address", toCmdArg alias] MockupMode Nothing
liftIO case lines output of
_ : [rawPK] -> do
pkText <- maybe
(throwM $ TezosClientUnexpectedOutputFormat rawPK) pure
(T.stripPrefix "Public Key: " rawPK)
either (throwM . TezosClientCryptoParseError pkText) pure $
parsePublicKey pkText
_ -> throwM $ TezosClientUnexpectedOutputFormat output
-- | Return 'SecretKey' corresponding to given 'AddressOrAlias'.
getSecretKey
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> AddressOrAlias
-> m SecretKey
getSecretKey addrOrAlias = do
alias <- getAlias addrOrAlias
logDebug $ "Getting " +| alias |+ " secret key"
output <- callTezosClientStrict ["show", "address", toCmdArg alias, "--show-secret"] MockupMode Nothing
liftIO case lines output of
_ : _ : [rawSK] -> do
skText <- maybe
(throwM $ TezosClientUnexpectedOutputFormat rawSK) pure
(T.stripPrefix "Secret Key: " rawSK)
either (throwM . TezosClientCryptoParseError skText) pure $
parseSecretKey skText
_ -> throwM $ TezosClientUnexpectedOutputFormat output
-- | Save a contract with given address and alias.
-- If @replaceExisting@ is @False@ and a contract with given alias
-- already exists, this function does nothing.
rememberContract
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> Bool
-> Address
-> Alias
-> m ()
rememberContract replaceExisting contractAddress name = do
let
isAlreadyExistsError = T.isInfixOf "already exists"
errHandler _ errOut = pure (isAlreadyExistsError errOut)
args = ["remember", "contract", toCmdArg name, pretty contractAddress]
_ <-
callTezosClient errHandler
(if replaceExisting then args <> ["--force"] else args)
MockupMode Nothing
pure ()
importKey
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> Bool
-> Alias
-> SecretKey
-> m Alias
importKey replaceExisting name key = do
let
isAlreadyExistsError = T.isInfixOf "already exists"
errHandler _ errOut = pure (isAlreadyExistsError errOut)
args = ["import", "secret", "key", toCmdArg name, toCmdArg key]
_ <-
callTezosClient errHandler
(if replaceExisting then args <> ["--force"] else args)
MockupMode Nothing
pure name
-- | Read @tezos-client@ configuration.
getTezosClientConfig :: FilePath -> Maybe FilePath -> IO TezosClientConfig
getTezosClientConfig client mbDataDir = do
t <- readProcessWithExitCode' client
(maybe [] (\dir -> ["-d", dir]) mbDataDir ++ ["config", "show"]) ""
case t of
(ExitSuccess, toText -> output, _) -> case eitherDecodeStrict . encodeUtf8 . toText $ output of
Right config -> pure config
Left err -> throwM $ ConfigParseError err
(ExitFailure errCode, toText -> output, toText -> errOutput) ->
throwM $ UnexpectedClientFailure errCode output errOutput
-- | Calc baker fee for transfer using @tezos-client@.
calcTransferFee
:: ( WithClientLog env m, HasTezosClientEnv env
, MonadIO m, MonadCatch m
)
=> AddressOrAlias -> Maybe ScrubbedBytes -> TezosInt64 -> [CalcTransferFeeData] -> m [TezosMutez]
calcTransferFee from mbPassword burnCap transferFeeDatas = do
output <- callTezosClientStrict
[ "multiple", "transfers", "from", pretty from, "using"
, C.unpack $ encode transferFeeDatas, "--burn-cap", showBurnCap burnCap, "--dry-run"
] ClientMode mbPassword
withSomePeano (fromIntegralOverflowing $ length transferFeeDatas) $
\(_ :: Proxy n) -> toList <$> feeOutputParser @n output
-- | Calc baker fee for origination using @tezos-client@.
calcOriginationFee
:: ( UntypedValScope st, WithClientLog env m, HasTezosClientEnv env
, MonadIO m, MonadCatch m
)
=> CalcOriginationFeeData cp st -> m TezosMutez
calcOriginationFee CalcOriginationFeeData{..} = do
output <- callTezosClientStrict
[ "originate", "contract", "-", "transferring"
, showTez cofdBalance
, "from", pretty cofdFrom, "running"
, toCmdArg cofdContract, "--init"
, toCmdArg cofdStorage, "--burn-cap"
, showBurnCap cofdBurnCap, "--dry-run"
] ClientMode cofdMbFromPassword
fees <- feeOutputParser @1 output
case fees of
singleFee :< Nil -> return singleFee
-- | Calc baker fee for revealing using @tezos-client@.
--
-- Note that @tezos-client@ does not support passing an address here,
-- at least at the moment of writing.
calcRevealFee
:: ( WithClientLog env m, HasTezosClientEnv env
, MonadIO m, MonadCatch m
)
=> Alias -> Maybe ScrubbedBytes -> TezosInt64 -> m TezosMutez
calcRevealFee alias mbPassword burnCap = do
output <- callTezosClientStrict
[ "reveal", "key", "for", toCmdArg alias
, "--burn-cap", showBurnCap burnCap
, "--dry-run"
] ClientMode mbPassword
fees <- feeOutputParser @1 output
case fees of
singleFee :< Nil -> return singleFee
feeOutputParser :: forall n m. (SingIPeano n, MonadIO m, MonadThrow m) => Text -> m (SizedList n TezosMutez)
feeOutputParser output =
case parseBakerFeeFromOutput @n output of
Right fee -> return fee
Left err -> throwM $ TezosClientParseFeeError output $ pretty err
showBurnCap :: TezosInt64 -> String
showBurnCap x = printf "%.6f" $ (fromIntegralToRealFrac @TezosInt64 @Float x) / 1000
showTez :: TezosMutez -> String
showTez = toCmdArg . unTezosMutez
-- | Get password for secret key associated with given address
-- in case this key is password-protected
getKeyPassword
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadMask m)
=> Address -> m (Maybe ScrubbedBytes)
getKeyPassword = \case
ContractAddress _ -> pure Nothing
keyAddr -> (getAlias $ AddressResolved keyAddr) >>= getKeyPassword'
where
getKeyPassword'
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m, MonadMask m)
=> Alias -> m (Maybe ScrubbedBytes)
getKeyPassword' alias = do
output <- callTezosClientStrict [ "show", "address", pretty alias, "-S"] MockupMode Nothing
encryptionType <-
case parseSecretKeyEncryption output of
Right t -> return t
Left err -> throwM $ TezosClientParseEncryptionTypeError output $ pretty err
case encryptionType of
EncryptedKey -> do
putTextLn $ "Please enter password for '" <> pretty alias <> "':"
Just <$> withoutEcho readScrubbedBytes
_ -> pure Nothing
-- | Hide entered password
withoutEcho :: (MonadIO m, MonadMask m) => m a -> m a
withoutEcho action = do
old <- hGetEcho stdin
bracket_ (hSetEcho stdin False) (hSetEcho stdin old) action
----------------------------------------------------------------------------
-- Helpers
-- All interesting @tezos-client@ functionality is supposed to be
-- exported as functions with types closely resembling inputs of
-- respective @tezos-client@ functions. If something is not
-- available, consider adding it here. But if it is not feasible,
-- you can use these two functions directly to constructor a custom
-- @tezos-client@ call.
----------------------------------------------------------------------------
-- | Datatype that represents modes for calling node from @tezos-client@.
data CallMode
= MockupMode
-- ^ Mode in which @tezos-client@ doesn't perform any actual RPC calls to the node
-- and use mock instead.
| ClientMode
-- ^ Normal mode in which @tezos-client@ performs all necessary RPC calls to the node.
-- | Call @tezos-client@ with given arguments. Arguments defined by
-- config are added automatically. The second argument specifies what
-- should be done in failure case. It takes stdout and stderr
-- output. Possible handling:
--
-- 1. Parse a specific error and throw it.
-- 2. Parse an expected error that shouldn't cause a failure.
-- Return @True@ in this case.
-- 3. Detect an unexpected error, return @False@.
-- In this case 'UnexpectedClientFailure' will be throw.
callTezosClient
:: forall env m. (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> (Text -> Text -> IO Bool) -> [String] -> CallMode -> Maybe ScrubbedBytes -> m Text
callTezosClient errHandler args mode mbInput = retryEConnreset mode $ do
TezosClientEnv {..} <- view tezosClientEnvL
let
extraArgs :: [String]
extraArgs = mconcat
[ ["-E", toCmdArg tceEndpointUrl]
, maybe [] (\dir -> ["-d", dir]) tceMbTezosClientDataDir
, ["--mode", case mode of
MockupMode -> "mockup"
ClientMode -> "client"
]
]
allArgs = extraArgs ++ args
logDebug $ "Running: " <> unwords (toText <$> tceTezosClientPath:allArgs)
let
ifNotEmpty prefix output
| null output = ""
| otherwise = prefix <> ":\n" <> output
logOutput :: Text -> Text -> m ()
logOutput output errOutput = logDebug $
ifNotEmpty "stdout" output <>
ifNotEmpty "stderr" errOutput
liftIO (readProcessWithExitCode' tceTezosClientPath allArgs
(maybe "" scrubbedBytesToString mbInput)) >>= \case
(ExitSuccess, toText -> output, toText -> errOutput) ->
output <$ logOutput output errOutput
(ExitFailure errCode, toText -> output, toText -> errOutput) -> do
checkCounterError errOutput
checkEConnreset errOutput
liftIO $ unlessM (errHandler output errOutput) $
throwM $ UnexpectedClientFailure errCode output errOutput
output <$ logOutput output errOutput
where
checkCounterError
:: Text -> m ()
checkCounterError errOutput |
"Counter" `T.isPrefixOf` errOutput && "already used for contract" `T.isInfixOf` errOutput = do
let splittedErrOutput = words errOutput
liftIO $ throwM $
CounterIsAlreadyUsed (splittedErrOutput Unsafe.!! 1) (splittedErrOutput Unsafe.!! 5)
checkCounterError _ = pass
checkEConnreset :: Text -> m ()
checkEConnreset errOutput
| "Unix.ECONNRESET" `T.isInfixOf` errOutput = throwM EConnreset
checkEConnreset _ = pass
-- | Helper function that retries @tezos-client@ call action in case of @ECONNRESET@.
-- Note that this error cannot appear in case of 'MockupMode' call.
retryEConnreset :: CallMode -> m a -> m a
retryEConnreset MockupMode action = action
retryEConnreset ClientMode action = retryEConnresetImpl 0 action
retryEConnresetImpl :: Integer -> m a -> m a
retryEConnresetImpl attempt action = action `catch` \err -> do
case err of
EConnreset ->
if attempt >= maxRetryAmount then throwM err
else retryEConnresetImpl (attempt + 1) action
anotherErr -> throwM anotherErr
maxRetryAmount = 5
-- | Call tezos-client and expect success.
callTezosClientStrict
:: (WithClientLog env m, HasTezosClientEnv env, MonadIO m, MonadCatch m)
=> [String] -> CallMode -> Maybe ScrubbedBytes -> m Text
callTezosClientStrict args mode mbInput = callTezosClient errHandler args mode mbInput
where
errHandler _ _ = pure False
-- | Variant of @readProcessWithExitCode@ that prints a better error in case of
-- an exception in the inner @readProcessWithExitCode@ call.
readProcessWithExitCode'
:: FilePath
-> [String]
-> String
-> IO (ExitCode, String, String)
readProcessWithExitCode' fp args inp =
catch
(readProcessWithExitCode fp args inp) handler
where
handler :: IOException -> IO (ExitCode, String, String)
handler e = do
hPutStrLn @Text stderr $ formatWith [red] errorMsg
throwIO e
errorMsg =
"ERROR!! There was an error in executing `" <> toText fp <> "` program. Is the \
\ executable available in PATH ?"
-- | Return 'Address' corresponding to given 'AddressOrAlias'.
resolveAddress
:: (MonadThrow m, Class.HasTezosClient m)
=> AddressOrAlias
-> m Address
resolveAddress addr = case addr of
AddressResolved addrResolved -> pure addrResolved
alias@(AddressAlias originatorName) ->
Class.resolveAddressMaybe alias >>= (\case
Nothing -> throwM $ UnknownAddressAlias originatorName
Just existingAddress -> return existingAddress
)