Skip to content

Commit

Permalink
Improve builtin contract support for PAB
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mueller committed Apr 6, 2021
1 parent f007682 commit ad3e844
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 149 deletions.
Expand Up @@ -160,6 +160,7 @@
"Plutus/PAB/Db/Eventful/Query"
"Plutus/PAB/Effects/Contract"
"Plutus/PAB/Effects/Contract/ContractTest"
"Plutus/PAB/Effects/Contract/Builtin"
"Plutus/PAB/Effects/Contract/ContractExe"
"Plutus/PAB/Effects/ContractRuntime"
"Plutus/PAB/Effects/ContractTest/AtomicSwap"
Expand Down
1 change: 1 addition & 0 deletions plutus-contract/src/Plutus/Contract/State.hs
Expand Up @@ -16,6 +16,7 @@ module Plutus.Contract.State(
, ContractResponse(..)
, insertAndUpdateContract
, initialiseContract
, mkResponse
) where

import Control.Monad.Freer.Extras.Log (LogMessage)
Expand Down
1 change: 1 addition & 0 deletions plutus-pab/plutus-pab.cabal
Expand Up @@ -84,6 +84,7 @@ library
Plutus.PAB.Db.Eventful.Query
Plutus.PAB.Effects.Contract
Plutus.PAB.Effects.Contract.ContractTest
Plutus.PAB.Effects.Contract.Builtin
Plutus.PAB.Effects.Contract.ContractExe
Plutus.PAB.Effects.ContractRuntime
Plutus.PAB.Effects.ContractTest.AtomicSwap
Expand Down
172 changes: 172 additions & 0 deletions plutus-pab/src/Plutus/PAB/Effects/Contract/Builtin.hs
@@ -0,0 +1,172 @@
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-
Builtin contracts that are compiled together with the PAB.
-}
module Plutus.PAB.Effects.Contract.Builtin(
Builtin
, ContractConstraints
, SomeBuiltin(..)
, handleBuiltin
-- * Extracting schemas from contracts
, type (.\\)
, type (.\/)
, BlockchainActions
, Empty
, endpointsToSchemas
) where

import Control.Monad.Freer
import Control.Monad.Freer.Error (Error, throwError)
import Data.Aeson (FromJSON, ToJSON, Value)
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import Data.Row
import qualified Data.Text as Text

import Plutus.PAB.Effects.Contract (ContractEffect (..), PABContract (..))
import Plutus.PAB.Events.Contract (ContractPABRequest)
import qualified Plutus.PAB.Events.Contract as C
import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse)
import qualified Plutus.PAB.Events.ContractInstanceState as C
import Plutus.PAB.Types (PABError (..))

import Playground.Schema (endpointsToSchemas)
import Playground.Types (FunctionSchema)
import Plutus.Contract (BlockchainActions, Contract)
import Plutus.Contract.Resumable (Response)
import Plutus.Contract.Schema (Event, Handlers, Input, Output)
import Plutus.Contract.State (ContractResponse (..))
import qualified Plutus.Contract.State as ContractState
import qualified Plutus.Trace.Emulator.Types as Emulator
import Schema (FormSchema)

-- | Contracts that are built into the PAB (ie. compiled with it) and receive
-- an initial value of type 'a'.
data Builtin a

type ContractConstraints w schema error =
( Monoid w
, Forall (Output schema) ToJSON
, Forall (Input schema) ToJSON
, Forall (Input schema) FromJSON
, ToJSON error
, ToJSON w
, AllUniqueLabels (Input schema)
)

-- | Plutus contract with all parameters existentially quantified. Can be any contract that satisfies the
-- 'ContractConstraints'.
data SomeBuiltin where
SomeBuiltin :: forall w schema error a. ContractConstraints w schema error => Contract w schema error a -> SomeBuiltin

data SomeBuiltinState a where
SomeBuiltinState ::
forall a w schema error b.
ContractConstraints w schema error
=> Emulator.ContractInstanceStateInternal w schema error b -- ^ Internal state
-> SomeBuiltinState a

instance PABContract (Builtin a) where
type ContractDef (Builtin a) = a
type State (Builtin a) = SomeBuiltinState a
serialisableState _ = getResponse

