-- SPDX-FileCopyrightText: 2021 Oxhead Alpha -- SPDX-License-Identifier: LicenseRef-MIT-OA module Morley.App.CLI ( argParser ) where import Data.Default (def) import Data.Singletons (demote) import Data.Text.Lazy.IO.Utf8 qualified as Utf8 (writeFile) import Fmt (pretty, prettyText, unlinesF) import Named (paramF) import Options.Applicative (help, long, short, subparser, switch) import Options.Applicative qualified as Opt import Text.Hex (encodeHex) import Morley.App.REPL import Morley.CLI import Morley.Michelson.Analyzer (analyze) import Morley.Michelson.Interpret (RemainingSteps(..)) import Morley.Michelson.Optimizer (OptimizerConf(..), optimizeVerboseWithConf) import Morley.Michelson.Printer (printSomeContract, printTypedContract, printUntypedContract) import Morley.Michelson.Runtime import Morley.Michelson.Runtime.GState (genesisAddress) import Morley.Michelson.TypeCheck (tcVerbose, typeCheckContract, typeCheckValue, typeCheckingWith) import Morley.Michelson.TypeCheck qualified as TypeCheck import Morley.Michelson.Typed (Contract'(..), SomeContract(..), unContractCode) import Morley.Michelson.Typed qualified as T import Morley.Michelson.Typed.Contract (mapContractCodeM) import Morley.Tezos.Address import Morley.Tezos.Address.Alias import Morley.Tezos.Core (Mutez, tz) import Morley.Tezos.Crypto.Timelock (chestBytes, chestKeyBytes, createChestAndChestKey) import Morley.Util.CLI (mkCLOptionParser, mkCommandParser, outputOption) import Morley.Util.Named data DryRunOrWrite = DryRun | Write mkCommandParser' :: String -> String -> Opt.Parser a -> Opt.Mod Opt.CommandFields a mkCommandParser' = flip . mkCommandParser argParser :: Opt.Parser (IO ()) argParser = subparser $ mconcat [ printSubCmd , typecheckSubCmd , emulateSubCmd , optimizeSubCmd , analyzeSubCmd , createChestSubCmd , replSubCmd ] -------------------------------------------------------------------------------- -- Subcommands -------------------------------------------------------------------------------- -- NB: in case this looks mysterious, in IOCmd functions, outer 'do' is -- Opt.Parser using ApplicativeDo, which then returns an IO action, i.e. inner -- 'do' is 'IO ()'. -- @lierdakil type IOCmd = Opt.Mod Opt.CommandFields (IO ()) typecheckSubCmd :: IOCmd typecheckSubCmd = mkCommandParser' "typecheck" "Typecheck passed contract." do contractFile <- optional contractFileOption tcOptions <- typeCheckOptionsOption pure do morleyContract <- prepareContract contractFile -- 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 tcOptions $ typeCheckContract morleyContract when (TypeCheck.tcVerbose tcOptions) $ putStrLn $ printSomeContract False contract putTextLn "Contract is well-typed" printSubCmd :: IOCmd printSubCmd = mkCommandParser' "print" "Parse a Morley contract and print corresponding Michelson \ \contract that can be parsed by the OCaml reference client: octez-client." do mInputFile <- optional contractFileOption mOutputFile <- outputOption forceSingleLine <- onelineOption pure do contract <- prepareContract mInputFile let write = maybe putStrLn Utf8.writeFile mOutputFile write $ printUntypedContract forceSingleLine contract emulateSubCmd :: IOCmd emulateSubCmd = mkCommandParser' "emulate" "Set of commands to run in an emulated environment." $ subparser $ mconcat [ runSubCmd , originateSubCmd , transferSubCmd , transferTicketSubCmd , runViewSubCmd ] runSubCmd :: IOCmd runSubCmd = mkCommandParser' "run" "Run passed contract. It's originated first and then a transaction is sent to it." do txData <- txDataOption contract <- contractSimpleOriginationDataOption cro <- commonRunOptions Write pure do michelsonContract <- traverse prepareContract contract void $ runContract cro michelsonContract txData runViewSubCmd :: IOCmd runViewSubCmd = mkCommandParser' "view" "Run some view on a contract either supplied directly or identified \ \by an address. It's originated first if supplied directly." do cro <- commonRunOptions Write addressOrContract <- ContractSpecAddressOrAlias <$> addressOrAliasOption Nothing ! #name "contract-addr" ! #help "Contract address to call view on." <|> ContractSpecOrigination <$> contractSimpleOriginationDataOption viewName <- mkCLOptionParser Nothing ! #name "name" ! #help "View name." sender <- someAddressOrAliasOption (Just $ SAOAKindSpecified $ AddressResolved genesisAddress) ! #name "sender" ! #help "Sender address." viewArg <- TxUntypedParam <$> valueOption Nothing ! #name "arg" ! #help "View call argument." pure do contractSpec <- (traverse . traverse) prepareContract addressOrContract void $ runView cro contractSpec viewName sender viewArg replSubCmd :: IOCmd replSubCmd = mkCommandParser' "repl" "Start a Morley REPL." $ pure runRepl originateSubCmd :: IOCmd originateSubCmd = mkCommandParser' "originate" "Originate passed contract. Add it to passed DB." do simpleOriginationData <- contractSimpleOriginationDataOption dbPath <- dbPathOption tcOptions <- typeCheckOptionsOption verbose <- verboseFlag originator <- addressOption (Just genesisAddress) ! #name "originator" ! #help "Contract's originator." alias <- optional (aliasOption "alias") delegate <- optional $ keyHashOption Nothing ! #name "delegate" ! #help "Contract's optional delegate." pure do michelsonContract <- traverse prepareContract simpleOriginationData addr <- originateContract ! #dbPath dbPath ! #tcOpts tcOptions ! #originator originator ! paramF #alias alias ! paramF #delegate delegate ! #csod michelsonContract ! #verbose verbose putTextLn $ "Originated contract " <> pretty addr transferSubCmd :: IOCmd transferSubCmd = mkCommandParser' "transfer" "Transfer tokens to given address." do destination <- someAddressOrAliasOption Nothing ! #name "to" ! #help "Address or alias of the transfer's destination." txData <- txDataOption cro <- commonRunOptions DryRun pure $ transfer cro destination txData transferTicketSubCmd :: IOCmd transferTicketSubCmd = mkCommandParser' "transfer-ticket" "Transfer ticket to given address." do destination <- someAddressOrAliasOption Nothing ! #name "to" ! #help "Address or alias of the transfer's destination." tdSenderAddress :: L1Address <- Constrained <$> addressOption (Just genesisAddress) ! #name "sender" ! #help "Sender address" ticketer <- mkCLOptionParser @Address Nothing ! #name "ticketer" ! #help "Ticketer" value <- valueOption Nothing ! #name "value" ! #help "Ticket value" ty <- mkCLOptionParser Nothing ! #name "type" ! #help "Ticket argument type" tAmount <- mkCLOptionParser @Natural Nothing ! #name "amount" ! #help "Amount of tickets" tdAmount <- mutezOption (Just minBound) ! #name "mutez" ! #help "Mutez amount additionally sent by a transaction. \ \Note that on network, as of Mumbai, implicit accounts can't send tickets \ \and mutez in the same operation, however the Morley emulator allows it." tdEntrypoint <- entrypointOption ! #name "entrypoint" ! #help "Entrypoint to call" cro <- commonRunOptions DryRun pure $ T.withUType ty \(_ :: T.Notes t) -> do tValue <- either throwM pure . typeCheckingWith (croTCOpts cro) $ typeCheckValue @t value T.Dict <- either (throwM . TypeCheck.UnsupportedTypeForScope (demote @t)) pure $ T.checkScope @(T.ParameterScope t, T.Comparable t) let tdParameter = TxTypedParam $ T.VTicket ticketer tValue tAmount transfer cro destination TxData{..} optimizeSubCmd :: IOCmd optimizeSubCmd = mkCommandParser' "optimize" "Optimize the contract." do contractFile <- optional contractFileOption output <- outputOption singleLine <- onelineOption maxStageIterations <- mkCLOptionParser (Just $ ocMaxIterations def) ! #name "max-stage-iterations" ! #help "Maximum number of iterations per optimizer stage. \ \The default is usually adequate, but you want to try raising it \ \to see if it affects the result." verbose <- verboseFlag pure do untypedContract <- prepareContract contractFile SomeContract checkedContract <- either throwM pure . typeCheckingWith laxTcOptions $ typeCheckContract untypedContract let (logs, optimizedContract) = mapContractCodeM (optimizeVerboseWithConf conf) checkedContract conf = def { ocMaxIterations = maxStageIterations } maybe putStrLn Utf8.writeFile output $ printTypedContract singleLine optimizedContract when verbose $ hPutStrLn stderr $ prettyText $ unlinesF logs analyzeSubCmd :: IOCmd analyzeSubCmd = mkCommandParser' "analyze" "Analyze the contract." do contractFile <- optional contractFileOption pure do untypedContract <- prepareContract contractFile SomeContract contract <- either throwM pure . typeCheckingWith laxTcOptions $ typeCheckContract untypedContract putTextLn $ pretty $ analyze (unContractCode $ cCode contract) createChestSubCmd :: IOCmd createChestSubCmd = mkCommandParser' "create_chest" "Create a timelocked chest and key." do payload <- payloadOption time <- timeOption pure do (chest, key) <- createChestAndChestKey payload time putStrLn $ "Chest: 0x" <> encodeHex (chestBytes chest) putStrLn $ "Key: 0x" <> encodeHex (chestKeyBytes key) -------------------------------------------------------------------------------- -- Parsers -------------------------------------------------------------------------------- verboseFlag :: Opt.Parser Bool verboseFlag = switch $ short 'v' <> long "verbose" <> help "Whether output should be verbose." typeCheckOptionsOption :: Opt.Parser TypeCheck.TypeCheckOptions typeCheckOptionsOption = do tcVerbose <- verboseFlag tcStrict <- fmap not . switch $ long "typecheck-lax" <> help "Whether actions permitted in `octez-client run` but forbidden in \ \e.g. `octez-client originate` should be allowed here." return TypeCheck.TypeCheckOptions{..} commonRunOptions :: DryRunOrWrite -> Opt.Parser CommonRunOptions commonRunOptions defaultDryRun = do croNow <- nowOption croLevel <- fromMaybe (croLevel def) <$> levelOption croMinBlockTime <- fromMaybe (croMinBlockTime def) <$> minBlockTimeOption croMaxSteps <- RemainingSteps <$> maxStepsOption croDBPath <- dbPathOption croTCOpts <- typeCheckOptionsOption croVerbose <- verboseFlag croDryRun <- case defaultDryRun of Write -> fmap not . switch $ long "write" <> help "Write updated DB to the DB file." DryRun -> switch $ long "dry-run" <> help "Do not write updated DB to the DB file." pure CommonRunOptions{..} contractSimpleOriginationDataOption :: Opt.Parser (ContractSimpleOriginationData (Maybe FilePath)) contractSimpleOriginationDataOption = do csodStorage <- valueOption Nothing ! #name "storage" ! #help "Initial storage of the contract." csodContract <- optional contractFileOption csodBalance <- mutezOption (Just defaultBalance) ! #name "balance" ! #help "Initial balance of the contract." pure ContractSimpleOriginationData{..} -------------------------------------------------------------------------------- -- Constants -------------------------------------------------------------------------------- defaultBalance :: Mutez defaultBalance = [tz|4|] -- | Most permitting options, when we don't care much about typechecking. laxTcOptions :: TypeCheck.TypeCheckOptions laxTcOptions = TypeCheck.TypeCheckOptions { TypeCheck.tcVerbose = False , TypeCheck.tcStrict = False }