diff --git a/cli/cli.repl b/cli/cli.repl new file mode 100644 index 000000000..fed0f251b --- /dev/null +++ b/cli/cli.repl @@ -0,0 +1,299 @@ +(module cli G + "Provides data model and utility functions for the Pact client terminal." + (defcap G () (enforce false "X")) + + (defschema cap + "Capability storage" + name:string + args:list + signers:[string]) + + (defschema signer + "Signer ID and optional signature" + signer:string + signature:string) + + (defschema env + "Transaction values that are kept in sync with REPL environment." + gaslimit:integer + gasprice:decimal + msgdata:object) + + (defschema data + "Client state object." + sender:string + ttl:integer + network-id:string + creation-time:time + nonce:string + host:string + chain-id:integer + signers:[object{signer}] + caps:[object{cap}] + code:string + env:object{env} + ) + + (deftable state:{data}) + (defconst S "S" + "State singleton key") + + (defschema keydata + "Public key storage with optional alias or keyfile." + key:string + alias:string + file:string) + + (deftable keystore:{keydata}) + + (defconst PACT_CHAIN_ID:integer -1 + "chain id sentinel for pact -s server to not use SSL/TLS.") + + (defun init () + "Reset state to default values." + (write + state S + { 'sender: "sender" + , 'ttl: 150000 + , 'network-id: "mainnet01" + , 'creation-time: (now) + , 'nonce: "nonce" + , 'host: "api.chainweb.com" + , 'chain-id: 0 + , 'signers: [] + , 'caps: [] + , 'code: "" + , 'env: + { 'gaslimit: 5000 + , 'gasprice: 0.00000000001 + , 'msgdata: {} + } + }) + (conform-env) + (rehash)) + + (defun conform-env () + "Conform REPL environment to state." + (with-read state S + { 'env:= e } + (env-data (at 'msgdata e)) + (env-gaslimit (at 'gaslimit e)) + (env-gasprice (at 'gasprice e)))) + + (defun sstore (name:string) + "Store state under a custom NAME." + (write state name (cli-state))) + + (defun sload(name:string) + "Load custom state NAME." + (write state S (read state name)) + (conform-env) + (rehash)) + + (defun cli-state () + "Read current state." + (read state S)) + + (defun mainnet () + "Mainnet state vars." + (network-id "mainnet01") + (host "api.chainweb.com") + (rehash)) + + (defun testnet () + "Testnet state vars." + (network-id "testnet04") + (host "api.testnet.chainweb.com") + (rehash)) + + (defun devnet () + "Devnet state vars." + (network-id "development") + (host "us1.tn1.chainweb.com") + (rehash)) + + (defun update-state (u:object rh:bool) + "Update state to U and if RH, rehash." + (update state S u) + (conform-env) + (if rh (rehash) "State updated")) + + (defun sender (s:string) + "Set state sender/gaspayer account." + (update-state { 'sender: s } true)) + + (defun sender-key (key:string) + "Set state sender/gaspayer account to KEY match in keystore." + (sender (find-key key))) + + (defun gaslimit (i:integer) + "Set state gaslimit." + (update-env { 'gaslimit: i })) + + (defun gasprice (d:decimal) + "Set state gasprice." + (update-env { 'gasprice: d })) + + (defun msgdata (o:object) + "Set state message data." + (update-env { 'msgdata: o })) + + (defun update-env (e:object) + "Update and rehash state with env value." + (update-state + { 'env: + (+ e (at 'env (cli-state)))} + true)) + + (defun ttl (i:integer) + "Set state TTL." + (update-state { 'ttl: i } true)) + + (defun network-id (s:string) + "Set state network id." + (update-state { 'network-id: s } true)) + + (defun creation-time (t:time) + "Set state creation time." + (update-state { 'creation-time: t } true)) + + (defun creation-ymd (s:string) + "Set state creation time as 'YYYY-MM-DD'" + (creation-time (parse-time "%F" s))) + + (defun nonce (s:string) + "Set state nonce." + (update-state { 'nonce: s } true)) + + (defun host (h:string) + "Set state host value." + (update-state { 'host: h } false)) + + (defun chain-id (i:integer) + "Set state chain id." + (update-state { 'chain-id: i } true)) + + (defun set-code (s:string) + "Set state code." + (update-state { 'code: s } true)) + + (defun add-key:object{keydata} (key:string) + "Add KEY as public key to keystore with default values." + (with-default-read keystore key + { 'file: "", 'alias: "" } + { 'file:= f, 'alias:= a } + (let ((k { 'key: key, 'file: f, 'alias: a })) + (write keystore key k) + k))) + + (defun add-key-alias:object{keydata} (alias:string key:string) + "Add KEY with ALIAS to keystore." + (with-default-read keystore key + { 'file: "" } + { 'file:= f } + (let ((k { 'key: key, 'file: f, 'alias: alias })) + (write keystore key k) + k))) + + (defun add-keyfile1:object{keydata} (key:string file:string) + "INTERNAL add keyfile under KEY with FILE." + (with-default-read keystore key + { 'alias: "" } + { 'alias:= a} + (let ((k { 'key: key, 'file: file, 'alias: a })) + (write keystore key k) + (rehash) + k))) + + (defun get-key:object{keydata} (key:string) + "Read keystore at KEY." + (read keystore key)) + + (defun add-cap1 (name:string args:list signers:[string]) + "Add signature capability (NAME ARGS) for SIGNERS." + (let ((ss (map (add-signer) signers))) + (update-state + { 'caps: + (+ [{ 'name: name + , 'args: args + , 'signers: ss }] + (at 'caps (read state S)))} + true))) + + (defun sign (signer:string signature:string) + "Set SIGNATURE for SIGNER." + (let ((s (add-signer signer))) + (update-state + { 'signers: + (map (sign1 s signature) + (at 'signers (cli-state))) } true) + (format "Added signature {}->{}" (map (elide) [signer signature])))) + + (defun elide:string (s:string) + "Elide to 12 chars" + (if (< (length s) 12) s + (+ (take 12 s) "..."))) + + (defun sign1 (signer:string signature:string s:object{signer}) + "INTERNAL update SIGNATURE in S if matches SIGNER." + (if (= signer (at 'signer s)) + (+ { 'signature: signature} s) + s)) + + + (defun add-signer-unsafe (s:string sig:string) + "INTERNAL add signer S with SIG without keystore check." + (let ((ss (at 'signers (read state S)))) + (if (contains s (map (at 'signer) ss)) "Already added" + (update-state + { 'signers: (+ [{'signer:s,'signature:sig}] ss) } true)))) + + (defun add-signer (signer:string) + "Matches SIGNER to keystore to add to signers list." + (enforce (!= "" signer) "Empty signer") + (let ((matched (find-key signer))) + (add-signer-unsafe matched "") + matched)) + + (defun add-keyset-signers (aliased-only:bool k:keyset) + " Adds all keys in keyset to signers unless ALIASED-ONLY, in which \ + \ case only adds aliased keys." + (let* ((ks (add-keyset k)) + (ks1 (if aliased-only + (filter (compose (at 'alias) (!= "")) ks) + ks))) + (map (compose (at 'key) (add-signer)) ks1) + ks1)) + + (defun clear-sigs () + "Clear signatures from state." + (update-state + { 'signers: + (map (+ { 'signature: "" }) + (at 'signers (cli-state))) } + false)) + + (defun all-keys () + "Table read from keystore." + (select keystore (constantly true))) + + (defun find-key:string (k:string) + "Searches keystore by key and alias for K, returning key string" + (let ((matched (fold (match-key k) "" (all-keys)))) + (enforce (!= "" matched) (format "Key not found: {}" [k])) + matched)) + + (defun match-key (part:string r:string key:object{keydata}) + "INTERNAL for find-key." + (if (or (= part (take (length part) (at 'key key))) + (= part (take (length part) (at 'alias key)))) + (let ((x 0)) + (enforce (= "" r) "Multiple matches found") + (at 'key key)) + r)) + + +) +(create-table state) +(create-table keystore) +(init) diff --git a/pact.cabal b/pact.cabal index 912a92582..750c6fa46 100644 --- a/pact.cabal +++ b/pact.cabal @@ -27,6 +27,11 @@ flag ghc-flags default: False manual: True +flag cli-repl + description: Include support for Pact Cli terminal. + default: False + manual: True + flag build-tool description: Include modules and deps for tests + executable default: True @@ -143,6 +148,7 @@ library , vector >= 0.11.0.0 && < 0.13 , vector-algorithms >= 0.7 , vector-space >= 0.10.4 && < 0.17 + if flag(ghc-flags) build-tool-depends: hsinspect:hsinspect ghc-options: -fplugin GhcFlags.Plugin @@ -263,6 +269,20 @@ library if !os(windows) build-depends: unix + if flag(cli-repl) + exposed-modules: + Pact.Repl.Cli + Pact.Embed + build-depends: + , connection >= 0.3.1 + , http-client-tls >= 0.3.5.3 + , template-haskell + , qrcode-core + , qrcode-juicypixels + , JuicyPixels + cpp-options: -DCLI_REPL + + executable pact if impl(ghcjs) || !flag(build-tool) buildable: False diff --git a/src-ghc/Pact/ApiReq.hs b/src-ghc/Pact/ApiReq.hs index a06fbe7bb..2f99734b8 100644 --- a/src-ghc/Pact/ApiReq.hs +++ b/src-ghc/Pact/ApiReq.hs @@ -23,11 +23,13 @@ module Pact.ApiReq ,mkExec ,mkCont ,mkKeyPairs + ,mkUnsignedExec ,AddSigsReq(..),addSigsReq ,combineSigs ,combineSigDatas ,signCmd ,decodeYaml + ,importKeyFile ,returnCommandIfDone ) where diff --git a/src-ghc/Pact/Embed.hs b/src-ghc/Pact/Embed.hs new file mode 100644 index 000000000..53858280b --- /dev/null +++ b/src-ghc/Pact/Embed.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE TemplateHaskell #-} +module Pact.Embed where + +import Language.Haskell.TH +import Language.Haskell.TH.Syntax +import qualified Data.ByteString as B +import qualified Data.ByteString.Internal as B +import qualified Data.ByteString.Char8 as B8 +import Data.ByteString.Unsafe (unsafePackAddressLen) +import System.IO.Unsafe (unsafePerformIO) + +embedFile :: FilePath -> Q Exp +embedFile fp = do + qAddDependentFile fp + bs@(B.PS ptr off sz) <- runIO $ B.readFile fp + return + (AppE + (VarE 'unsafePerformIO) + (AppE + (AppE + (VarE 'unsafePackAddressLen) + (LitE (IntegerL $ fromIntegral $ B8.length bs))) + (LitE (bytesPrimL (mkBytes ptr (fromIntegral off) (fromIntegral sz)))))) diff --git a/src-ghc/Pact/Main.hs b/src-ghc/Pact/Main.hs index 8ab730361..3a3734f09 100644 --- a/src-ghc/Pact/Main.hs +++ b/src-ghc/Pact/Main.hs @@ -56,6 +56,9 @@ import Pact.Types.Crypto import Pact.Types.SPV import Pact.ApiReq +#ifdef CLI_REPL +import Pact.Repl.Cli +#endif data Option = OVersion @@ -69,11 +72,21 @@ data Option = | OAddSigsReq { _oKeyFiles :: [FilePath], _oReqLocal :: Bool } | OCombineSigs { _oSigFiles :: [FilePath], _oReqLocal :: Bool } | OSignCmd { _oSigFile :: [FilePath] } +#ifdef CLI_REPL + | OCli { _oCliConfig :: Maybe FilePath, _oCliLoad :: Maybe FilePath } +#endif deriving (Eq,Show) replOpts :: O.Parser Option replOpts = - O.subparser (addSigParser <> combineSigsParser <> signParser) + O.subparser + (addSigParser + <> combineSigsParser + <> signParser +#ifdef CLI_REPL + <> cliParser +#endif + ) <|> O.flag' OVersion (O.short 'v' <> O.long "version" <> O.help "Display version") <|> @@ -108,16 +121,18 @@ replOpts = localFlag :: O.Parser Bool localFlag = O.flag False True (O.short 'l' <> O.long "local" <> O.help "Format for /local endpoint") +cmdInfo :: String -> O.InfoMod a +cmdInfo synopsis = O.fullDesc + <> O.header synopsis + <> O.progDesc synopsis + combineSigsParser :: O.Mod O.CommandFields Option combineSigsParser = O.command "combine-sigs" $ O.info (OCombineSigs <$> parser <*> localFlag <**> O.helper) i where parser = many $ O.strArgument ( O.metavar "SIG_FILE" <> O.help "A signature file generated by \"pact -u\" or \"pact add-sig\"") - i = O.fullDesc - <> O.header synopsis - <> O.progDesc synopsis - synopsis = "Combine multiple signature files" + i = cmdInfo "Combine multiple signature files" addSigParser :: O.Mod O.CommandFields Option addSigParser = O.command "add-sig" $ O.info (OAddSigsReq <$> parser <*> localFlag <**> O.helper) i @@ -125,10 +140,7 @@ addSigParser = O.command "add-sig" $ O.info (OAddSigsReq <$> parser <*> localFla parser = many $ O.strArgument ( O.metavar "KEY_FILE" <> O.help "File with a public / private key pair in the format generated by \"pact -g\"") - i = O.fullDesc - <> O.header synopsis - <> O.progDesc synopsis - synopsis = "Add a signature to a signature data from stdin" + i = cmdInfo "Add a signature to a signature data from stdin" signParser :: O.Mod O.CommandFields Option signParser = O.command "sign" $ O.info (OSignCmd <$> parser <**> O.helper) i @@ -136,11 +148,17 @@ signParser = O.command "sign" $ O.info (OSignCmd <$> parser <**> O.helper) i parser = many $ O.strArgument ( O.metavar "KEY_FILE" <> O.help "File with a public / private key pair in the format generated by \"pact -g\"") - i = O.fullDesc - <> O.header synopsis - <> O.progDesc synopsis - synopsis = "Sign arbitrary base64url-encoded data from stdin" + i = cmdInfo "Sign arbitrary base64url-encoded data from stdin" +#ifdef CLI_REPL +cliParser :: O.Mod O.CommandFields Option +cliParser = O.command "cli" $ O.info (OCli <$> parser <*> fParser <**> O.helper) (cmdInfo "Run cli pact terminal") + where + parser = optional $ O.strOption + (O.metavar "CONFIG_FILE" <> O.help cliHelp <> O.short 'c' <> O.long "cli-file") + fParser = optional $ O.argument O.str + (O.metavar "LOAD_FILE" <> O.help "Repl to load") +#endif argParser :: O.ParserInfo Option argParser = O.info (O.helper <*> replOpts) @@ -180,6 +198,12 @@ main = O.execParser argParser >>= \as -> case as of OCombineSigs sigs l -> BS8.putStrLn =<< combineSigs sigs l OSignCmd kfs -> BS8.putStrLn =<< signCmd kfs =<< fmap (encodeUtf8 . T.strip) T.getContents +#ifdef CLI_REPL + OCli confM loadM -> do + m <- getMode + s <- setupCli m confM loadM + generalRepl' m s >>= exitEither (const (return ())) +#endif where diff --git a/src-ghc/Pact/Repl/Cli.hs b/src-ghc/Pact/Repl/Cli.hs new file mode 100644 index 000000000..a0f130a1b --- /dev/null +++ b/src-ghc/Pact/Repl/Cli.hs @@ -0,0 +1,669 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TupleSections #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeApplications #-} + +{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +-- | +-- Module : Pact.Repl.Cli +-- Copyright : (C) 2020 Stuart Popejoy +-- License : BSD-style (see the file LICENSE) +-- Maintainer : Stuart Popejoy +-- +-- Pact client terminal implementation. +-- + +module Pact.Repl.Cli + ( setupCli + , cliHelp + ) where + +import qualified Codec.Picture as JP +import qualified Codec.QRCode as QR +import qualified Codec.QRCode.JuicyPixels as QR +import Control.Lens +import Control.Monad.Catch +import Control.Monad.Reader +import Control.Monad.State.Strict +import Data.Aeson as Aeson hiding (Object,(.=)) +import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as BSL +import Data.Decimal +import Data.Default +import Data.Function +import qualified Data.Map.Strict as M +import Data.Maybe +import qualified Data.HashMap.Strict as HM +import Data.List (sortBy,elemIndex) +import Data.Semigroup (Endo(..)) +import qualified Data.Set as S +import Data.String +import Data.Text (Text,pack,unpack,intercalate) +import Data.Text.Encoding +import qualified Data.Vector as V +import qualified Data.Yaml as Y +import GHC.Generics +import Network.Connection +import Network.HTTP.Client.TLS +import Servant.Client.Core +import Servant.Client +import System.IO +import System.Directory +import System.FilePath +import Text.Trifecta as TF hiding (line,err,try,newline) + +import Pact.ApiReq +import Pact.Compile +import Pact.Embed +import Pact.Eval +import Pact.Native +-- intentionally hidden unused functions to prevent lib functions from consuming gas +import Pact.Native.Internal hiding (defRNative,defGasRNative,defNative) +import Pact.Parse +import Pact.Repl +import Pact.Repl.Lib +import Pact.Repl.Types +import Pact.Time +import Pact.Types.API +import Pact.Types.Capability +import Pact.Types.Command +import Pact.Types.Runtime +import Pact.Types.PactValue +import Pact.Types.Pretty +import Pact.Types.SigData +import Pact.Server.API + +data KeyPairFile = KeyPairFile Text Text +instance FromJSON KeyPairFile where + parseJSON = withObject "KeyPairFile" $ \o -> + KeyPairFile <$> o .: "public" <*> o .: "secret" + +cliDefs :: NativeModule +cliDefs = ("Cli", + [ + defZNative "local" local' + (funType a [("exec",a)] <> + funType a []) + [LitExample "(local (+ 1 2))",LitExample "(local)"] + "Evaluate EXEC on server, or without argument, current code value (see 'code')." + , + defZRNative "eval-local" evalLocal + (funType a [("exp",a)]) + [LitExample "(eval \"(+ 1 2)\")"] + "Evaluate EXP locally on server." + , + defZNative "add-cap" addCap + (funType tTyString [("cap",TyFun $ funType' tTyBool []),("signers",TyList (tTyString))]) + [LitExample "(add-cap (coin.TRANSFER \"alice\" \"bob\" 10.0) [\"alice\"])"] + "Add signer capability CAP for SIGNERS" + , + defZRNative "import" import' + (funType tTyString [("modules", TyList (tTyString))]) + [LitExample "(import ['fungible-v2,'coin])"] + "Import and load code for MODULES from remote." + , + defZRNative "add-keyset" addKeyset + (funType (TyList (tTyObject TyAny)) [("keyset",tTyGuard (Just GTyKeySet))]) + [LitExample "(add-keyset (local (describe-keyset 'abc)))"] + "Add all keys in KEYSET to keystore, returning keydata objects." + , + defZNative "code" code' + (funType tTyString [("exprs",a)]) + [LitExample "(code (+ 1 2) (/ 3 4))"] + "Store EXPRS as current command code." + , + defZRNative "code-file" codeFile + (funType tTyString [("file",a)]) + [LitExample "(code-file \"transfers.repl\")"] + "Set current command code to contents of FILE" + , + defZRNative "rehash" rehash + (funType tTyString []) + [LitExample "(rehash)"] + "Build current command and hash, and update 'env-hash' accordingly." + , + defZRNative "add-keyfile" addKeyfile + (funType tTyString [("keyfile",tTyString)]) + [LitExample "(add-keyfile \"keys/000.yaml\")"] + "Add a key yaml file for signing." + , + defZRNative "now" now (funType tTyTime []) [LitExample "(now)"] + "Return current time" + , + defZRNative "send" send (funType tTyString []) [LitExample "(send)"] + "Build and send current command." + , + defZRNative "poll" poll + (funType a [] <> funType a [("req-key",tTyString)]) + [LitExample "(poll)", LitExample "(poll \"DldRwCblQ7Loqy6wYJnaodHl30d3j3eH-qtFzfEv46g\")"] + "Poll for result on server." + , + defZRNative "prep-offline" prepOffline + (funType tTyString [("file-root",tTyString)]) + [LitExample "(prep-offline \"offline-unsigned.yaml\")"] + "Generate FILE-ROOT.yaml and FILE-ROOT.png for offline signing." + , + defZRNative "read-offline" readOffline + (funType tTyString [("file",tTyString)]) + [LitExample "(read-offline \"sig.yaml\")"] + "Read offline signature yaml FILE." + , + defZRNative "show" showCmd + (funType tTyString []) + [LitExample "(show)"] + "Show/print current command." + , + defZRNative "resolve-keys" resolveGuard + (funType (TyList tTyString) [("guard",tTyGuard Nothing)]) + [LitExample "(resolve-keys (local (at 'guard (coin.details 'abc))))"] + "Attempt to resolve keystore keys/aliases in a remote guard." + ]) + + where + a = mkTyVar "a" [] + +resolveGuard :: RNativeFun LibState +resolveGuard i [TGuard g _] = case g of + GKeySet ks -> go ks + GKeySetRef ksn -> resKs ksn >>= go + _ -> evalError' i $ "resolve-guard: not a keyset/keysetref: " <> pretty g + where + resKs (KeySetName n) = do + r <- localExec i False ("(describe-keyset \"" <> n <> "\")") + case r of + PGuard (GKeySet ks) -> return ks + _ -> evalError' i $ "resolve-guard: unexpected result from server: " <> pretty r + go KeySet {..} = do + rs <- forM (S.toList _ksKeys) $ \k -> do + kr <- evalPact1 $ "(try {} (cli.get-key \"" <> renderCompactText k <> "\"))" + case kr of + (TObject (Object (ObjectMap m) _ _ _) _) -> case M.lookup "alias" m of + Nothing | m == mempty -> return Nothing + | otherwise -> return $ Just $ toTerm $ renderCompactText k + Just a -> return $ Just a + _ -> evalError' i $ "resolve-guard: unexpected result from get-key: " <> pretty kr + return $ toTList tTyString def (catMaybes rs) + + +resolveGuard i as = argsError i as + +addKeyset :: RNativeFun LibState +addKeyset _ [TGuard (GKeySet KeySet {..}) _] = do + fmap (toTList TyAny def) $ forM (S.toList _ksKeys) $ \k -> + evalPact1 $ "(cli.add-key \"" <> renderCompactText k <> "\")" +addKeyset i as = argsError i as + +instance Pretty Y.ParseException where + pretty = pretty . show + +addKeyfile :: RNativeFun LibState +addKeyfile i [TLitString f'] = do + f <- computeCurPath f' + (KeyPairFile p _s) <- eitherDie i =<< liftIO (Y.decodeFileEither f) + evalApp i "cli.add-keyfile1" [tStr p, tStr $ pack f] +addKeyfile i as = argsError i as + +import' :: RNativeFun LibState +import' i as = do + ms <- forM as $ \a -> case a of + TLitString m -> return $ "(describe-module \"" <> m <> "\")" + _ -> evalError' a "Expected string" + mds <- localExec i False $ "[" <> (intercalate " " ms) <> "]" + rs <- case mds of + PList vs -> forM vs $ \md -> + case (preview (_PAt "code" . _PString) md, preview (_PAt "name". _PString) md) of + (Just code,Just mname) -> do + (evalRefs . rsNamespace) .= + (case fromString (unpack mname) of + (ModuleName _ (Just ns)) -> + Just $ Namespace ns dummyKeyset dummyKeyset + _ -> Nothing) + evalPact' code + (_,_) -> evalError' i "Expected code, module name" + _ -> evalError' i "Expected list" + return $ toTList TyAny def (concat rs) + where + dummyKeyset = GKeySet $ mkKeySet [] "keys-all" + +now :: RNativeFun LibState +now _ _ = toTerm <$> liftIO getCurrentTime + + +prepOffline :: RNativeFun LibState +prepOffline i [TLitString file] = do + cmd <- buildCurrentCode i + sgs <- getSigners i + f <- computeCurPath file + let (sd :: SigData Text) = + SigData (_cmdHash cmd) (map toSigs (fst sgs)) Nothing + shortFile = f ++ ".yaml" + qrFile = f ++ ".png" + longFile = f ++ "-full.yaml" + liftIO $ Y.encodeFile shortFile sd + liftIO $ Y.encodeFile longFile (sd { _sigDataCmd = Just $ _cmdPayload cmd }) + qri <- maybeDie i "QR encode failed" $ + QR.encodeText (QR.defaultQRCodeOptions QR.L) QR.Iso8859_1OrUtf8WithECI {- QR.Iso8859_1 -} $ + decodeUtf8 $ Y.encode sd + liftIO $ JP.savePngImage qrFile $ JP.ImageY8 $ QR.toImage 4 3 qri + return $ tStr $ pack $ "Wrote " ++ shortFile ++ ", " ++ longFile ++ ", " ++ qrFile + where + toSigs (Signer _ s _ _) = (PublicKeyHex s,Nothing) +prepOffline i as = argsError i as + +computeCurPath :: Text -> Eval LibState FilePath +computeCurPath file = do + curFileM <- viewLibState _rlsFile + return $ computePath curFileM (unpack file) + +readOffline :: RNativeFun LibState +readOffline i [TLitString f] = do + file <- computeCurPath f + SigData{..} <- eitherDie i =<< liftIO (Y.decodeFileEither @(SigData Text) file) + cmd <- buildCurrentCode i + when (_cmdHash cmd /= _sigDataHash) $ evalError' i $ "Sig data file hash mismatch: " <> pretty _sigDataHash + rs <- forM _sigDataSigs $ \(PublicKeyHex k,sigM) -> forM sigM $ \(UserSig sig) -> + evalApp i "cli.sign" [toTerm k,toTerm sig] + return $ toTList TyAny def $ catMaybes rs + +readOffline i as = argsError i as + +termToCode :: Term Ref -> Text +termToCode (TLiteral l _) = renderCompactText l +termToCode (TList vs _ _) = "[" <> intercalate " " (V.toList $ termToCode <$> vs) <> "]" +termToCode (TObject (Object (ObjectMap om) _ ko _) _) = + "{" <> intercalate ", " (map go (psort ko $ M.toList $ (termToCode <$> om))) <> "}" + where psort Nothing = id + psort (Just o) = sortBy (compare `on` ((`elemIndex` o) . fst)) + go (k,v) = renderCompactText k <> ": " <> v +termToCode (TApp (App f as _) _) = + "(" <> termToCode f <> " " <> intercalate " " (map termToCode as) <> ")" +termToCode (TConst (Arg n _ _) mn _ _ _) = case mn of + Nothing -> n + Just m -> renderCompactText m <> "." <> n +termToCode (TVar v _) = case v of + Direct d -> termToCode $ fmap (\_ -> error "Direct with var unsupported") d + Ref r -> termToCode r +termToCode TNative {..} = renderCompactText _tNativeName +termToCode (TDef Def {..} _) = renderCompactText _dModule <> "." <> renderCompactText _dDefName +termToCode t = renderCompactText t + +buildCurrentCode :: HasInfo i => i -> Eval LibState (Command Text) +buildCurrentCode i = do + code <- cliState i "code" (_PLiteral . _LString) + buildCmd i True code + +jsonToTerm + :: (ToJSON b, HasInfo i, Show b) => i -> b -> Eval e (Term Name) +jsonToTerm i r = case fromJSON $ toJSON r of + Aeson.Success s -> return $ fromPactValue s + e -> evalError' i $ "Failed to coerce JSON: " <> (pretty $ show (e,r)) + +valueToTerm + :: HasInfo i => i -> Value -> Eval e (Term Name) +valueToTerm i v = case fromJSON v of + Aeson.Success s -> return $ fromPactValue s + e -> evalError' i $ "Failed to coerce JSON: " <> (pretty $ show (e,v)) + +send :: RNativeFun LibState +send i _ = do + cmd <- buildCurrentCode i + env <- buildEndpoint i + r <- sendTx i env (mkSubmitBatch cmd []) + jsonToTerm i r + +poll :: RNativeFun LibState +poll i as = case as of + [TLitString k] -> (go . RequestKey) =<< eitherDie i (fromText' k) + [] -> do + cmd <- buildCurrentCode i + go $ RequestKey $ toUntypedHash $ _cmdHash cmd + _ -> argsError i as + where + go :: RequestKey -> Eval LibState (Term Name) + go rk = do + env <- buildEndpoint i + PollResponses r <- sendPoll i env (mkPoll rk []) + case HM.lookup rk r of + Just CommandResult{..} -> do + met <- valueToTerm i (maybe (String "[empty]") id _crMetaData) + return $ toTObject TyAny def $ + [("result", case _crResult of + (PactResult (Left e)) -> tStr (tShow e) + (PactResult (Right sr)) -> fromPactValue sr) + ,("gas",toTerm @Int $ fromIntegral _crGas) + ,("meta",met) + ,("events",toTList TyAny def $ map ev _crEvents) + ] + Nothing -> return $ tStr "No response" + ev PactEvent{..} = toTObject TyAny def $ + [("name",toTerm _eventName) + ,("params",toTList TyAny def (map fromPactValue _eventParams)) + ,("module",toTerm (renderCompactText _eventModule)) + ,("module-hash",toTerm (renderCompactText _eventModuleHash))] + +local' :: ZNativeFun LibState +local' i [] = do + code <- cliState i "code" (_PLiteral . _LString) + fromPactValue <$> localExec i True code +local' i as = do + let code = intercalate " " $ map termToCode as + fromPactValue <$> localExec i False code + +evalLocal :: RNativeFun LibState +evalLocal i [TLitString e] = do + fromPactValue <$> localExec i False e +evalLocal i as = argsError i as + +code' :: ZNativeFun LibState +code' i as = do + let code = intercalate " " $ map termToCode as + evalApp i "cli.set-code" [tStr code] + +codeFile :: RNativeFun LibState +codeFile i [TLitString f] = do + f' <- computeCurPath f + code <- liftIO $ readFile f' + evalApp i "cli.set-code" [tStr $ pack code] +codeFile i as = argsError i as + +rehash :: RNativeFun LibState +rehash i _ = do + code <- cliState i "code" (_PLiteral . _LString) + cmd <- buildCmd i True code + evalApp i "env-hash" $ [tStr $ asString $ _cmdHash cmd] + +showCmd :: RNativeFun LibState +showCmd i _ = do + code <- cliState i "code" (_PLiteral . _LString) + c@Command{..} <- buildCmd i True code + liftIO $ do + putStrLn "Hash:" + BS.putStr $ " " <> Y.encode _cmdHash + putStrLn "Sigs:" + forM_ _cmdSigs $ \s -> BS.putStr $ " " <> Y.encode s + putStrLn "Payload:" + BS.putStr . Y.encode . decodeStrict @Value . encodeUtf8 $ _cmdPayload + putStrLn "JSON:" + BSL.putStrLn $ encode c + return $ tStr "---" + + + + +localExec :: HasInfo i => i -> Bool -> Text -> Eval LibState PactValue +localExec i includeSigners code = do + cmd <- buildCmd i includeSigners code + env <- buildEndpoint i + r <- sendLocal i env cmd + case _pactResult (_crResult r) of + Left e -> throwM e + Right v -> return v + +evalAppExpect :: HasInfo i => i -> Text -> [Term Name] -> Fold PactValue a -> Eval e a +evalAppExpect i an as f = evalApp i an as >>= toPV >>= \r -> case preview f r of + Nothing -> evalError' i $ "unexpected result for app " <> pretty (an,as) + Just v -> return v + +evalApp :: HasInfo i => i -> Text -> [Term Name] -> Eval e (Term Name) +evalApp i an as = do + let qn = parseQualifiedName (getInfo i) an + nn = case qn of + Left {} -> Name (BareName an def) + Right q -> QName $ q + eval (TApp (App (TVar nn def) as def) def) + +addCap :: ZNativeFun LibState +addCap i [TApp (App n as _) _,signers] = do + let name = tStr $ termToCode n + as' <- mapM reduce as + signers' <- reduce signers + evalApp i "cli.add-cap1" + [ name + , TList (V.fromList as') TyAny def + , signers' ] + + +addCap i as = argsError' i as + +cliState :: HasInfo i => i -> FieldKey -> Fold PactValue a -> Eval LibState a +cliState i k p = evalExpect i (asString k) (_head . _PAt k . p) "(cli.cli-state)" + +buildEndpoint :: HasInfo i => i -> Eval LibState ClientEnv +buildEndpoint i = do + cid <- cliState i "chain-id" (_PLiteral . _LInteger) + pactCid <- evalExpect1 i "integer" (_PLiteral . _LInteger) "PACT_CHAIN_ID" + let isPact = cid == pactCid + host <- cliState i "host" (_PLiteral . _LString) + nw <- cliState i "network-id" (_PLiteral . _LString) + let url | isPact = "http://" <> host <> "/" + | otherwise = "https://" <> host <> "/chainweb/0.0/" <> nw <> "/chain/" <> tShow cid <> "/pact" + burl <- parseBaseUrl (unpack url) + -- mgr <- liftIO $ newManager defaultManagerSettings + liftIO $ getClientEnv burl + +_PString :: Fold PactValue Text +_PString = _PLiteral . _LString +_PInteger :: Fold PactValue Integer +_PInteger = _PLiteral . _LInteger +_PDecimal :: Fold PactValue Decimal +_PDecimal = _PLiteral . _LDecimal +_PTime :: Fold PactValue UTCTime +_PTime = _PLiteral . _LTime +_PBool :: Fold PactValue Bool +_PBool = _PLiteral . _LBool +_PObjectMap :: Fold PactValue (M.Map FieldKey PactValue) +_PObjectMap = _PObject . objectMap +_PAt :: FieldKey -> Fold PactValue PactValue +_PAt k = _PObjectMap . ix k + +buildCmd :: HasInfo i => i -> Bool -> Text -> Eval LibState (Command Text) +buildCmd i includeSigners cmd = withAppliedEnv $ do + ttl <- cliState i "ttl" _PInteger + nw <- NetworkId <$> cliState i "network-id" _PString + ctime <- cliState i "creation-time" _PTime + nonce <- cliState i "nonce" _PString + cid <- cliState i "chain-id" _PInteger + sender <- cliState i "sender" _PString + gasLimit <- view (eeGasEnv . geGasLimit) + gasPrice <- view (eeGasEnv . geGasPrice) + md <- view eeMsgBody + let toCT = TxCreationTime . fromIntegral . (`div` 1000000) . toPosixTimestampMicros + (signers,sigs) <- if includeSigners then getSigners i else return ([],Nothing) + cmdu <- liftIO $ mkUnsignedExec + cmd + md + (PublicMeta (ChainId (tShow cid)) + sender gasLimit gasPrice (fromIntegral ttl) + (toCT ctime)) + signers + (Just nw) + (Just nonce) + return $ case sigs of + Nothing -> cmdu + Just ss -> set cmdSigs ss cmdu + +withAppliedEnv :: Eval LibState a -> Eval LibState a +withAppliedEnv a = do + f <- viewLibState $ \s -> case _rlsOp s of + UpdateEnv e -> e + _ -> mempty + local (appEndo f) a + +getSigners :: HasInfo i => i -> Eval LibState ([Signer],Maybe [UserSig]) +getSigners i = do + sss <- cliState i "signers" _PList >>= \ss -> forM (V.toList ss) $ \s -> + case (str s "signer",str s "signature") of + (Just a,Just b) + | b /= "" -> return (a, Just $ UserSig b) + | otherwise -> do + ks <- evalAppExpect i "cli.get-key" [tStr a] (_PAt "file" . _PString) + case ks of + "" -> return (a, Nothing) + kf -> ((a,) . Just) <$> signHashWithFile (unpack kf) + _ -> evalError' i $ "invalid signer: " <> pretty s + caps <- fmap (M.fromListWith (++) . concat) $ cliState i "caps" _PList >>= \ss -> forM (V.toList ss) $ \s -> + case (str s "name", lst s "args", lst s "signers") of + (Just n,Just as,Just css) -> do + qn <- eitherDie i $ parseQualifiedName (getInfo i) n + let cap = [SigCapability qn (V.toList as)] + forM (V.toList css) $ \cs -> case preview _PString cs of + Just ps -> return (ps,cap) + _ -> evalError' i $ "invalid signer in cap: " <> pretty cs + _ -> evalError' i $ "invalid cap: " <> pretty s + return (map (mkSigner caps) (map fst sss),sequence (map snd sss)) + where + str o k = preview (_PAt k . _PString) o + lst o k = preview (_PAt k . _PList) o + mkSigner caps s = Signer Nothing s Nothing $ case M.lookup s caps of + Nothing -> [] + Just cs -> cs + + +signHashWithFile :: FilePath -> Eval LibState UserSig +signHashWithFile fp = do + skp <- liftIO $ importKeyFile fp + h <- view eeHash + liftIO $ signHash (fromUntypedHash h) skp + +getClientEnv :: BaseUrl -> IO ClientEnv +getClientEnv url = flip mkClientEnv url <$> newTlsManagerWith mgrSettings + where + mgrSettings = mkManagerSettings + (TLSSettingsSimple True False False) + Nothing + +evalExpect1 :: HasInfo i => i -> Text -> Fold PactValue a -> Text -> Eval LibState a +evalExpect1 i msg f = evalExpect i msg (_head . f) + +evalExpect :: HasInfo i => i -> Text -> Fold [PactValue] a -> Text -> Eval LibState a +evalExpect i msg f cmd = do + r <- evalPactValue cmd + case preview f r of + Nothing -> evalError' i $ "Expected " <> pretty msg <> " for command " <> pretty cmd + Just v -> return v + +evalPactValue :: Text -> Eval e [PactValue] +evalPactValue e = evalPact' e >>= traverse toPV + +toPV :: Term Name -> Eval e PactValue +toPV t = eitherDie t $ toPactValue t + +evalPact1 :: Text -> Eval e (Term Name) +evalPact1 = fmap head . evalPact' + +evalPact' :: Text -> Eval e [Term Name] +evalPact' cmd = compilePact cmd >>= mapM eval + +compilePact :: Text -> Eval e [Term Name] +compilePact cmd = case TF.parseString exprsOnly mempty (unpack cmd) of + TF.Success es -> mapM go es + TF.Failure f -> evalError def $ unAnnotate $ _errDoc f + where + go e = case compile (mkTextInfo cmd) e of + Right t -> return t + Left l -> evalError (peInfo l) (peDoc l) + +data CliConfig = CliConfig + { cliRepl :: Maybe FilePath + , preloads :: [FilePath] + } + deriving (Eq,Show,Generic) +instance FromJSON CliConfig + +instance Default CliConfig where + def = CliConfig + { cliRepl = Nothing + , preloads = [] } + +cliHelp :: String +cliHelp = "YAML file with properties 'cliRepl' (location of cli.repl file) and 'preloads' (list of repls to pre-load)" + +cliModule :: String +cliModule = BS.unpack $ $(embedFile "cli/cli.repl") + +loadCli :: Maybe FilePath -> Repl () +loadCli confm = case confm of + Nothing -> loadDotFile + Just c -> Y.decodeFileThrow c >>= go + where + go CliConfig{..} = do + + -- add all lib and cli defs to natives + rEnv . eeRefStore . rsNatives %= + HM.union (moduleToMap ("",snd cliDefs <> snd replDefs)) + void $ maybe + (evalRepl' cliModule) + (loadFile def) + cliRepl + forM_ preloads (loadFile def) + + loadDotFile = do + home <- liftIO getHomeDirectory + let dotFile = home ".pact-cli" + x <- liftIO $ doesFileExist dotFile + if x then do + pwd <- liftIO getCurrentDirectory + liftIO $ setCurrentDirectory home + Y.decodeFileThrow dotFile >>= go + liftIO $ setCurrentDirectory pwd + else + go def + + +setupCli :: ReplMode -> Maybe FilePath -> Maybe FilePath -> IO ReplState +setupCli m confm loadm = do + s <- initReplState m Nothing + (`execStateT` s) $ do + useReplLib + loadCli confm + mapM_ (loadFile def) loadm + +_cli :: IO () +_cli = do + s <- setupCli Interactive Nothing Nothing + void $ (`evalStateT` s) $ forever $ pipeLoop True stdin Nothing + +_eval :: Eval LibState a -> IO a +_eval e = do + s <- setupCli Interactive Nothing Nothing + (r,_) <- evalStateT (evalEval def e) s + either (error . show) return r + +_run :: String -> IO () +_run f = do + r <- _eval $ evalPact' $ pack f + mapM_ (putStrLn . unpack . renderCompactText) r + +_testCode :: Text -> IO [Text] +_testCode code = _eval (fmap termToCode <$> (compilePact code >>= mapM enscope)) + + +sendTx :: HasInfo i => i -> ClientEnv -> SubmitBatch -> Eval e RequestKeys +sendTx i env sb = + liftIO (runClientM (sendClient sb) env) >>= eitherDieS i + +sendLocal :: HasInfo i => i -> ClientEnv -> Command Text -> Eval e (CommandResult Hash) +sendLocal i env cmd = + liftIO (runClientM (localClient cmd) env) >>= eitherDieS i + +sendPoll :: HasInfo i => i -> ClientEnv -> Poll -> Eval e (PollResponses) +sendPoll i env p = + liftIO (runClientM (pollClient p) env) >>= eitherDieS i + +eitherDieS :: HasInfo i => Show a => i -> Either a b -> Eval e b +eitherDieS i = either (evalError' i . pretty . show) return + +eitherDie :: HasInfo i => Pretty a => i -> Either a b -> Eval e b +eitherDie i = either (evalError' i . pretty) return + +maybeDie :: HasInfo i => i -> Text -> Maybe b -> Eval e b +maybeDie i msg = maybe (evalError' i $ pretty msg) return diff --git a/src-ghc/Pact/ReplTools.hs b/src-ghc/Pact/ReplTools.hs index bf44ae32b..6c880e7e1 100644 --- a/src-ghc/Pact/ReplTools.hs +++ b/src-ghc/Pact/ReplTools.hs @@ -24,7 +24,6 @@ import System.Console.Haskeline import Pact.Parse import Pact.Types.Runtime -import Pact.Native import Pact.Repl import Pact.Repl.Types @@ -40,11 +39,12 @@ completeFn :: (MonadIO m, MonadState ReplState m) => CompletionFunc m completeFn = completeQuotedWord (Just '\\') "\"" listFiles $ completeWord (Just '\\') ("\"\'" ++ filenameWordBreakChars) $ \str -> do modules <- use (rEvalState . evalRefs . rsLoadedModules) + nats <- use (rEnv . eeRefStore . rsNatives) let namesInModules = toListOf (traverse . _1 . mdRefMap . to HM.keys . each) modules allNames = concat [ namesInModules , nameOfModule <$> HM.keys modules - , unName <$> HM.keys nativeDefs + , unName <$> HM.keys nats ] matchingNames = filter (str `isPrefixOf`) (unpack <$> allNames) pure $ simpleCompletion <$> matchingNames @@ -65,10 +65,13 @@ replSettings = Settings True -- automatically add each line to history generalRepl :: ReplMode -> IO (Either () (Term Name)) -generalRepl m = initReplState m Nothing >>= \s -> case m of +generalRepl m = initReplState m Nothing >>= generalRepl' m + +generalRepl' :: ReplMode -> ReplState -> IO (Either () (Term Name)) +generalRepl' m s = case m of Interactive -> evalStateT (runInputT replSettings (withInterrupt (haskelineLoop [] Nothing))) - (setReplLib s) + s _StdInPipe -> runPipedRepl s stdin type HaskelineRepl = InputT (StateT ReplState IO) diff --git a/src/Pact/Eval.hs b/src/Pact/Eval.hs index 01fad22d5..9442f3385 100644 --- a/src/Pact/Eval.hs +++ b/src/Pact/Eval.hs @@ -40,6 +40,7 @@ module Pact.Eval ,enforcePactValue,enforcePactValue' ,toPersistDirect ,reduceDynamic + ,enscope ,instantiate' ) where diff --git a/src/Pact/Repl.hs b/src/Pact/Repl.hs index a7465a9a2..65164871d 100644 --- a/src/Pact/Repl.hs +++ b/src/Pact/Repl.hs @@ -23,6 +23,7 @@ module Pact.Repl ( errToUnit , execScript , execScript' + , evalEval , execScriptF , execScriptF' , evalPact @@ -46,6 +47,8 @@ module Pact.Repl , replGetModules , replLookupModule , useReplLib + , pipeLoop + , computePath ) where import Prelude hiding (exp) @@ -113,7 +116,7 @@ runPipedRepl' p s@ReplState{} h = initReplState :: MonadIO m => ReplMode -> Maybe String -> m ReplState initReplState m verifyUri = - liftIO (initPureEvalEnv verifyUri) >>= \e -> return (ReplState e def m def def def) + liftIO (initPureEvalEnv verifyUri) >>= \e -> return (ReplState e def m def def) initPureEvalEnv :: Maybe String -> IO (EvalEnv LibState) initPureEvalEnv verifyUri = do @@ -225,7 +228,7 @@ pureEval ei e = do evalEval :: Info -> Eval LibState a -> Repl (Either PactError a, EvalState) evalEval ei e = do - (ReplState evalE evalS _ _ _ _) <- get + (ReplState evalE evalS _ _ _) <- get er <- try (liftIO $ runEval' evalS evalE e) return $ case er of Left (SomeException ex) -> (Left (PactError EvalError ei def (prettyString (show ex))),evalS) @@ -283,15 +286,12 @@ updateForOp i a = do -- Track file and use current file to mangle directory as necessary. loadFile :: Info -> FilePath -> Repl (Either String (Term Name)) loadFile i f = do - curFileM <- use rFile - let computedPath = case curFileM of - Nothing -> f -- no current file, just use f - Just curFile - | isAbsolute f -> f -- absolute always wins - | takeFileName curFile == curFile -> f -- current with no directory loses - | otherwise -> combine (takeDirectory curFile) f -- otherwise start with dir of curfile - restoreFile = rFile .= curFileM - rFile .= Just computedPath + mv <- use (rEnv.eePactDbVar) + curFileM <- view rlsFile <$> liftIO (readMVar mv) + let computedPath = computePath curFileM f + setFile f' = liftIO $ modifyMVar_ mv $ return . set rlsFile f' + restoreFile = setFile curFileM + setFile $ Just computedPath catch (do pr <- TF.parseFromFileEx exprsOnly computedPath srcBS <- liftIO $ BS.readFile computedPath @@ -309,6 +309,15 @@ loadFile i f = do outStrLn HErr pe return (Left (show e)) +-- | Mangle argument vs current file, if any. +computePath :: Maybe FilePath -> FilePath -> FilePath +computePath curFileM f = case curFileM of + Nothing -> f -- no current file, just use f + Just curFile + | isAbsolute f -> f -- absolute always wins + | takeFileName curFile == curFile -> f -- current with no directory loses + | otherwise -> combine (takeDirectory curFile) f -- otherwise start with dir of curfile + out :: ReplMode -> Hdl -> Bool -> String -> Repl () out m hdl newline str = case m of diff --git a/src/Pact/Repl/Lib.hs b/src/Pact/Repl/Lib.hs index 2eb9a820d..601be27aa 100644 --- a/src/Pact/Repl/Lib.hs +++ b/src/Pact/Repl/Lib.hs @@ -83,7 +83,7 @@ initLibState loggers verifyUri = do (newLogger loggers "Repl") def 0 def) createSchema m - return (LibState m Noop def def verifyUri M.empty def) + return (LibState m Noop def def verifyUri M.empty def def) -- | Native function with no gas consumption. type ZNativeFun e = FunApp -> [Term Ref] -> Eval e (Term Name) diff --git a/src/Pact/Repl/Types.hs b/src/Pact/Repl/Types.hs index aa8f32794..0214dc5ec 100644 --- a/src/Pact/Repl/Types.hs +++ b/src/Pact/Repl/Types.hs @@ -2,11 +2,11 @@ module Pact.Repl.Types ( ReplMode(..) , Hdl(..) - , ReplState(..),rEnv,rEvalState,rMode,rOut,rFile,rTermOut + , ReplState(..),rEnv,rEvalState,rMode,rOut,rTermOut , TestResult(..) , Repl , LibOp(..) - , LibState(..),rlsPure,rlsOp,rlsTx,rlsTests,rlsVerifyUri,rlsMockSPV,rlsDynEnv + , LibState(..),rlsPure,rlsOp,rlsTx,rlsTests,rlsVerifyUri,rlsMockSPV,rlsFile,rlsDynEnv , Tx(..) , SPVMockKey(..) , getAllModules @@ -52,7 +52,6 @@ data ReplState = ReplState { , _rMode :: ReplMode , _rOut :: String , _rTermOut :: [Term Name] - , _rFile :: Maybe FilePath } type Repl a = StateT ReplState IO a @@ -99,6 +98,7 @@ data LibState = LibState , _rlsTests :: [TestResult] , _rlsVerifyUri :: Maybe String , _rlsMockSPV :: M.Map SPVMockKey (Object Name) + , _rlsFile :: Maybe FilePath , _rlsDynEnv :: DynEnv } diff --git a/src/Pact/Types/API.hs b/src/Pact/Types/API.hs index bf0f146a1..7eaf38b49 100644 --- a/src/Pact/Types/API.hs +++ b/src/Pact/Types/API.hs @@ -18,8 +18,8 @@ module Pact.Types.API ( RequestKeys(..), rkRequestKeys - , SubmitBatch(..), sbCmds - , Poll(..) + , SubmitBatch(..), sbCmds, mkSubmitBatch + , Poll(..), mkPoll , PollResponses(..) , ListenerRequest(..) , ListenResponse(..) @@ -33,7 +33,7 @@ import Control.Monad import Data.Text (Text) import Data.Aeson hiding (Success) import qualified Data.HashMap.Strict as HM -import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty (NonEmpty(..)) import GHC.Generics @@ -55,6 +55,9 @@ newtype SubmitBatch = SubmitBatch { _sbCmds :: NonEmpty (Command Text) } deriving (Eq,Generic,Show) makeLenses ''SubmitBatch +mkSubmitBatch :: Command Text -> [Command Text] -> SubmitBatch +mkSubmitBatch h t = SubmitBatch (h :| t) + instance ToJSON SubmitBatch where toJSON = lensyToJSON 3 instance FromJSON SubmitBatch where @@ -64,6 +67,9 @@ instance FromJSON SubmitBatch where newtype Poll = Poll { _pRequestKeys :: NonEmpty RequestKey } deriving (Eq,Show,Generic) +mkPoll :: RequestKey -> [RequestKey] -> Poll +mkPoll h t = Poll (h :| t) + instance ToJSON Poll where toJSON = lensyToJSON 2 instance FromJSON Poll where diff --git a/src/Pact/Types/Term.hs b/src/Pact/Types/Term.hs index 7c1f20ad5..79b3a7f58 100644 --- a/src/Pact/Types/Term.hs +++ b/src/Pact/Types/Term.hs @@ -64,7 +64,7 @@ module Pact.Types.Term DefcapMeta(..), Example(..), derefDef, - ObjectMap(..),objectMapToListWith, + ObjectMap(..),objectMapToListWith,objectMap, Object(..),oObject,oObjectType,oInfo,oKeyOrder, FieldKey(..), Step(..),sEntity,sExec,sRollback,sInfo, @@ -1483,6 +1483,7 @@ makeLenses ''Step makeLenses ''ModuleHash makeLenses ''ModRef makePrisms ''Guard +makeLenses ''ObjectMap makeLenses ''FunApp makePrisms ''Ref'