-- | Handle the 'ContractEffect' for a builtin contract type with parameter
-- @a@.
handleBuiltin ::
forall a effs.
Member (Error PABError) effs
=> (a -> [FunctionSchema FormSchema]) -- ^ The schema (construct with 'endpointsToSchemas'. Can also be an empty list)
-> (a -> SomeBuiltin) -- ^ The actual contract
-> ContractEffect (Builtin a)
~> Eff effs
handleBuiltin mkSchema initialise = \case
InitialState c -> case initialise c of SomeBuiltin c' -> initBuiltin c'
UpdateContract _ state p -> case state of SomeBuiltinState s -> updateBuiltin s p
ExportSchema a -> pure $ mkSchema a

getResponse :: forall a. SomeBuiltinState a -> PartiallyDecodedResponse ContractPABRequest
getResponse (SomeBuiltinState s) =
mkResponse
$ ContractState.mkResponse
$ Emulator.instContractState
$ Emulator.toInstanceState s

initBuiltin ::
forall effs a w schema error b.
ContractConstraints w schema error
=> Contract w schema error b
-> Eff effs (SomeBuiltinState a)
initBuiltin = pure . SomeBuiltinState . Emulator.emptyInstanceState

updateBuiltin ::
forall effs a w schema error b.
( ContractConstraints w schema error
, Member (Error PABError) effs
)
=> Emulator.ContractInstanceStateInternal w schema error b
-> Response C.ContractResponse
-> Eff effs (SomeBuiltinState a)
updateBuiltin oldState event = do
resp <- traverse toEvent event
let newState = Emulator.addEventInstanceState resp oldState
case newState of
Just k -> pure (SomeBuiltinState k)
_ -> throwError $ ContractCommandError 0 "failed to update contract"

toEvent ::
forall schema effs.
( Member (Error PABError) effs
, AllUniqueLabels (Input schema)
, Forall (Input schema) FromJSON
)
=> C.ContractResponse
-> Eff effs (Event schema)
toEvent = fromJSON . JSON.toJSON . C.ContractHandlersResponse

mkResponse ::
forall w schema err.
( Forall (Output schema) ToJSON
, Forall (Input schema) ToJSON
, ToJSON err
, ToJSON w
)
=> ContractResponse w err (Event schema) (Handlers schema)
-> PartiallyDecodedResponse ContractPABRequest
mkResponse ContractResponse{newState, hooks, logs, observableState, err} =
C.PartiallyDecodedResponse
{ C.newState = fmap JSON.toJSON newState
, C.hooks = fmap (fmap (encodeRequest @schema)) hooks
, C.logs = logs
, C.observableState = JSON.toJSON observableState
, C.err = fmap JSON.toJSON err
}

encodeRequest ::
forall schema.
( Forall (Output schema) ToJSON
)
=> Handlers schema
-> ContractPABRequest
encodeRequest = either error C.unContractHandlerRequest . JSON.eitherDecode . JSON.encode

fromJSON :: (Member (Error PABError) effs, FromJSON a) => Value -> Eff effs a
fromJSON =
either (throwError . OtherError . Text.pack) pure
. JSON.parseEither JSON.parseJSON
168 changes: 37 additions & 131 deletions plutus-pab/src/Plutus/PAB/Effects/Contract/ContractTest.hs
Expand Up @@ -22,162 +22,68 @@ module Plutus.PAB.Effects.Contract.ContractTest(
, handleContractTest
) where

import Control.Monad (void)
import Control.Monad.Freer
import Control.Monad.Freer.Error (Error, throwError)
import Data.Aeson (FromJSON, ToJSON, Value)
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import Control.Monad.Freer.Error (Error)
import Data.Aeson (FromJSON, ToJSON)
import Data.Bifunctor (Bifunctor (..))
import Data.Row
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Prettyprint.Doc
import GHC.Generics (Generic)

import Data.Text.Extras (tshow)
import Plutus.PAB.Effects.Contract (ContractEffect (..), PABContract (..))
import Plutus.PAB.Events.Contract (ContractPABRequest)
import qualified Plutus.PAB.Events.Contract as C
import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse)
import qualified Plutus.PAB.Events.ContractInstanceState as C
import Plutus.PAB.Monitoring.PABLogMsg (ContractEffectMsg (..))
import Plutus.PAB.Types (PABError (..))

