packages feed

morley-1.16.1: app/Main.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

module Main
  ( main
  ) where

import qualified Data.Text.Lazy.IO.Utf8 as Utf8 (writeFile)
import Data.Version (showVersion)
import Fmt (pretty)
import Options.Applicative
  (execParser, footerDoc, fullDesc, header, help, helper, info, infoOption, long, progDesc, short,
  subparser, switch)
import qualified Options.Applicative as Opt
import Options.Applicative.Help.Pretty (Doc, linebreak)
import Paths_morley (version)
import Text.Hex (encodeHex)

import Morley.CLI
import Morley.Michelson.Analyzer (analyze)
import Morley.Michelson.Optimizer (optimize)
import Morley.Michelson.Printer (printSomeContract, printUntypedContract)
import Morley.Michelson.Runtime
  (TxData(..), originateContract, prepareContract, runContract, transfer)
import Morley.Michelson.Runtime.GState (genesisAddress)
import Morley.Michelson.TypeCheck (tcVerbose, typeCheckContract, typeCheckingWith)
import qualified Morley.Michelson.TypeCheck as TypeCheck
import Morley.Michelson.TypeCheck.Types (mapSomeContract)
import Morley.Michelson.Typed (Contract'(..), SomeContract(..))
import qualified Morley.Michelson.Untyped as U
import Morley.Tezos.Address (Address)
import Morley.Tezos.Core (Mutez, Timestamp(..), toMutez)
import Morley.Tezos.Crypto
import Morley.Tezos.Crypto.Timelock (chestBytes, chestKeyBytes, createChestAndChestKey)
import Morley.Util.CLI (mkCommandParser, outputOption)
import Morley.Util.Main (wrapMain)
import Morley.Util.Named
import REPL

----------------------------------------------------------------------------
-- Command line options
----------------------------------------------------------------------------

data CmdLnArgs
  = Print ("input" :? FilePath) ("output" :? FilePath) ("singleLine" :! Bool)
  | Optimize OptimizeOptions
  | Analyze AnalyzeOptions
  | TypeCheck TypeCheckOptions
  | Run RunOptions
  | Originate OriginateOptions
  | Transfer TransferOptions
  | CreateChest CreateChestOptions
  | REPL

data OptimizeOptions = OptimizeOptions
  { optoContractFile :: Maybe FilePath
  , optoOutput :: Maybe FilePath
  , optoSingleLine :: Bool
  }

data AnalyzeOptions = AnalyzeOptions
  { aoContractFile :: Maybe FilePath
  }

data CreateChestOptions = CreateChestOptions
  { ccPayload :: ByteString
  , ccTime :: TLTime
  }

data TypeCheckOptions = TypeCheckOptions
  { tcoContractFile :: Maybe FilePath
  , tcoTcOptions    :: TypeCheck.TypeCheckOptions
  }

data RunOptions = RunOptions
  { roContractFile :: Maybe FilePath
  , roDBPath :: FilePath
  , roTcOptions :: TypeCheck.TypeCheckOptions
  , roStorageValue :: U.Value
  , roTxData :: TxData
  , roVerbose :: Bool
  , roNow :: Maybe Timestamp
  , roLevel :: Maybe Natural
  , roMaxSteps :: Word64
  , roInitBalance :: Mutez
  , roWrite :: Bool
  }

data OriginateOptions = OriginateOptions
  { ooContractFile :: Maybe FilePath
  , ooDBPath :: FilePath
  , ooTcOptions :: TypeCheck.TypeCheckOptions
  , ooOriginator :: Address
  , ooDelegate :: Maybe KeyHash
  , ooStorageValue :: U.Value
  , ooBalance :: Mutez
  , ooVerbose :: Bool
  }

data TransferOptions = TransferOptions
  { toDBPath :: FilePath
  , toTcOptions :: TypeCheck.TypeCheckOptions
  , toDestination :: Address
  , toTxData :: TxData
  , toNow :: Maybe Timestamp
  , toLevel :: Maybe Natural
  , toMaxSteps :: Word64
  , toVerbose :: Bool
  , toDryRun :: Bool
  }

argParser :: Opt.Parser CmdLnArgs
argParser = subparser $
  printSubCmd <>
  typecheckSubCmd <>
  runSubCmd <>
  originateSubCmd <>
  transferSubCmd <>
  optimizeSubCmd <>
  analyzeSubCmd <>
  createChestSubCmd <>
  replSubCmd
  where
    typecheckSubCmd =
      mkCommandParser "typecheck"
        (TypeCheck <$> typeCheckOptions) $
        ("Typecheck passed contract")

    printSubCmd =
      mkCommandParser "print"
      (Print <$> (#input <:?> optional contractFileOption)
             <*> (#output <:?> outputOption)
             <*> (#singleLine <:!> onelineOption))
      ("Parse a Morley contract and print corresponding Michelson " <>
       "contract that can be parsed by the OCaml reference client: tezos-client")

    runSubCmd =
      mkCommandParser "run"
      (Run <$> runOptions) $
      "Run passed contract. \
      \It's originated first and then a transaction is sent to it"

    replSubCmd =
      mkCommandParser "repl"
      (pure REPL)
      "Start a Morley REPL"

    originateSubCmd =
      mkCommandParser "originate"
      (Originate <$> originateOptions)
      "Originate passed contract. Add it to passed DB"

    transferSubCmd =
      mkCommandParser "transfer"
      (Transfer <$> transferOptions)
      "Transfer tokens to given address"

    optimizeSubCmd =
      mkCommandParser "optimize"
      (Optimize <$> optimizeOptions)
      "Optimize the contract."

    analyzeSubCmd =
      mkCommandParser "analyze"
      (Analyze <$> analyzeOptions)
      "Analyze the contract."

    createChestSubCmd =
      mkCommandParser "create_chest"
      (CreateChest <$> createChestOptions)
      "Create a timelocked chest and key."

    verboseFlag :: Opt.Parser Bool
    verboseFlag = switch $
      short 'v' <>
      long "verbose" <>
      help "Whether output should be verbose"

    writeFlag :: Opt.Parser Bool
    writeFlag = switch $
      long "write" <>
      help "Whether updated DB should be written to DB file"

    dryRunFlag :: Opt.Parser Bool
    dryRunFlag = switch $
      long "dry-run" <>
      help "Do not write updated DB to DB file"

    typeCheckOptionsOption :: Opt.Parser TypeCheck.TypeCheckOptions
    typeCheckOptionsOption = do
      tcVerbose <- verboseFlag
      tcStrict <- fmap not . switch $
        long "typecheck-lax" <>
        help "Whether actions permitted in `tezos-client run` but forbidden in \
             \e.g. `tezos-client originate` should be allowed here"
      return TypeCheck.TypeCheckOptions{..}

    typeCheckOptions :: Opt.Parser TypeCheckOptions
    typeCheckOptions = TypeCheckOptions
      <$> optional contractFileOption
      <*> typeCheckOptionsOption

    defaultBalance :: Mutez
    defaultBalance = toMutez 4000000

    optimizeOptions :: Opt.Parser OptimizeOptions
    optimizeOptions = OptimizeOptions
      <$> optional contractFileOption
      <*> outputOption
      <*> onelineOption

    analyzeOptions :: Opt.Parser AnalyzeOptions
    analyzeOptions = AnalyzeOptions <$> optional contractFileOption

    createChestOptions :: Opt.Parser CreateChestOptions
    createChestOptions = CreateChestOptions <$> payloadOption <*> timeOption

    runOptions :: Opt.Parser RunOptions
    runOptions =
      RunOptions
        <$> optional contractFileOption
        <*> dbPathOption
        <*> typeCheckOptionsOption
        <*> valueOption Nothing (#name :! "storage")
            (#help :! "Initial storage of a running contract")
        <*> txDataOption
        <*> verboseFlag
        <*> nowOption
        <*> levelOption
        <*> maxStepsOption
        <*> mutezOption (Just defaultBalance)
            (#name :! "balance") (#help :! "Initial balance of this contract")
        <*> writeFlag

    originateOptions :: Opt.Parser OriginateOptions
    originateOptions =
      OriginateOptions
        <$> optional contractFileOption
        <*> dbPathOption
        <*> typeCheckOptionsOption
        <*> addressOption (Just genesisAddress)
            (#name :! "originator") (#help :! "Contract's originator")
        <*> optional
            (keyHashOption Nothing
              (#name :! "delegate") (#help :! "Contract's optional delegate")
            )
        <*> valueOption Nothing
            (#name :! "storage") (#help :! "Initial storage of an originating contract")
        <*> mutezOption (Just defaultBalance)
            (#name :! "balance") (#help :! "Initial balance of an originating contract")
        <*> verboseFlag

    transferOptions :: Opt.Parser TransferOptions
    transferOptions = do
      toDBPath <- dbPathOption
      toTcOptions <- typeCheckOptionsOption
      toDestination <-
        addressOption Nothing
        (#name :! "to") (#help :! "Destination address")
      toTxData <- txDataOption
      toNow <- nowOption
      toLevel <- levelOption
      toMaxSteps <- maxStepsOption
      toVerbose <- verboseFlag
      toDryRun <- dryRunFlag
      pure TransferOptions {..}

-- | Most permitting options, when we don't care much about typechecking.
laxTcOptions :: TypeCheck.TypeCheckOptions
laxTcOptions = TypeCheck.TypeCheckOptions
  { TypeCheck.tcVerbose = False
  , TypeCheck.tcStrict = False
  }

----------------------------------------------------------------------------
-- Actual main
----------------------------------------------------------------------------

main :: IO ()
main = wrapMain $ do
  cmdLnArgs <- execParser programInfo
  run cmdLnArgs
  where
    programInfo = info (helper <*> versionOption <*> argParser) $
      mconcat
      [ fullDesc
      , progDesc "Morley: Haskell implementation of Michelson typechecker and interpreter"
      , header "Morley tools"
      , footerDoc $ usageDoc
      ]

    versionOption = infoOption ("morley-" <> showVersion version)
      (long "version" <> help "Show version.")

    run :: CmdLnArgs -> IO ()
    run args = case args of
      Print (M mInputFile)
            (M mOutputFile)
            (N forceSingleLine) -> do
        contract <- prepareContract mInputFile
        let write = maybe putStrLn Utf8.writeFile mOutputFile
        write $ printUntypedContract forceSingleLine contract
      Optimize OptimizeOptions{..} -> do
        untypedContract <- prepareContract optoContractFile
        checkedContract <-
          either throwM pure . typeCheckingWith laxTcOptions $
            typeCheckContract untypedContract
        let optimizedContract = mapSomeContract optimize checkedContract
        let write = maybe putStrLn Utf8.writeFile optoOutput
        write $ printSomeContract optoSingleLine optimizedContract
      Analyze AnalyzeOptions{..} -> do
        untypedContract <- prepareContract aoContractFile
        SomeContract contract <-
          either throwM pure . typeCheckingWith laxTcOptions $
            typeCheckContract untypedContract
        putTextLn $ pretty $ analyze (cCode contract)
      TypeCheck TypeCheckOptions{..} -> do
        morleyContract <- prepareContract tcoContractFile
        -- At the moment of writing, 'tcStrict' option does not change anything
        -- because it affects only values parsing; but this may change
        contract <- either throwM pure . typeCheckingWith tcoTcOptions
          $ typeCheckContract morleyContract
        when (TypeCheck.tcVerbose tcoTcOptions) $
          putStrLn $ printSomeContract False contract
        putTextLn "Contract is well-typed"
      Run RunOptions {..} -> do
        michelsonContract <- prepareContract roContractFile
        void $ runContract roNow roLevel roMaxSteps roInitBalance roDBPath roTcOptions roStorageValue michelsonContract roTxData
          ! #verbose roVerbose
          ! #dryRun (not roWrite)
      Originate OriginateOptions {..} -> do
        michelsonContract <- prepareContract ooContractFile
        addr <-
          originateContract
            ooDBPath
            ooTcOptions
            ooOriginator
            ooDelegate
            ooBalance
            ooStorageValue
            michelsonContract
            ! #verbose ooVerbose
        putTextLn $ "Originated contract " <> pretty addr
      Transfer TransferOptions {..} -> do
        transfer toNow toLevel toMaxSteps toDBPath toTcOptions toDestination toTxData
          ! #verbose toVerbose
          ! #dryRun toDryRun
      REPL -> runRepl
      CreateChest CreateChestOptions {..} -> do
        (chest, key) <- createChestAndChestKey ccPayload ccTime
        putStrLn $ "Chest: 0x" <> encodeHex (chestBytes chest)
        putStrLn $ "Key: 0x" <> encodeHex (chestKeyBytes key)


usageDoc :: Maybe Doc
usageDoc = Just $ mconcat
   [ "You can use help for specific COMMAND", linebreak
   , "EXAMPLE:", linebreak
   , "  morley run --help", linebreak
   , linebreak
   , "Documentation for morley tools can be found at the following links:", linebreak
   , "  https://gitlab.com/morley-framework/morley/blob/master/README.md", linebreak
   , "  https://gitlab.com/morley-framework/morley/tree/master/docs", linebreak
   , linebreak
   , "Sample contracts for running can be found at the following link:", linebreak
   , "  https://gitlab.com/morley-framework/morley/tree/master/contracts", linebreak
   , linebreak
   , "USAGE EXAMPLE:", linebreak
   , "  morley parse --contract add1.tz", linebreak
   , linebreak
   , "  This command will parse contract stored in add1.tz", linebreak
   , "  and return its representation in haskell types", linebreak
   , linebreak
   , "  morley originate --contract add1.tz --storage 1 --verbose", linebreak
   , linebreak
   , "  This command will originate contract with code stored in add1.tz", linebreak
   , "  with initial storage value set to 1 and return info about", linebreak
   , "  originated contract: its balance, storage and contract code"]