diff --git a/marlowe-runtime-web/marlowe-runtime-web.cabal b/marlowe-runtime-web/marlowe-runtime-web.cabal index 8c9afd1216..c2cfa4b8cd 100644 --- a/marlowe-runtime-web/marlowe-runtime-web.cabal +++ b/marlowe-runtime-web/marlowe-runtime-web.cabal @@ -92,6 +92,7 @@ library server Language.Marlowe.Runtime.Web.Server.REST Language.Marlowe.Runtime.Web.Server.REST.ApiError Language.Marlowe.Runtime.Web.Server.REST.Contracts + Language.Marlowe.Runtime.Web.Server.REST.Contracts.Possibilities Language.Marlowe.Runtime.Web.Server.REST.Transactions Language.Marlowe.Runtime.Web.Server.REST.Withdrawals Language.Marlowe.Runtime.Web.Server.SyncClient diff --git a/marlowe-runtime-web/server/Language/Marlowe/Runtime/Web/Server/REST/Contracts/Possibilities.hs b/marlowe-runtime-web/server/Language/Marlowe/Runtime/Web/Server/REST/Contracts/Possibilities.hs new file mode 100644 index 0000000000..8680a4fe82 --- /dev/null +++ b/marlowe-runtime-web/server/Language/Marlowe/Runtime/Web/Server/REST/Contracts/Possibilities.hs @@ -0,0 +1,50 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE GADTs #-} + +-- | This module defines a server for the /contracts REST API. + +module Language.Marlowe.Runtime.Web.Server.REST.Contracts.Possibilities + ( get + ) where + +import Cardano.Api (BabbageEra, TxBody, getTxBody, makeSignedTransaction) +import qualified Cardano.Api as Cardano +import Cardano.Ledger.Alonzo.TxWitness (TxWitness(TxWitness)) +import Control.Monad (unless) +import Data.Aeson (Value(Null)) +import Data.List.NonEmpty as NEL +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) +import qualified Data.Set as Set +import Data.Text (Text) +import Language.Marlowe (Environment) +import Language.Marlowe.Core.V1.Semantics.Possibilities (Possibilities, possibilities) +import Language.Marlowe.Protocol.Query.Types (ContractFilter(..), Page(..), SomeContractState(SomeContractState)) +import Language.Marlowe.Runtime.ChainSync.Api (Lovelace(..)) +import Language.Marlowe.Runtime.Core.Api + (ContractId, MarloweMetadataTag(..), MarloweTransactionMetadata(..), MarloweVersion(..), SomeMarloweVersion(..)) +import Language.Marlowe.Runtime.Transaction.Api (ContractCreated(..), WalletAddresses(..)) +import qualified Language.Marlowe.Runtime.Transaction.Api as Tx +import Language.Marlowe.Runtime.Web hiding (Unsigned) +import Language.Marlowe.Runtime.Web.Server.DTO +import Language.Marlowe.Runtime.Web.Server.Monad + (AppM, createContract, loadContract, loadContractHeaders, submitContract) +import Language.Marlowe.Runtime.Web.Server.REST.ApiError + (ApiError(ApiError), badRequest', notFound', rangeNotSatisfiable', throwDTOError) +import qualified Language.Marlowe.Runtime.Web.Server.REST.ApiError as ApiError +import qualified Language.Marlowe.Runtime.Web.Server.REST.Transactions as Transactions +import Language.Marlowe.Runtime.Web.Server.TxClient (TempTx(TempTx), TempTxStatus(Unsigned)) +import Language.Marlowe.Runtime.Web.Server.Util (makeSignedTxWithWitnessKeys) +import Servant +import Servant.Pagination + +get :: Environment -> TxOutRef -> AppM [Possibilities] +get environment contractId = do + contractId' <- fromDTOThrow (badRequest' "Invalid contract id value") contractId + loadContract contractId' >>= \case + Nothing -> throwError $ notFound' "Contract not found" + Just eitherTmpContractStateorContractState -> do + let ContractState {state,initialContract,currentContract} = either toDTO toDTO eitherTmpContractStateorContractState + pure $ NEL.toList $ fromMaybe [] $ possibilities environment state (fromMaybe initialContract currentContract) + + diff --git a/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/API.hs b/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/API.hs index 0001271a2c..76b3fd3f06 100644 --- a/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/API.hs +++ b/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/API.hs @@ -51,6 +51,7 @@ import GHC.Exts (IsList(..)) import GHC.Generics (Generic) import GHC.Show (showSpace) import GHC.TypeLits (KnownSymbol, symbolVal) +import Language.Marlowe.Core.V1.Semantics.Possibilities (Possibilities) import Language.Marlowe.Runtime.Web.Types import Network.HTTP.Media ((//)) import Servant @@ -122,7 +123,8 @@ type PostContractsAPI -- | /contracts/:contractId sup-API type ContractAPI = GetContractAPI :<|> PutSignedTxAPI - :<|> "transactions" :> TransactionsAPI + :<|> "possibilities" :> GETPossibilitiesAPI + :<|> "transactions" :> TransactionsAPI -- | GET /contracts/:contractId sub-API type GetContractAPI = Get '[JSON] GetContractResponse @@ -134,6 +136,9 @@ instance HasNamedLink ContractState API "transactions" where "contracts" :> Capture "contractId" TxOutRef :> "transactions" :> GetTransactionsAPI namedLink _ _ mkLink ContractState{..} = guard (status == Confirmed) $> mkLink contractId +-- | GET /contracts/:contractId/possibilities sub-API +type GETPossibilitiesAPI = Get '[JSON] [Possibilities] + -- | /contracts/:contractId/transactions sup-API type TransactionsAPI = GetTransactionsAPI :<|> PostTransactionsAPI diff --git a/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/Client.hs b/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/Client.hs index 160b8a3af5..6fa1246d65 100644 --- a/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/Client.hs +++ b/marlowe-runtime-web/src/Language/Marlowe/Runtime/Web/Client.hs @@ -187,7 +187,7 @@ getTransactions getTransactions contractId range = do let contractsClient :<|> _ = client let _ :<|> _ :<|> contractApi = contractsClient - let _ :<|> _ :<|> getTransactions' :<|> _ = contractApi contractId + let _ :<|> _ :<|> _ :<|> getTransactions' :<|> _ = contractApi contractId response <- getTransactions' $ putRange <$> range totalCount <- reqHeaderValue $ lookupResponseHeader @"Total-Count" response nextRanges <- headerValue $ lookupResponseHeader @"Next-Range" response @@ -208,7 +208,7 @@ postTransaction postTransaction changeAddress otherAddresses collateralUtxos contractId request = do let contractsClient :<|> _ = client let _ :<|> _ :<|> contractApi = contractsClient - let _ :<|> _ :<|> _ :<|> (postTransaction' :<|> _) :<|> _ = contractApi contractId + let _ :<|> _ :<|> _ :<|> _ :<|> (postTransaction' :<|> _) :<|> _ = contractApi contractId response <- postTransaction' request changeAddress @@ -225,7 +225,7 @@ postTransactionCreateTx -> ClientM (ApplyInputsTxEnvelope CardanoTx) postTransactionCreateTx changeAddress otherAddresses collateralUtxos contractId request = do let (_ :<|> _ :<|> contractApi) :<|> _ = client - let _ :<|> _ :<|> _ :<|> (_ :<|> postTransactionCreateTx') :<|> _ = contractApi contractId + let _ :<|> _ :<|> _ :<|> _ :<|> (_ :<|> postTransactionCreateTx') :<|> _ = contractApi contractId response <- postTransactionCreateTx' request changeAddress @@ -238,7 +238,7 @@ getTransaction :: TxOutRef -> TxId -> ClientM Tx getTransaction contractId transactionId = do let contractsClient :<|> _ = client let _ :<|> _ :<|> contractApi = contractsClient - let _ :<|> _ :<|> _ :<|> _ :<|> transactionApi = contractApi contractId + let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> transactionApi = contractApi contractId let getTransaction' :<|> _ = transactionApi transactionId retractLink . retractLink <$> getTransaction' @@ -246,7 +246,7 @@ putTransaction :: TxOutRef -> TxId -> TextEnvelope -> ClientM () putTransaction contractId transactionId tx = do let contractsClient :<|> _ = client let _ :<|> _ :<|> contractApi = contractsClient - let _ :<|> _ :<|> _ :<|> _ :<|> transactionApi = contractApi contractId + let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> transactionApi = contractApi contractId let _ :<|> putTransaction' = transactionApi transactionId void $ putTransaction' tx diff --git a/marlowe/marlowe-cardano.cabal b/marlowe/marlowe-cardano.cabal index 994261c732..9d17ea9a19 100644 --- a/marlowe/marlowe-cardano.cabal +++ b/marlowe/marlowe-cardano.cabal @@ -96,7 +96,7 @@ library Language.Marlowe.Core.V1.Plate Language.Marlowe.Core.V1.Semantics Language.Marlowe.Core.V1.Semantics.Types - Language.Marlowe.Core.V1.Semantics.Possibilities + Language.Marlowe.Core.V1.Semantics.Next Language.Marlowe.Core.V1.Semantics.Types.Address Language.Marlowe.Extended.V1 Language.Marlowe.Extended.V1.Metadata.Types diff --git a/marlowe/src/Language/Marlowe/Core/V1/Semantics/Next.hs b/marlowe/src/Language/Marlowe/Core/V1/Semantics/Next.hs new file mode 100644 index 0000000000..6ca6c8a54e --- /dev/null +++ b/marlowe/src/Language/Marlowe/Core/V1/Semantics/Next.hs @@ -0,0 +1,150 @@ +----------------------------------------------------------------------------- +-- +-- Module : $Headers +-- License : Apache 2.0 +-- +-- Stability : Experimental +-- Portability : Portable +-- +-- | Types for Marlowe semantics +-- +----------------------------------------------------------------------------- + + +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DataKinds #-} + +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} + +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} + +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} + + +module Language.Marlowe.Core.V1.Semantics.Next + ( Next(..) + , NextActionPossibility(..) + , next + ) where + +import Control.Applicative ((<*>), (<|>)) +import qualified Data.Aeson as JSON +import Data.Aeson.Types hiding (Error, Value) +import Data.ByteString.Base16.Aeson (EncodeBase16(..)) +import Data.List.NonEmpty as NEL +import Data.Text (pack) +import Deriving.Aeson +import Language.Marlowe.Core.V1.Semantics +import Language.Marlowe.Core.V1.Semantics.Types +import Language.Marlowe.Pretty (Pretty(..)) +import PlutusTx.Prelude hiding (encodeUtf8, mapM, (<$>), (<*>), (<>)) +import Prelude ((<$>)) +import qualified Prelude as Haskell + +-- | Description on What can be done next +data Next + = WaitingForInput (NonEmpty NextAction) + | ContractClosed + | SuspendedOnATimeOut + deriving stock (Haskell.Show,Generic,Haskell.Eq,Haskell.Ord) + deriving anyclass (Pretty) + +data NextContinuation + = NextContract Contract + | NextMerkleizedContract BuiltinByteString + deriving stock (Haskell.Show,Generic,Haskell.Eq,Haskell.Ord) + deriving anyclass (Pretty) + +instance FromJSON NextContinuation where + parseJSON (Object v) + = NextContract <$> v .: "contract" + <|> NextMerkleizedContract <$> do + mt <- v .: "merkleized_contract_hash" + EncodeBase16 bs <- parseJSON mt + return $ toBuiltin bs + parseJSON _ = Haskell.fail "NextAction must be an object with 2 fields \"action_possibility\" and \"continuation\"" + +data NextAction = NextAction NextActionPossibility NextContinuation + deriving stock (Haskell.Show,Generic,Haskell.Eq,Haskell.Ord) + deriving anyclass (Pretty) + +instance FromJSON NextAction where + parseJSON (Object v) = NextAction <$> v .: "action_possibility" <*> v .: "continuation" + parseJSON _ = Haskell.fail "NextAction must be an object with 2 fields \"action_possibility\" and \"continuation\"" + +-- | an Action that can be done out of several possible alternatives. +data NextActionPossibility + = CanDeposit Party AccountId Token Integer + | CanChoose ChoiceId [Bound] + | CanNotify + | CanAdvance + deriving stock (Haskell.Show,Haskell.Eq,Haskell.Ord,Generic) + deriving anyclass (Pretty) + + +instance FromJSON NextActionPossibility where + parseJSON (String "can_notify") = return CanNotify + parseJSON (String "can_advance") = return CanAdvance + parseJSON (Object v) = + CanChoose + <$> v .: "can_choose_id" + <*> v .: "within_bounds" + <|> CanDeposit + <$> v .: "party" + <*> v .: "into_account" + <*> v .: "of_token" + <*> v .: "can_deposit" + parseJSON _ = Haskell.fail "Action Possibility must be either an object or the string \"can_notify\" or \"can_advance\"" + +instance ToJSON NextActionPossibility where + toJSON CanNotify = JSON.String $ pack "can_notify" + toJSON CanAdvance = JSON.String $ pack "can_advance" + toJSON (CanDeposit party accountId token quantity) = object + [ "party" .= party + , "can_deposit" .= quantity + , "of_token" .= token + , "into_account" .= accountId + ] + toJSON (CanChoose choiceId bounds) = object + [ "for_choice" .= choiceId + , "can_choose_between" .= bounds + ] + + +next :: Environment -> State -> Contract -> Next +next _ _ Close = ContractClosed +next environment _ (When _ timeout continuation) | isTimedOut timeout environment = WaitingForInput $ (NextAction CanAdvance $ NextContract continuation):| [] +next _ _ (When [] _ _) = SuspendedOnATimeOut +next environment state (When (x:xs) _ _) = WaitingForInput . caseContractsToNextAction environment state $ x :| xs +next _ _ (Pay _ _ _ _ continuation) = WaitingForInput $ (NextAction CanAdvance $ NextContract continuation):| [] +next _ _ (If _ _ continuation) = WaitingForInput $ (NextAction CanAdvance $ NextContract continuation):| [] +next _ _ (Let _ _ continuation) = WaitingForInput $ (NextAction CanAdvance $ NextContract continuation):| [] +next _ _ (Assert _ continuation) = WaitingForInput $ (NextAction CanAdvance $ NextContract continuation):| [] + +caseContractsToNextAction :: Environment -> State -> NonEmpty (Case Contract) -> NonEmpty NextAction +caseContractsToNextAction environment state + = (caseContractToNextAction environment state <$>) + +caseContractToNextAction :: Environment -> State -> Case Contract -> NextAction +caseContractToNextAction environment state + = \case + (Case action contract) -> NextAction (actionToNextPossibilities environment state action)(NextContract contract) + (MerkleizedCase action hash) -> NextAction (actionToNextPossibilities environment state action)(NextMerkleizedContract hash) + +actionToNextPossibilities :: Environment -> State -> Action -> NextActionPossibility +actionToNextPossibilities environment state = \case + (Deposit accountId party token value) + -> CanDeposit + accountId + party + token + (evalValue environment state value) + (Choice choiceId bounds) -> CanChoose choiceId bounds + (Notify _) -> CanNotify diff --git a/marlowe/src/Language/Marlowe/Core/V1/Semantics/Possibilities.hs b/marlowe/src/Language/Marlowe/Core/V1/Semantics/Possibilities.hs deleted file mode 100644 index 8786aaaa55..0000000000 --- a/marlowe/src/Language/Marlowe/Core/V1/Semantics/Possibilities.hs +++ /dev/null @@ -1,143 +0,0 @@ ------------------------------------------------------------------------------ --- --- Module : $Headers --- License : Apache 2.0 --- --- Stability : Experimental --- Portability : Portable --- --- | Types for Marlowe semantics --- ------------------------------------------------------------------------------ - - -{-# LANGUAGE BlockArguments #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DefaultSignatures #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TemplateHaskell #-} - -{-# OPTIONS_GHC -fno-specialise #-} -- A big hammer, but it helps. -{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -{-# OPTIONS_GHC -fno-omit-interface-pragmas #-} - -{-# OPTIONS_GHC -Wno-name-shadowing #-} -{-# OPTIONS_GHC -Wno-simplifiable-class-constraints #-} -{-# OPTIONS_GHC -Wno-unused-imports #-} - -module Language.Marlowe.Core.V1.Semantics.NextInputPossibilities - ( Possibilities(..) - , possibilities - ) where - -import Control.Applicative ((<*>), (<|>)) -import Control.Newtype.Generics (Newtype) -import qualified Data.Aeson as A -import qualified Data.Aeson as JSON -import qualified Data.Aeson.KeyMap as KeyMap -import Data.Aeson.Types hiding (Error, Value) -import qualified Data.Aeson.Types as JSON -import Data.ByteString.Base16.Aeson (EncodeBase16(EncodeBase16)) -import qualified Data.ByteString.Base16.Aeson as Base16.Aeson -import qualified Data.Foldable as F -import Data.Scientific (floatingOrInteger, scientific) -import Data.String (IsString(..)) -import Data.Text (pack) -import Data.Text.Encoding as Text (decodeUtf8, encodeUtf8) -import Deriving.Aeson -import Language.Marlowe.Core.V1.Semantics.Types.Address -import Language.Marlowe.ParserUtil (getInteger, withInteger) -import Language.Marlowe.Pretty (Pretty(..)) -import qualified Plutus.V1.Ledger.Value as Val -import Plutus.V2.Ledger.Api (CurrencySymbol(unCurrencySymbol), POSIXTime(..), TokenName(unTokenName)) -import qualified Plutus.V2.Ledger.Api as Ledger (Address(..)) -import PlutusTx (makeIsDataIndexed) -import PlutusTx.AssocMap (Map) -import qualified PlutusTx.AssocMap as Map -import PlutusTx.Lift (makeLift) -import PlutusTx.Prelude hiding (encodeUtf8, mapM, (<$>), (<*>), (<>)) -import Prelude (mapM, (<$>)) -import qualified Prelude as Haskell -import Text.PrettyPrint.Leijen (parens, text) - -import Language.Marlowe.Core.V1.Semantics -import Language.Marlowe.Core.V1.Semantics.Types - --- | Possibilities describe the possible next inputs to be applied for a Marlowe Contract in execution . -data Possibilities - = CanDeposit AccountId Party Token Integer - | CanChoose ChoiceId [Bound] - | CanNotify - | CanAdvance - deriving stock (Haskell.Show,Haskell.Eq,Generic) - deriving anyclass (Pretty) - -instance FromJSON Possibilities where - parseJSON (String "can_notify") = return CanNotify - parseJSON (String "can_advance") = return CanAdvance - parseJSON (Object v) = - CanChoose <$> v .: "can_choose_id" <*> v .: "within_bounds" - <|> CanDeposit <$> v .: "can_deposit_into_account" - <*> v .: "from_party" - <*> v .: "of_token" - <*> v .: "a_quantity_of" - parseJSON _ = Haskell.fail "Next Input Possibilities must be either an object or the string \"can_notify\" or \"can_advance\"" - -instance ToJSON Possibilities where - toJSON CanNotify = JSON.String $ pack "can_notify" - toJSON CanAdvance = JSON.String $ pack "can_advance" - toJSON (CanDeposit accountId party token quantity) = object - [ "can_deposit_into_account" .= accountId - , "from_party" .= party - , "of_token" .= token - , "a_quantity_of" .= quantity - ] - toJSON (CanChoose choiceId bounds) = object - [ "can_choose_id" .= choiceId - , "within_bounds" .= bounds - ] - - --- | Possibilities for a contract -possibilities :: Environment -> State -> Contract -> [Possibilities] -possibilities a b = - (\case - (ContractQuiescent _ _ _ _ quiescentContract ) -> possibilities' a b quiescentContract - RRAmbiguousTimeIntervalError -> [] - ) . reduceContractUntilQuiescent a b - - where - -- | Possibilities for a quiescent contract - possibilities' :: Environment -> State -> Contract -> [Possibilities] - possibilities' environment _ (When _ timeout _) | isTimedOut timeout environment = [CanAdvance] - possibilities' environment state (When caseContracts _ _) = - actionToPossibilities environment state - . (\case - (Case action _) -> action - (MerkleizedCase action _) -> action ) - <$> caseContracts - possibilities' _ _ _ = [] -- N.B Side effect of not having a typed concept for a Quiescent Contract - - actionToPossibilities :: Environment -> State -> Action -> Possibilities - actionToPossibilities environment state = \case - (Deposit accountId party token value) - -> CanDeposit - accountId - party - token - (evalValue environment state value) - (Choice choiceId bounds) -> CanChoose choiceId bounds - (Notify _) -> CanNotify diff --git a/marlowe/src/Language/Marlowe/Core/V1/Semantics/Types.hs b/marlowe/src/Language/Marlowe/Core/V1/Semantics/Types.hs index 1ba279a4dc..4f1aa363e8 100644 --- a/marlowe/src/Language/Marlowe/Core/V1/Semantics/Types.hs +++ b/marlowe/src/Language/Marlowe/Core/V1/Semantics/Types.hs @@ -309,7 +309,7 @@ newtype Environment = Environment { timeInterval :: TimeInterval } deriving stock (Haskell.Show,Haskell.Eq,Haskell.Ord) isTimedOut :: POSIXTime -> Environment -> Bool -isTimedOut t Environment {timeInterval = (a,b)} | t > b = True +isTimedOut t Environment {timeInterval = (_,b)} | t > b = True isTimedOut _ _ = False instance FromJSON Environment where diff --git a/marlowe/src/Language/Marlowe/Pretty.hs b/marlowe/src/Language/Marlowe/Pretty.hs index 0de7cd3f53..c53a990ad1 100644 --- a/marlowe/src/Language/Marlowe/Pretty.hs +++ b/marlowe/src/Language/Marlowe/Pretty.hs @@ -9,6 +9,7 @@ module Language.Marlowe.Pretty where import qualified Data.ByteString as BS +import qualified Data.List.NonEmpty as NEL import Data.Text (Text) import qualified Data.Text as Text import GHC.Generics (C, Constructor, D, Generic, K1(K1), M1(M1), Rep, S, U1, conName, from, (:*:)((:*:)), (:+:)(L1, R1)) @@ -94,6 +95,8 @@ instance (Pretty a, Pretty b) => Pretty (a, b) where instance (Pretty a) => Pretty [a] where prettyFragment a = encloseSep lbracket rbracket comma (map prettyFragment a) +instance (Pretty a) => Pretty (NEL.NonEmpty a) where + prettyFragment = prettyFragment . NEL.toList {- Currently, Marlowe Playground saves a Haskell contract to a file,