import Control.Monad.Freer.Extras.Log (LogMsg, logDebug)

import Playground.Schema (endpointsToSchemas)
import Plutus.Contract (BlockchainActions, Contract, ContractError)
import Playground.Types (FunctionSchema)
import Plutus.Contract (BlockchainActions, ContractError)
import Plutus.Contract.Effects.RPC (RPCClient)
import Plutus.Contract.Resumable (Response)
import Plutus.Contract.Schema (Event, Handlers, Input, Output)
import Plutus.Contract.State (ContractRequest (..), ContractResponse (..))
import qualified Plutus.Contract.State as ContractState
import qualified Plutus.Contracts.Currency as Contracts.Currency
import qualified Plutus.Contracts.Game as Contracts.Game
import qualified Plutus.Contracts.RPC as Contracts.RPC
import Plutus.PAB.Effects.Contract (ContractEffect (..))
import Plutus.PAB.Effects.Contract.Builtin (Builtin, SomeBuiltin (..))
import qualified Plutus.PAB.Effects.Contract.Builtin as Builtin
import qualified Plutus.PAB.Effects.ContractTest.AtomicSwap as Contracts.AtomicSwap
import qualified Plutus.PAB.Effects.ContractTest.PayToWallet as Contracts.PayToWallet
import Plutus.PAB.Types (PABError (..))
import Schema (FormSchema)

data TestContracts = Game | Currency | AtomicSwap | PayToWallet | RPCClient | RPCServer
deriving (Eq, Ord, Show, Generic)
deriving anyclass (FromJSON, ToJSON)

instance PABContract TestContracts where
type ContractDef TestContracts = TestContracts
type State TestContracts = PartiallyDecodedResponse ContractPABRequest
serialisableState _ = id

instance Pretty TestContracts where
pretty = viaShow

-- | A mock/test handler for 'ContractEffect'
-- | A mock/test handler for 'ContractEffect'. Uses 'Plutus.PAB.Effects.Contract.Builtin'.
handleContractTest ::
( Member (Error PABError) effs
, Member (LogMsg ContractEffectMsg) effs
)
=> ContractEffect TestContracts
=> ContractEffect (Builtin TestContracts)
~> Eff effs
handleContractTest = \case
InitialState c -> case c of
Game -> doContractInit game
Currency -> doContractInit currency
AtomicSwap -> doContractInit swp
PayToWallet -> doContractInit payToWallet
RPCClient -> doContractInit rpcClient
RPCServer -> doContractInit rpcServer
UpdateContract c state p -> case c of
Game -> doContractUpdate game state p
Currency -> doContractUpdate currency state p
AtomicSwap -> doContractUpdate swp state p
PayToWallet -> doContractUpdate payToWallet state p
RPCClient -> doContractUpdate rpcClient state p
RPCServer -> doContractUpdate rpcServer state p
ExportSchema t -> case t of
Game -> pure $ endpointsToSchemas @(Contracts.Game.GameSchema .\\ BlockchainActions)
Currency -> pure $ endpointsToSchemas @(Contracts.Currency.CurrencySchema .\\ BlockchainActions)
AtomicSwap -> pure $ endpointsToSchemas @(Contracts.AtomicSwap.AtomicSwapSchema .\\ BlockchainActions)
PayToWallet -> pure $ endpointsToSchemas @(Contracts.PayToWallet.PayToWalletSchema .\\ BlockchainActions)
RPCClient -> pure adderSchema
RPCServer -> pure adderSchema
handleContractTest = Builtin.handleBuiltin getSchema getContract

getSchema :: TestContracts -> [FunctionSchema FormSchema]
getSchema = \case
Game -> Builtin.endpointsToSchemas @(Contracts.Game.GameSchema .\\ BlockchainActions)
Currency -> Builtin.endpointsToSchemas @(Contracts.Currency.CurrencySchema .\\ BlockchainActions)
AtomicSwap -> Builtin.endpointsToSchemas @(Contracts.AtomicSwap.AtomicSwapSchema .\\ BlockchainActions)
PayToWallet -> Builtin.endpointsToSchemas @(Contracts.PayToWallet.PayToWalletSchema .\\ BlockchainActions)
RPCClient -> adderSchema
RPCServer -> adderSchema
where
game = first tshow $ Contracts.Game.game @ContractError
currency = first tshow $ void Contracts.Currency.forgeCurrency
adderSchema = Builtin.endpointsToSchemas @(Contracts.RPC.AdderSchema .\\ (BlockchainActions .\/ RPCClient Contracts.RPC.Adder))

getContract :: TestContracts -> SomeBuiltin
getContract = \case
Game -> SomeBuiltin game
Currency -> SomeBuiltin currency
AtomicSwap -> SomeBuiltin swp
PayToWallet -> SomeBuiltin payToWallet
RPCClient -> SomeBuiltin rpcClient
RPCServer -> SomeBuiltin rpcServer
where
game = Contracts.Game.game @ContractError
currency = Contracts.Currency.forgeCurrency
swp = first tshow Contracts.AtomicSwap.atomicSwap
payToWallet = first tshow Contracts.PayToWallet.payToWallet
rpcClient = first tshow $ void Contracts.RPC.callAdder
rpcServer = first tshow $ void Contracts.RPC.respondAdder
adderSchema = endpointsToSchemas @(Contracts.RPC.AdderSchema .\\ (BlockchainActions .\/ RPCClient Contracts.RPC.Adder))

doContractInit ::
forall w schema effs.
( Member (Error PABError) effs
, Forall (Output schema) ToJSON
, Forall (Input schema) ToJSON
, Monoid w
, ToJSON w
)
=> Contract w schema Text ()
-> Eff effs (PartiallyDecodedResponse ContractPABRequest)
doContractInit contract = either throwError pure $ do
let value = ContractState.initialiseContract contract
fromString $ fmap (fmap C.unContractHandlerRequest) $ JSON.eitherDecode $ JSON.encode value

doContractUpdate ::
forall w schema effs.
( Member (Error PABError) effs
, AllUniqueLabels (Input schema)
, Forall (Input schema) FromJSON
, Forall (Input schema) ToJSON
, Forall (Output schema) ToJSON
, Member (LogMsg ContractEffectMsg) effs
, Monoid w
, ToJSON w
)
=> Contract w schema Text ()
-> PartiallyDecodedResponse ContractPABRequest
-> Response C.ContractResponse
-> Eff effs (PartiallyDecodedResponse ContractPABRequest)
doContractUpdate contract oldState response = do
let C.PartiallyDecodedResponse{C.newState} = oldState
oldState' <- traverse fromJSON newState
typedResp <- traverse (fromJSON . JSON.toJSON . C.ContractHandlersResponse) response
let conReq = ContractRequest{oldState = oldState', event = typedResp }
logDebug $ SendContractRequest (fmap JSON.toJSON conReq)
let response' = mkResponse $ ContractState.insertAndUpdateContract contract conReq
logDebug $ ReceiveContractResponse response'
pure response'

mkResponse ::
forall w schema err.
( Forall (Output schema) ToJSON
, Forall (Input schema) ToJSON
, ToJSON err
, ToJSON w
)
=> ContractResponse w err (Event schema) (Handlers schema)
-> PartiallyDecodedResponse ContractPABRequest
mkResponse ContractResponse{newState, hooks, logs, observableState, err} =
C.PartiallyDecodedResponse
{ C.newState = fmap JSON.toJSON newState
, C.hooks = fmap (fmap (encodeRequest @schema)) hooks
, C.logs = logs
, C.observableState = JSON.toJSON observableState
, C.err = fmap JSON.toJSON err
}

encodeRequest ::
forall schema.
( Forall (Output schema) ToJSON
)
=> Handlers schema
-> ContractPABRequest
encodeRequest = either error C.unContractHandlerRequest . JSON.eitherDecode . JSON.encode

fromJSON :: (Member (Error PABError) effs, FromJSON a) => Value -> Eff effs a
fromJSON =
either (throwError . OtherError . Text.pack) pure
. JSON.parseEither JSON.parseJSON
payToWallet =Contracts.PayToWallet.payToWallet
rpcClient = Contracts.RPC.callAdder
rpcServer = Contracts.RPC.respondAdder

fromString :: Either String a -> Either PABError a
fromString = first (ContractCommandError 0 . Text.pack)

0 comments on commit ad3e844

Please sign in to comment.