diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix index 8a52955b34..17ea8ff35f 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-contract.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-contract"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -154,6 +154,7 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) @@ -189,6 +190,7 @@ "Spec/State" "Spec/ThreadToken" "Spec/Secrets" + "Spec/Plutus/Contract/Wallet" ]; hsSourceDirs = [ "test" ]; mainPath = [ "Spec.hs" ]; diff --git a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-pab.nix b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-pab.nix index 62f680dc48..e8bdd8d560 100644 --- a/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-pab.nix +++ b/nix/pkgs/haskell/materialized-darwin/.plan.nix/plutus-pab.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-pab"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -123,7 +123,9 @@ "Cardano/Node/Types" "Cardano/Protocol/Socket/Mock/Client" "Cardano/Protocol/Socket/Mock/Server" - "Cardano/Wallet/Client" + "Cardano/Wallet/LocalClient" + "Cardano/Wallet/RemoteClient" + "Cardano/Wallet/Types" "Cardano/Wallet/Mock/API" "Cardano/Wallet/Mock/Client" "Cardano/Wallet/Mock/Handlers" @@ -424,7 +426,9 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) + (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) @@ -453,6 +457,7 @@ buildable = true; modules = [ "Cardano/Api/NetworkId/ExtraSpec" + "Cardano/Wallet/RemoteClientSpec" "Cardano/Wallet/ServerSpec" "Control/Concurrent/STM/ExtrasSpec" ]; diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix index 8a52955b34..17ea8ff35f 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-contract.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-contract"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -154,6 +154,7 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) @@ -189,6 +190,7 @@ "Spec/State" "Spec/ThreadToken" "Spec/Secrets" + "Spec/Plutus/Contract/Wallet" ]; hsSourceDirs = [ "test" ]; mainPath = [ "Spec.hs" ]; diff --git a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-pab.nix b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-pab.nix index 62f680dc48..e8bdd8d560 100644 --- a/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-pab.nix +++ b/nix/pkgs/haskell/materialized-linux/.plan.nix/plutus-pab.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-pab"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -123,7 +123,9 @@ "Cardano/Node/Types" "Cardano/Protocol/Socket/Mock/Client" "Cardano/Protocol/Socket/Mock/Server" - "Cardano/Wallet/Client" + "Cardano/Wallet/LocalClient" + "Cardano/Wallet/RemoteClient" + "Cardano/Wallet/Types" "Cardano/Wallet/Mock/API" "Cardano/Wallet/Mock/Client" "Cardano/Wallet/Mock/Handlers" @@ -424,7 +426,9 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) + (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) @@ -453,6 +457,7 @@ buildable = true; modules = [ "Cardano/Api/NetworkId/ExtraSpec" + "Cardano/Wallet/RemoteClientSpec" "Cardano/Wallet/ServerSpec" "Control/Concurrent/STM/ExtrasSpec" ]; diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix index 8a52955b34..17ea8ff35f 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-contract.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-contract"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -154,6 +154,7 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) @@ -189,6 +190,7 @@ "Spec/State" "Spec/ThreadToken" "Spec/Secrets" + "Spec/Plutus/Contract/Wallet" ]; hsSourceDirs = [ "test" ]; mainPath = [ "Spec.hs" ]; diff --git a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-pab.nix b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-pab.nix index 62f680dc48..e8bdd8d560 100644 --- a/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-pab.nix +++ b/nix/pkgs/haskell/materialized-windows/.plan.nix/plutus-pab.nix @@ -10,7 +10,7 @@ { flags = { defer-plugin-errors = false; }; package = { - specVersion = "2.2"; + specVersion = "3.0"; identifier = { name = "plutus-pab"; version = "0.1.0.0"; }; license = "Apache-2.0"; copyright = ""; @@ -123,7 +123,9 @@ "Cardano/Node/Types" "Cardano/Protocol/Socket/Mock/Client" "Cardano/Protocol/Socket/Mock/Server" - "Cardano/Wallet/Client" + "Cardano/Wallet/LocalClient" + "Cardano/Wallet/RemoteClient" + "Cardano/Wallet/Types" "Cardano/Wallet/Mock/API" "Cardano/Wallet/Mock/Client" "Cardano/Wallet/Mock/Handlers" @@ -424,7 +426,9 @@ (hsPkgs."base" or (errorHandler.buildDepError "base")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) (hsPkgs."cardano-api" or (errorHandler.buildDepError "cardano-api")) + (hsPkgs."cardano-api".components.sublibs.gen or (errorHandler.buildDepError "cardano-api:gen")) (hsPkgs."containers" or (errorHandler.buildDepError "containers")) + (hsPkgs."data-default" or (errorHandler.buildDepError "data-default")) (hsPkgs."freer-extras" or (errorHandler.buildDepError "freer-extras")) (hsPkgs."freer-simple" or (errorHandler.buildDepError "freer-simple")) (hsPkgs."hedgehog" or (errorHandler.buildDepError "hedgehog")) @@ -453,6 +457,7 @@ buildable = true; modules = [ "Cardano/Api/NetworkId/ExtraSpec" + "Cardano/Wallet/RemoteClientSpec" "Cardano/Wallet/ServerSpec" "Control/Concurrent/STM/ExtrasSpec" ]; diff --git a/plutus-contract/plutus-contract.cabal b/plutus-contract/plutus-contract.cabal index cb8e155b8d..10c1a13276 100644 --- a/plutus-contract/plutus-contract.cabal +++ b/plutus-contract/plutus-contract.cabal @@ -1,4 +1,4 @@ -cabal-version: 2.2 +cabal-version: 3.0 name: plutus-contract version: 0.1.0.0 license: Apache-2.0 @@ -181,10 +181,12 @@ test-suite plutus-contract-test Spec.State Spec.ThreadToken Spec.Secrets + Spec.Plutus.Contract.Wallet build-depends: base >=4.9 && <5, bytestring -any, cardano-api -any, + cardano-api:gen -any, containers -any, data-default -any, freer-extras -any, diff --git a/plutus-contract/src/Plutus/Contract.hs b/plutus-contract/src/Plutus/Contract.hs index 5c19c2a9c4..b768988a13 100644 --- a/plutus-contract/src/Plutus/Contract.hs +++ b/plutus-contract/src/Plutus/Contract.hs @@ -80,6 +80,7 @@ module Plutus.Contract( , Request.submitBalancedTx , Request.balanceTx , Request.mkTxConstraints + , Request.yieldUnbalancedTx -- ** Creating transactions , module Tx -- ** Tx confirmation diff --git a/plutus-contract/src/Plutus/Contract/Effects.hs b/plutus-contract/src/Plutus/Contract/Effects.hs index 6fdcf8b833..6c2454f3f1 100644 --- a/plutus-contract/src/Plutus/Contract/Effects.hs +++ b/plutus-contract/src/Plutus/Contract/Effects.hs @@ -23,6 +23,7 @@ module Plutus.Contract.Effects( -- TODO: Move to Requests.Internal _WriteBalancedTxReq, _ExposeEndpointReq, _PosixTimeRangeToContainedSlotRangeReq, + _YieldUnbalancedTxReq, -- ** Chain index query effect types _DatumFromHash, _ValidatorFromHash, @@ -52,6 +53,7 @@ module Plutus.Contract.Effects( -- TODO: Move to Requests.Internal _WriteBalancedTxResp, _ExposeEndpointResp, _PosixTimeRangeToContainedSlotRangeResp, + _YieldUnbalancedTxResp, -- ** Chain index response effect types _DatumHashResponse, _ValidatorHashResponse, @@ -113,6 +115,7 @@ data PABReq = | WriteBalancedTxReq CardanoTx | ExposeEndpointReq ActiveEndpoint | PosixTimeRangeToContainedSlotRangeReq POSIXTimeRange + | YieldUnbalancedTxReq UnbalancedTx deriving stock (Eq, Show, Generic) deriving anyclass (ToJSON, FromJSON, OpenApi.ToSchema) @@ -133,6 +136,7 @@ instance Pretty PABReq where WriteBalancedTxReq tx -> "Write balanced tx:" <+> pretty tx ExposeEndpointReq ep -> "Expose endpoint:" <+> pretty ep PosixTimeRangeToContainedSlotRangeReq r -> "Posix time range to contained slot range:" <+> pretty r + YieldUnbalancedTxReq utx -> "Yield unbalanced tx:" <+> pretty utx -- | Responses that 'Contract's receive data PABResp = @@ -151,6 +155,7 @@ data PABResp = | WriteBalancedTxResp WriteBalancedTxResponse | ExposeEndpointResp EndpointDescription (EndpointValue JSON.Value) | PosixTimeRangeToContainedSlotRangeResp (Either SlotConversionError SlotRange) + | YieldUnbalancedTxResp () deriving stock (Eq, Show, Generic) deriving anyclass (ToJSON, FromJSON) @@ -171,6 +176,7 @@ instance Pretty PABResp where WriteBalancedTxResp r -> "Write balanced tx:" <+> pretty r ExposeEndpointResp desc rsp -> "Call endpoint" <+> pretty desc <+> "with" <+> pretty rsp PosixTimeRangeToContainedSlotRangeResp r -> "Slot range:" <+> pretty r + YieldUnbalancedTxResp () -> "Yielded unbalanced tx" matches :: PABReq -> PABResp -> Bool matches a b = case (a, b) of @@ -190,6 +196,7 @@ matches a b = case (a, b) of (ExposeEndpointReq ActiveEndpoint{aeDescription}, ExposeEndpointResp desc _) | aeDescription == desc -> True (PosixTimeRangeToContainedSlotRangeReq{}, PosixTimeRangeToContainedSlotRangeResp{}) -> True + (YieldUnbalancedTxReq{}, YieldUnbalancedTxResp{}) -> True _ -> False chainIndexMatches :: ChainIndexQuery -> ChainIndexResponse -> Bool diff --git a/plutus-contract/src/Plutus/Contract/Request.hs b/plutus-contract/src/Plutus/Contract/Request.hs index 97300060f5..563ea65b22 100644 --- a/plutus-contract/src/Plutus/Contract/Request.hs +++ b/plutus-contract/src/Plutus/Contract/Request.hs @@ -82,6 +82,7 @@ module Plutus.Contract.Request( , submitTxConstraintsWith , submitTxConfirmed , mkTxConstraints + , yieldUnbalancedTx -- * Etc. , ContractRow , pabReq @@ -790,3 +791,11 @@ submitTxConstraintsWith sl constraints = mkTxConstraints sl constraints >>= subm -- confirmed on the ledger before returning. submitTxConfirmed :: forall w s e. (AsContractError e) => UnbalancedTx -> Contract w s e () submitTxConfirmed t = submitUnbalancedTx t >>= awaitTxConfirmed . getCardanoTxId + +-- | Take an 'UnbalancedTx' then balance, sign and submit it to the blockchain +-- without returning any results. +yieldUnbalancedTx + :: forall w s e. (AsContractError e) + => UnbalancedTx + -> Contract w s e () +yieldUnbalancedTx utx = pabReq (YieldUnbalancedTxReq utx) E._YieldUnbalancedTxResp diff --git a/plutus-contract/src/Plutus/Contract/Trace.hs b/plutus-contract/src/Plutus/Contract/Trace.hs index 6083685a79..24b50383fc 100644 --- a/plutus-contract/src/Plutus/Contract/Trace.hs +++ b/plutus-contract/src/Plutus/Contract/Trace.hs @@ -24,7 +24,6 @@ module Plutus.Contract.Trace , AsTraceError(..) , toNotifyError -- * Handle contract requests - , handleBlockchainQueries , handleSlotNotifications , handleTimeNotifications , handleOwnPubKeyHashQueries @@ -35,6 +34,7 @@ module Plutus.Contract.Trace , handlePendingTransactions , handleChainIndexQueries , handleOwnInstanceIdQueries + , handleYieldedUnbalancedTx -- * Initial distributions of emulated chains , InitialDistribution , defaultDist @@ -67,7 +67,6 @@ import Ledger.Ada qualified as Ada import Ledger.Value (Value) import Plutus.ChainIndex (ChainIndexQueryEffect) -import Plutus.Trace.Emulator.Types (EmulatedWalletEffects) import Wallet.Effects (NodeClientEffect, WalletEffect) import Wallet.Emulator (Wallet) import Wallet.Emulator qualified as EM @@ -139,23 +138,6 @@ handleTimeToSlotConversions :: handleTimeToSlotConversions = generalise (preview E._PosixTimeRangeToContainedSlotRangeReq) (E.PosixTimeRangeToContainedSlotRangeResp . Right) RequestHandler.handleTimeToSlotConversions -handleBlockchainQueries :: - RequestHandler - (Reader ContractInstanceId ': EmulatedWalletEffects) - PABReq - PABResp -handleBlockchainQueries = - handleUnbalancedTransactions - <> handlePendingTransactions - <> handleChainIndexQueries - <> handleOwnPubKeyHashQueries - <> handleOwnInstanceIdQueries - <> handleSlotNotifications - <> handleCurrentSlotQueries - <> handleTimeNotifications - <> handleCurrentTimeQueries - <> handleTimeToSlotConversions - handleUnbalancedTransactions :: ( Member (LogObserve (LogMessage Text)) effs , Member (LogMsg RequestHandlerLogMsg) effs @@ -207,6 +189,17 @@ handleOwnInstanceIdQueries :: handleOwnInstanceIdQueries = generalise (preview E._OwnContractInstanceIdReq) E.OwnContractInstanceIdResp RequestHandler.handleOwnInstanceIdQueries +handleYieldedUnbalancedTx :: + ( Member (LogObserve (LogMessage Text)) effs + , Member WalletEffect effs + ) + => RequestHandler effs PABReq PABResp +handleYieldedUnbalancedTx = + generalise + (preview E._YieldUnbalancedTxReq) + E.YieldUnbalancedTxResp + RequestHandler.handleYieldedUnbalancedTx + defaultDist :: InitialDistribution defaultDist = defaultDistFor EM.knownWallets diff --git a/plutus-contract/src/Plutus/Contract/Trace/RequestHandler.hs b/plutus-contract/src/Plutus/Contract/Trace/RequestHandler.hs index b1e2466516..7ad584e6b3 100644 --- a/plutus-contract/src/Plutus/Contract/Trace/RequestHandler.hs +++ b/plutus-contract/src/Plutus/Contract/Trace/RequestHandler.hs @@ -27,6 +27,7 @@ module Plutus.Contract.Trace.RequestHandler( , handlePendingTransactions , handleChainIndexQueries , handleOwnInstanceIdQueries + , handleYieldedUnbalancedTx ) where import Control.Applicative (Alternative (empty, (<|>))) @@ -59,7 +60,6 @@ import Wallet.Effects qualified import Wallet.Emulator.LogMessages (RequestHandlerLogMsg (..)) import Wallet.Types (ContractInstanceId) - -- | Request handlers that can choose whether to handle an effect (using -- 'Alternative'). This is useful if 'req' is a sum type. newtype RequestHandler effs req resp = RequestHandler { unRequestHandler :: req -> Eff (NonDet ': effs) resp } @@ -241,3 +241,14 @@ handleOwnInstanceIdQueries :: => RequestHandler effs a ContractInstanceId handleOwnInstanceIdQueries = RequestHandler $ \_ -> surroundDebug @Text "handleOwnInstanceIdQueries" ask + +handleYieldedUnbalancedTx :: + forall effs. + ( Member WalletEffect effs + , Member (LogObserve (LogMessage Text)) effs + ) + => RequestHandler effs UnbalancedTx () +handleYieldedUnbalancedTx = + RequestHandler $ \utx -> + surroundDebug @Text "handleYieldedUnbalancedTx" $ do + Wallet.yieldUnbalancedTx utx diff --git a/plutus-contract/src/Plutus/Contract/Wallet.hs b/plutus-contract/src/Plutus/Contract/Wallet.hs index eed928ca0a..a357e7bbcc 100644 --- a/plutus-contract/src/Plutus/Contract/Wallet.hs +++ b/plutus-contract/src/Plutus/Contract/Wallet.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE BlockArguments #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE NamedFieldPuns #-} @@ -13,26 +15,32 @@ module Plutus.Contract.Wallet( balanceTx , handleTx + , yieldUnbalancedTx , getUnspentOutput , WAPI.signTxAndSubmit -- * Exporting transactions , ExportTx(..) , ExportTxInput(..) + , ExportTxRedeemer(..) , export ) where import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C +import Control.Applicative ((<|>)) import Control.Monad (join, (>=>)) import Control.Monad.Error.Lens (throwing) import Control.Monad.Freer (Eff, Member) import Control.Monad.Freer.Error (Error, throwError) -import Data.Aeson (ToJSON (..), Value (String), object, (.=)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..), Value (String), object, withObject, (.:), (.=)) import Data.Aeson.Extras qualified as Aeson.Extras import Data.Aeson.Extras qualified as JSON +import Data.Aeson.Types (Parser, parseFail) +import Data.Bifunctor (first) import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (mapMaybe) +import Data.OpenApi qualified as OpenApi import Data.Set qualified as Set import Data.Typeable (Typeable) import Data.Void (Void) @@ -46,9 +54,10 @@ import Plutus.Contract.CardanoAPI qualified as CardanoAPI import Plutus.Contract.Request qualified as Contract import Plutus.Contract.Types (Contract (..)) import Plutus.V1.Ledger.Scripts (MintingPolicyHash) +import Plutus.V1.Ledger.TxId (TxId (TxId)) import PlutusTx qualified import Wallet.API qualified as WAPI -import Wallet.Effects (WalletEffect, balanceTx) +import Wallet.Effects (WalletEffect, balanceTx, yieldUnbalancedTx) import Wallet.Emulator.Error (WalletAPIError) import Wallet.Types (AsContractError (_ConstraintResolutionError, _OtherError)) @@ -118,7 +127,30 @@ instance ToJSON ExportTxRedeemerPurpose where data ExportTxRedeemer = SpendingRedeemer{ redeemer:: Plutus.Redeemer, redeemerOutRef :: TxOutRef } | MintingRedeemer { redeemer:: Plutus.Redeemer, redeemerPolicyId :: MintingPolicyHash } - deriving stock (Generic, Typeable) + deriving stock (Eq, Show, Generic, Typeable) + deriving anyclass (OpenApi.ToSchema) + +instance FromJSON ExportTxRedeemer where + parseJSON v = parseSpendingRedeemer v <|> parseMintingRedeemer v + +parseSpendingRedeemer :: Value -> Parser ExportTxRedeemer +parseSpendingRedeemer = + withObject "Redeemer" $ \o -> do + inputObj <- o .: "input" :: Parser Object + let txOutRefParse = Plutus.TxOutRef <$> (TxId <$> (inputObj .: "id")) + <*> inputObj .: "index" + SpendingRedeemer <$> parseRedeemerData o <*> txOutRefParse + +parseMintingRedeemer :: Value -> Parser ExportTxRedeemer +parseMintingRedeemer = + withObject "Redeemer" $ \o -> MintingRedeemer + <$> parseRedeemerData o + <*> o .: "policy_id" + +parseRedeemerData :: Object -> Parser Plutus.Redeemer +parseRedeemerData o = + fmap (\(JSON.JSONViaSerialise d) -> Plutus.Redeemer $ PlutusTx.dataToBuiltinData d) + (o .: "data") instance ToJSON ExportTxRedeemer where toJSON SpendingRedeemer{redeemer=Plutus.Redeemer dt, redeemerOutRef=Plutus.TxOutRef{Plutus.txOutRefId, Plutus.txOutRefIdx}} = @@ -133,7 +165,29 @@ data ExportTx = , lookups :: [ExportTxInput] -- ^ The tx outputs for all inputs spent by the partial tx , redeemers :: [ExportTxRedeemer] } - deriving stock (Generic, Typeable) + deriving stock (Eq, Show, Generic, Typeable) + deriving anyclass (OpenApi.ToSchema) + +instance FromJSON ExportTx where + parseJSON = withObject "ExportTx" $ \v -> ExportTx + <$> parsePartialTx v + <*> v .: "inputs" + <*> v .: "redeemers" + where + parsePartialTx v = + v .: "transaction" >>= \t -> + either parseFail pure $ JSON.tryDecode t + >>= (first show . C.deserialiseFromCBOR (C.AsTx C.AsAlonzoEra)) + +-- IMPORTANT: The JSON produced here needs to match the schema expected by +-- https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/balanceTransaction +instance ToJSON ExportTx where + toJSON ExportTx{partialTx, lookups, redeemers} = + object + [ "transaction" .= Aeson.Extras.encodeByteString (C.serialiseToCBOR partialTx) + , "inputs" .= lookups + , "redeemers" .= redeemers + ] data ExportTxInput = ExportTxInput @@ -141,9 +195,31 @@ data ExportTxInput = , etxiTxIx :: C.TxIx , etxiAddress :: C.AddressInEra C.AlonzoEra , etxiLovelaceQuantity :: C.Lovelace - , etxiDatumHash :: C.Hash C.ScriptData + , etxiDatumHash :: Maybe (C.Hash C.ScriptData) , etxiAssets :: [(C.PolicyId, C.AssetName, C.Quantity)] } + deriving stock (Eq, Show, Generic) + deriving anyclass (OpenApi.ToSchema) + +instance FromJSON ExportTxInput where + parseJSON = withObject "ExportTxInput" $ \o -> ExportTxInput + <$> o .: "id" + <*> o .: "index" + <*> parseAddress o + <*> (o .: "amount" >>= \amountField -> amountField .: "quantity") + <*> o .: "datum" + <*> (o .: "assets" >>= mapM parseAsset) + where + parseAddress o = do + addressField <- o .: "address" + let deserialisedAddr = C.deserialiseAddress (C.AsAddressInEra C.AsAlonzoEra) addressField + maybe (parseFail "Failed to deserialise address field") pure deserialisedAddr + parseAsset :: Object -> Parser (C.PolicyId, C.AssetName, C.Quantity) + parseAsset o = do + policyId <- o .: "policy_id" + assetName <- o .: "asset_name" + qty <- o .: "quantity" + pure (policyId, assetName, qty) instance ToJSON ExportTxInput where toJSON ExportTxInput{etxiId, etxiTxIx, etxiLovelaceQuantity, etxiDatumHash, etxiAssets, etxiAddress} = @@ -156,16 +232,6 @@ instance ToJSON ExportTxInput where , "assets" .= fmap (\(p, a, q) -> object ["policy_id" .= p, "asset_name" .= a, "quantity" .= q]) etxiAssets ] --- IMPORTANT: The JSON produced here needs to match the schema expected by --- https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/balanceTransaction -instance ToJSON ExportTx where - toJSON ExportTx{partialTx, lookups, redeemers} = - object - [ "transaction" .= Aeson.Extras.encodeByteString (C.serialiseToCBOR partialTx) - , "inputs" .= lookups - , "redeemers" .= redeemers - ] - export :: C.ProtocolParameters -> C.NetworkId -> UnbalancedTx -> Either CardanoAPI.ToCardanoError ExportTx export params networkId UnbalancedTx{unBalancedTxTx, unBalancedTxUtxoIndex, unBalancedTxRequiredSignatories} = let requiredSigners = fst <$> Map.toList unBalancedTxRequiredSignatories in @@ -189,7 +255,7 @@ toExportTxInput networkId Plutus.TxOutRef{Plutus.txOutRefId, Plutus.txOutRefIdx} <*> pure (C.TxIx $ fromInteger txOutRefIdx) <*> CardanoAPI.toCardanoAddress networkId txOutAddress <*> pure (C.selectLovelace cardanoValue) - <*> CardanoAPI.toCardanoScriptDataHash dh + <*> either (const $ pure Nothing) (pure . Just) (CardanoAPI.toCardanoScriptDataHash dh) <*> pure otherQuantities toExportTxInput _ _ _ = Left CardanoAPI.PublicKeyInputsNotSupported diff --git a/plutus-contract/src/Plutus/Trace/Emulator/ContractInstance.hs b/plutus-contract/src/Plutus/Trace/Emulator/ContractInstance.hs index 1272d0884d..947a30e4bb 100644 --- a/plutus-contract/src/Plutus/Trace/Emulator/ContractInstance.hs +++ b/plutus-contract/src/Plutus/Trace/Emulator/ContractInstance.hs @@ -61,7 +61,7 @@ import Plutus.Contract.Effects (PABReq, PABResp (AwaitTxStatusChangeResp), match import Plutus.Contract.Effects qualified as E import Plutus.Contract.Resumable (Request (..), Response (..)) import Plutus.Contract.Resumable qualified as State -import Plutus.Contract.Trace (handleBlockchainQueries) +import Plutus.Contract.Trace qualified as RequestHandler import Plutus.Contract.Trace.RequestHandler (RequestHandler (..), RequestHandlerLogMsg, tryHandler, wrapHandler) import Plutus.Contract.Types (ResumableResult (..), lastLogs, requests, resumableResult) import Plutus.Trace.Emulator.Types (ContractConstraints, ContractHandle (..), ContractInstanceLog (..), @@ -233,6 +233,25 @@ waitForNextMessage isLogShowed = do response mkAgentSysCall prio WaitForMessage +handleBlockchainQueries :: + RequestHandler + (Reader ContractInstanceId ': EmulatedWalletEffects) + PABReq + PABResp +handleBlockchainQueries = + RequestHandler.handleUnbalancedTransactions + <> RequestHandler.handlePendingTransactions + <> RequestHandler.handleChainIndexQueries + <> RequestHandler.handleOwnPubKeyHashQueries + <> RequestHandler.handleOwnInstanceIdQueries + <> RequestHandler.handleSlotNotifications + <> RequestHandler.handleCurrentSlotQueries + <> RequestHandler.handleTimeNotifications + <> RequestHandler.handleCurrentTimeQueries + <> RequestHandler.handleTimeToSlotConversions + <> RequestHandler.handleYieldedUnbalancedTx + + decodeEvent :: forall effs. ( Member (LogMsg ContractInstanceMsg) effs diff --git a/plutus-contract/src/Wallet/API.hs b/plutus-contract/src/Wallet/API.hs index 17be968dd7..3a59521cc4 100644 --- a/plutus-contract/src/Wallet/API.hs +++ b/plutus-contract/src/Wallet/API.hs @@ -25,6 +25,7 @@ module Wallet.API( submitTxn, ownPubKeyHash, balanceTx, + yieldUnbalancedTx, NodeClientEffect, publishTx, getClientSlot, diff --git a/plutus-contract/src/Wallet/Effects.hs b/plutus-contract/src/Wallet/Effects.hs index 3830624b03..d5b0582d51 100644 --- a/plutus-contract/src/Wallet/Effects.hs +++ b/plutus-contract/src/Wallet/Effects.hs @@ -17,6 +17,7 @@ module Wallet.Effects( , balanceTx , totalFunds , walletAddSignature + , yieldUnbalancedTx -- * Node client , NodeClientEffect(..) , publishTx @@ -36,6 +37,8 @@ data WalletEffect r where BalanceTx :: UnbalancedTx -> WalletEffect (Either WalletAPIError CardanoTx) TotalFunds :: WalletEffect Value -- ^ Total of all funds that are in the wallet (incl. tokens) WalletAddSignature :: CardanoTx -> WalletEffect CardanoTx + -- | Sends an unbalanced tx to be balanced, signed and submitted. + YieldUnbalancedTx :: UnbalancedTx -> WalletEffect () makeEffect ''WalletEffect data NodeClientEffect r where diff --git a/plutus-contract/src/Wallet/Emulator/Wallet.hs b/plutus-contract/src/Wallet/Emulator/Wallet.hs index 68f1cbe735..298cc072e8 100644 --- a/plutus-contract/src/Wallet/Emulator/Wallet.hs +++ b/plutus-contract/src/Wallet/Emulator/Wallet.hs @@ -57,6 +57,7 @@ import Ledger.Value qualified as Value import Plutus.ChainIndex (PageQuery) import Plutus.ChainIndex qualified as ChainIndex import Plutus.ChainIndex.Emulator (ChainIndexEmulatorState, ChainIndexQueryEffect) +import Plutus.Contract (WalletAPIError) import Plutus.Contract.Checkpoint (CheckpointLogMsg) import PlutusTx.Prelude qualified as PlutusTx import Prelude as P @@ -189,7 +190,8 @@ emptyWalletState :: Wallet -> Maybe WalletState emptyWalletState = fmap fromMockWallet . walletMockWallet handleWallet :: - ( Member NodeClientEffect effs + ( Member (Error WalletAPIError) effs + , Member NodeClientEffect effs , Member ChainIndexQueryEffect effs , Member (State WalletState) effs , Member (LogMsg TxBalanceMsg) effs @@ -197,14 +199,32 @@ handleWallet :: => FeeConfig -> WalletEffect ~> Eff effs handleWallet feeCfg = \case - SubmitTxn ctx -> do - case ctx of - Left _ -> error "Wallet.Emulator.Wallet.handleWallet: Expecting a mock tx, not an Alonzo tx when submitting it." - Right tx -> do - logInfo $ SubmittingTx tx - publishTx tx - OwnPubKeyHash -> gets (CW.pubKeyHash . _mockWallet) - BalanceTx utx' -> runError $ do + SubmitTxn tx -> submitTxnH tx + OwnPubKeyHash -> ownPubKeyHashH + BalanceTx utx -> balanceTxH utx + WalletAddSignature tx -> walletAddSignatureH tx + TotalFunds -> totalFundsH + YieldUnbalancedTx utx -> yieldUnbalancedTxH utx + + where + submitTxnH :: (Member NodeClientEffect effs, Member (LogMsg TxBalanceMsg) effs) => CardanoTx -> Eff effs () + submitTxnH (Left _) = error "Wallet.Emulator.Wallet.handleWallet: Expecting a mock tx, not an Alonzo tx when submitting it." + submitTxnH (Right tx) = do + logInfo $ SubmittingTx tx + publishTx tx + + ownPubKeyHashH :: (Member (State WalletState) effs) => Eff effs PubKeyHash + ownPubKeyHashH = gets (CW.pubKeyHash . _mockWallet) + + balanceTxH :: + ( Member NodeClientEffect effs + , Member ChainIndexQueryEffect effs + , Member (State WalletState) effs + , Member (LogMsg TxBalanceMsg) effs + ) + => UnbalancedTx + -> Eff effs (Either WalletAPIError CardanoTx) + balanceTxH utx' = runError $ do logInfo $ BalancingUnbalancedTx utx' utxo <- get >>= ownOutputs slotConfig <- WAPI.getClientSlotConfig @@ -216,11 +236,28 @@ handleWallet feeCfg = \case tx'' <- handleAddSignature tx' logInfo $ FinishedBalancing tx'' pure $ Right tx'' - WalletAddSignature ctx -> - case ctx of - Left _ -> error "Wallet.Emulator.Wallet.handleWallet: Expecting a mock tx, not an Alonzo tx when adding a signature." - Right tx -> Right <$> handleAddSignature tx - TotalFunds -> foldMap (view ciTxOutValue) <$> (get >>= ownOutputs) + + walletAddSignatureH :: (Member (State WalletState) effs) => CardanoTx -> Eff effs CardanoTx + walletAddSignatureH (Left _) = error "Wallet.Emulator.Wallet.handleWallet: Expecting a mock tx, not an Alonzo tx when adding a signature." + walletAddSignatureH (Right tx) = Right <$> handleAddSignature tx + + totalFundsH :: (Member (State WalletState) effs, Member ChainIndexQueryEffect effs) => Eff effs Value + totalFundsH = foldMap (view ciTxOutValue) <$> (get >>= ownOutputs) + + yieldUnbalancedTxH :: + ( Member (Error WalletAPIError) effs + , Member NodeClientEffect effs + , Member ChainIndexQueryEffect effs + , Member (State WalletState) effs + , Member (LogMsg TxBalanceMsg) effs + ) + => UnbalancedTx + -> Eff effs () + yieldUnbalancedTxH utx = do + balancedTxM <- balanceTxH utx + case balancedTxM of + Left err -> throwError err + Right balancedTx -> walletAddSignatureH balancedTx >>= submitTxnH handleAddSignature :: Member (State WalletState) effs diff --git a/plutus-contract/test/Spec.hs b/plutus-contract/test/Spec.hs index 63c60e14b9..a159ed165e 100644 --- a/plutus-contract/test/Spec.hs +++ b/plutus-contract/test/Spec.hs @@ -4,6 +4,7 @@ module Main(main) where import Spec.Contract qualified import Spec.Emulator qualified import Spec.ErrorChecking qualified +import Spec.Plutus.Contract.Wallet qualified import Spec.Rows qualified import Spec.Secrets qualified import Spec.State qualified @@ -21,5 +22,6 @@ tests = testGroup "plutus-contract" [ Spec.Rows.tests, Spec.ThreadToken.tests, Spec.Secrets.tests, - Spec.ErrorChecking.tests + Spec.ErrorChecking.tests, + Spec.Plutus.Contract.Wallet.tests ] diff --git a/plutus-contract/test/Spec/Plutus/Contract/Wallet.hs b/plutus-contract/test/Spec/Plutus/Contract/Wallet.hs new file mode 100644 index 0000000000..12f364f94e --- /dev/null +++ b/plutus-contract/test/Spec/Plutus/Contract/Wallet.hs @@ -0,0 +1,100 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Spec.Plutus.Contract.Wallet + ( tests + ) where + +import Cardano.Api qualified as C +import Data.Aeson (decode, encode) +import Data.Functor.Identity (Identity) +import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty qualified as NonEmpty +import Data.Maybe (mapMaybe) +import Gen.Cardano.Api.Typed qualified as Gen +import Hedgehog (MonadGen, Property) +import Hedgehog qualified +import Hedgehog.Gen qualified as Gen +import Hedgehog.Range qualified as Range +import Ledger (MintingPolicyHash, TxOutRef (..)) +import Ledger.Scripts qualified as Script +import Ledger.Tx.CardanoAPI (fromCardanoPolicyId, fromCardanoTxId) +import Plutus.Contract.Wallet (ExportTx (ExportTx), ExportTxInput (..), + ExportTxRedeemer (MintingRedeemer, SpendingRedeemer)) +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.Hedgehog (testProperty) + +tests :: TestTree +tests = + testGroup + "Plutus.Cardano.Wallet" + [ testProperty "ExportTx FromJSON and ToJSON inverse property" jsonInvProp + ] + +jsonInvProp :: Property +jsonInvProp = Hedgehog.property $ do + exportTx <- Hedgehog.forAll exportTxGen + Hedgehog.tripping exportTx encode decode + +exportTxGen :: (Hedgehog.GenBase m ~ Identity, MonadFail m, MonadGen m) => m ExportTx +exportTxGen = do + exportTxInputs <- Gen.list (Range.linear 0 5) exportTxInputGen + ExportTx + <$> Hedgehog.fromGenT (Gen.genTx C.AlonzoEra) + <*> pure exportTxInputs + <*> exportTxRedeemersGen exportTxInputs + +exportTxInputGen :: (Hedgehog.GenBase m ~ Identity, MonadFail m, MonadGen m) => m ExportTxInput +exportTxInputGen = do + C.TxIn txId txIx <- Hedgehog.fromGenT Gen.genTxIn + C.TxOut addressInEra txOutValue txOutDatum <- Hedgehog.fromGenT (Gen.genTxOut C.AlonzoEra) + let datumToScriptDataHash C.TxOutDatumNone = Nothing + datumToScriptDataHash (C.TxOutDatumHash _ h) = Just h + datumToScriptDataHash (C.TxOutDatum _ d) = Just $ C.hashScriptData d + pure $ ExportTxInput + txId + txIx + addressInEra + (C.txOutValueToLovelace txOutValue) + (datumToScriptDataHash txOutDatum) + (currenciesFromTxOutValue txOutValue) + +currenciesFromTxOutValue :: C.TxOutValue C.AlonzoEra -> [(C.PolicyId, C.AssetName, C.Quantity)] +currenciesFromTxOutValue txOutValue = + mapMaybe currencyFromValue $ C.valueToList $ C.txOutValueToValue txOutValue + where + currencyFromValue :: (C.AssetId, C.Quantity) -> Maybe (C.PolicyId, C.AssetName, C.Quantity) + currencyFromValue (C.AdaAssetId, _) = Nothing + currencyFromValue (C.AssetId policyId assetName, qty) = Just (policyId, assetName, qty) + +exportTxRedeemersGen :: MonadGen m => [ExportTxInput] -> m [ExportTxRedeemer] +exportTxRedeemersGen [] = pure [] +exportTxRedeemersGen inputs = do + let spendingGenM = fmap exportTxSpendingRedeemerGen $ NonEmpty.nonEmpty $ fmap getTxOutRef inputs + mintingGenM = fmap exportTxMintingRedeemerGen $ NonEmpty.nonEmpty $ concatMap getMintingPolicyHashes inputs + case (spendingGenM, mintingGenM) of + (Just spendingGen, Just mintingGen) -> + Gen.list (Range.linear 0 (length inputs)) $ Gen.choice [mintingGen, spendingGen] + (Just spendingGen, Nothing) -> + Gen.list (Range.linear 0 (length inputs)) spendingGen + (Nothing, Just mintingGen) -> + Gen.list (Range.linear 0 (length inputs)) mintingGen + (Nothing, Nothing) -> pure [] + where + getTxOutRef :: ExportTxInput -> TxOutRef + getTxOutRef ExportTxInput { etxiId, etxiTxIx = (C.TxIx txIx) } = + TxOutRef (fromCardanoTxId etxiId) (toInteger txIx) + + getMintingPolicyHashes :: ExportTxInput -> [MintingPolicyHash] + getMintingPolicyHashes ExportTxInput { etxiAssets } = + fmap (\(policyId, _, _) -> fromCardanoPolicyId policyId) etxiAssets + +exportTxSpendingRedeemerGen :: MonadGen m => NonEmpty TxOutRef -> m ExportTxRedeemer +exportTxSpendingRedeemerGen txOutRefs = do + txOutRef <- Gen.element (NonEmpty.toList txOutRefs) + pure $ SpendingRedeemer Script.unitRedeemer txOutRef + +exportTxMintingRedeemerGen :: MonadGen m => NonEmpty MintingPolicyHash -> m ExportTxRedeemer +exportTxMintingRedeemerGen policyHashes = do + policyHash <- Gen.element (NonEmpty.toList policyHashes) + pure $ MintingRedeemer Script.unitRedeemer policyHash diff --git a/plutus-ledger/src/Ledger/Orphans.hs b/plutus-ledger/src/Ledger/Orphans.hs index 6ca009f145..545a901cf7 100644 --- a/plutus-ledger/src/Ledger/Orphans.hs +++ b/plutus-ledger/src/Ledger/Orphans.hs @@ -7,7 +7,12 @@ module Ledger.Orphans where +import Cardano.Api qualified as C +import Cardano.Crypto.Hash qualified as Hash import Cardano.Crypto.Wallet qualified as Crypto +import Cardano.Ledger.Crypto qualified as C +import Cardano.Ledger.Hashes qualified as Hashes +import Cardano.Ledger.SafeHash qualified as C import Control.Lens ((&), (.~), (?~)) import Control.Monad.Freer.Extras.Log (LogLevel, LogMessage) import Crypto.Hash qualified as Crypto @@ -18,6 +23,7 @@ import Data.OpenApi qualified as OpenApi import Data.Text qualified as Text import Data.Typeable import GHC.Exts (IsList (..)) +import GHC.Generics (Generic) import Plutus.V1.Ledger.Ada import Plutus.V1.Ledger.Api import Plutus.V1.Ledger.Bytes (bytes) @@ -30,7 +36,6 @@ import PlutusTx.AssocMap qualified as AssocMap import Prelude as Haskell import Web.HttpApiData (FromHttpApiData (..), ToHttpApiData (..)) - instance ToHttpApiData PrivateKey where toUrlPiece = toUrlPiece . getPrivateKey @@ -45,6 +50,32 @@ instance FromHttpApiData LedgerBytes where -- | OpenApi instances for swagger support +instance OpenApi.ToSchema C.ScriptHash where + declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "ScriptHash") mempty +instance OpenApi.ToSchema (C.AddressInEra C.AlonzoEra) where + declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "AddressInAlonzoEra") mempty +deriving instance Generic C.ScriptData +instance OpenApi.ToSchema C.ScriptData where + declareNamedSchema _ = + pure $ OpenApi.NamedSchema (Just "ScriptData") OpenApi.byteSchema +-- deriving instance Generic (C.Hash C.ScriptData) +instance OpenApi.ToSchema (C.Hash C.ScriptData) where + declareNamedSchema _ = + pure $ OpenApi.NamedSchema (Just "HashScriptData") OpenApi.byteSchema +deriving instance Generic C.TxId +deriving anyclass instance OpenApi.ToSchema C.TxId +deriving instance Generic C.TxIx +deriving anyclass instance OpenApi.ToSchema C.TxIx +deriving instance Generic C.Lovelace +deriving anyclass instance OpenApi.ToSchema C.Lovelace +deriving instance Generic C.PolicyId +deriving anyclass instance OpenApi.ToSchema C.PolicyId +instance OpenApi.ToSchema C.AssetName where + declareNamedSchema _ = + pure $ OpenApi.NamedSchema (Just "AssetName") OpenApi.byteSchema +deriving instance Generic C.Quantity +deriving anyclass instance OpenApi.ToSchema C.Quantity + deriving anyclass instance (OpenApi.ToSchema k, OpenApi.ToSchema v) => OpenApi.ToSchema (AssocMap.Map k v) instance OpenApi.ToSchema BuiltinByteString where declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "Bytes") mempty @@ -53,6 +84,10 @@ instance OpenApi.ToSchema Crypto.XPub where instance OpenApi.ToSchema Crypto.XPrv where declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "PrvKey") mempty instance OpenApi.ToSchema (Crypto.Digest Crypto.Blake2b_160) where + declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "Digest") mempty +instance OpenApi.ToSchema (Hash.Hash Hash.Blake2b_256 Hashes.EraIndependentTxBody) where + declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "Hash") mempty +instance OpenApi.ToSchema (C.SafeHash C.StandardCrypto Hashes.EraIndependentData) where declareNamedSchema _ = pure $ OpenApi.NamedSchema (Just "Hash") mempty deriving instance OpenApi.ToSchema (LogMessage JSON.Value) deriving instance OpenApi.ToSchema LogLevel diff --git a/plutus-ledger/src/Ledger/Tx/CardanoAPI.hs b/plutus-ledger/src/Ledger/Tx/CardanoAPI.hs index 0c9f8d9625..22e25ff376 100644 --- a/plutus-ledger/src/Ledger/Tx/CardanoAPI.hs +++ b/plutus-ledger/src/Ledger/Tx/CardanoAPI.hs @@ -23,9 +23,11 @@ module Ledger.Tx.CardanoAPI( , fromCardanoTxInsCollateral , fromCardanoTxInWitness , fromCardanoTxOut + , fromCardanoTxOutDatumHash , fromCardanoAddress , fromCardanoMintValue , fromCardanoValue + , fromCardanoPolicyId , fromCardanoFee , fromCardanoValidityRange , fromCardanoScriptInEra diff --git a/plutus-pab/local-cluster/Main.hs b/plutus-pab/local-cluster/Main.hs index 0cb9511d4c..98e3915420 100644 --- a/plutus-pab/local-cluster/Main.hs +++ b/plutus-pab/local-cluster/Main.hs @@ -1,7 +1,5 @@ {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} @@ -33,8 +31,6 @@ import Cardano.Wallet.Api.Types (ApiMnemonicT (..), ApiT (..), ApiWallet (..), E WalletOrAccountPostData (..)) import Cardano.Wallet.Api.Types qualified as Wallet.Types import Cardano.Wallet.Logging (stdoutTextTracer, trMessageText) -import Cardano.Wallet.Mock.Types (WalletUrl (..)) -import Cardano.Wallet.Mock.Types qualified as Wallet.Config import Cardano.Wallet.Primitive.AddressDerivation (NetworkDiscriminant (..), Passphrase (..)) import Cardano.Wallet.Primitive.SyncProgress (SyncTolerance (..)) import Cardano.Wallet.Primitive.Types (WalletName (..)) @@ -45,6 +41,8 @@ import Cardano.Wallet.Shelley.Launch.Cluster (ClusterLog (..), Credential (..), localClusterConfigFromEnv, moveInstantaneousRewardsTo, oneMillionAda, sendFaucetAssetsTo, sendFaucetFundsTo, testMinSeverityFromEnv, tokenMetadataServerFromEnv, walletMinSeverityFromEnv, withCluster) +import Cardano.Wallet.Types (WalletUrl (..)) +import Cardano.Wallet.Types qualified as Wallet.Config import ContractExample (ExampleContracts) import Control.Arrow (first) import Control.Concurrent (threadDelay) @@ -151,13 +149,11 @@ main = withLocalClusterSetup $ \dir lo@LogOutputs{loCluster} -> createDirectory db tokenMetadataServer <- tokenMetadataServerFromEnv - prometheusUrl <- (maybe "none" + prometheusUrl <- maybe "none" (\(h, p) -> T.pack h <> ":" <> toText @(Port "Prometheus") p) - ) <$> getPrometheusURL - ekgUrl <- (maybe "none" + ekgUrl <- maybe "none" (\(h, p) -> T.pack h <> ":" <> toText @(Port "EKG") p) - ) <$> getEKGURL void $ serveWallet @@ -202,12 +198,12 @@ launchChainIndex dir (RunningNode socketPath _block0 (_gp, _vData)) = do {-| Launch the PAB in a separate thread. -} launchPAB :: - Text -> -- ^ Passphrase - FilePath -> -- ^ Temp directory - BaseUrl -> -- ^ wallet url - RunningNode -> -- ^ Socket path - ChainIndexPort -> -- ^ Port of the chain index - IO () + Text -- ^ Passphrase + -> FilePath -- ^ Temp directory + -> BaseUrl -- ^ wallet url + -> RunningNode -- ^ Socket path + -> ChainIndexPort -- ^ Port of the chain index + -> IO () launchPAB passPhrase dir walletUrl (RunningNode socketPath _block0 (_gp, _vData)) (ChainIndexPort chainIndexPort) = do let opts = AppOpts{minLogLevel = Nothing, logConfigPath = Nothing, configPath = Nothing, runEkgServer = False, storageBackend = BeamSqliteBackend, cmd = PABWebserver, PAB.Command.passphrase = Just passPhrase} networkID = NetworkIdWrapper CAPI.Mainnet @@ -216,7 +212,7 @@ launchPAB passPhrase dir walletUrl (RunningNode socketPath _block0 (_gp, _vData) { nodeServerConfig = def{mscSocketPath=nodeSocketFile socketPath,mscNodeMode=AlonzoNode,mscNetworkId=networkID} , dbConfig = def{dbConfigFile = T.pack (dir "plutus-pab.db")} , chainIndexConfig = def{PAB.CI.ciBaseUrl = PAB.CI.ChainIndexUrl $ BaseUrl Http "localhost" chainIndexPort ""} - , walletServerConfig = def{Wallet.Config.baseUrl=WalletUrl walletUrl} + , walletServerConfig = set (Wallet.Config.walletSettings . Wallet.Config.baseUrlL) (WalletUrl walletUrl) def } -- TODO: For some reason this has to be async - program terminates if it's done synchronously??? void . async $ PAB.Run.runWithOpts @ExampleContracts handleBuiltin (Just config) opts{cmd=Migrate} @@ -239,7 +235,7 @@ restoreWallets walletHost walletPort = do (ApiT $ WalletName "plutus-wallet") (ApiT $ Passphrase $ fromString $ T.unpack fixturePassphrase) walletAcc = WalletOrAccountPostData{postData=Left wpData} - result <- flip runClientM clientEnv $ (WalletClient.postWallet WalletClient.walletClient) walletAcc + result <- flip runClientM clientEnv $ WalletClient.postWallet WalletClient.walletClient walletAcc case result of Left err -> do putStrLn "restoreWallet failed" diff --git a/plutus-pab/plutus-pab.cabal b/plutus-pab/plutus-pab.cabal index 4f6dfece2a..294bd00db4 100644 --- a/plutus-pab/plutus-pab.cabal +++ b/plutus-pab/plutus-pab.cabal @@ -1,4 +1,4 @@ -cabal-version: 2.2 +cabal-version: 3.0 name: plutus-pab version: 0.1.0.0 license: Apache-2.0 @@ -72,7 +72,9 @@ library Cardano.Node.Types Cardano.Protocol.Socket.Mock.Client Cardano.Protocol.Socket.Mock.Server - Cardano.Wallet.Client + Cardano.Wallet.LocalClient + Cardano.Wallet.RemoteClient + Cardano.Wallet.Types Cardano.Wallet.Mock.API Cardano.Wallet.Mock.Client Cardano.Wallet.Mock.Handlers @@ -423,6 +425,7 @@ test-suite plutus-pab-test-light main-is: Spec.hs other-modules: Cardano.Api.NetworkId.ExtraSpec + Cardano.Wallet.RemoteClientSpec Cardano.Wallet.ServerSpec Control.Concurrent.STM.ExtrasSpec @@ -433,7 +436,9 @@ test-suite plutus-pab-test-light base >=4.9 && <5, bytestring -any, cardano-api -any, + cardano-api:gen -any, containers -any, + data-default -any, freer-extras -any, freer-simple -any, hedgehog -any, diff --git a/plutus-pab/plutus-pab.yaml.sample b/plutus-pab/plutus-pab.yaml.sample index 5b8a40af3a..883e70b2b7 100644 --- a/plutus-pab/plutus-pab.yaml.sample +++ b/plutus-pab/plutus-pab.yaml.sample @@ -12,9 +12,8 @@ pabWebserverConfig: endpointTimeout: 5 walletServerConfig: + tag: LocalWalletConfig baseUrl: http://localhost:9081 - wallet: - getWallet: 1 nodeServerConfig: mscBaseUrl: http://localhost:9082 @@ -36,7 +35,6 @@ nodeServerConfig: chainIndexConfig: ciBaseUrl: http://localhost:9083 - ciWatchedAddresses: [] requestProcessingConfig: requestProcessingInterval: 1 diff --git a/plutus-pab/src/Cardano/Wallet/Client.hs b/plutus-pab/src/Cardano/Wallet/LocalClient.hs similarity index 84% rename from plutus-pab/src/Cardano/Wallet/Client.hs rename to plutus-pab/src/Cardano/Wallet/LocalClient.hs index b8c2fc27b8..72267a1263 100644 --- a/plutus-pab/src/Cardano/Wallet/Client.hs +++ b/plutus-pab/src/Cardano/Wallet/LocalClient.hs @@ -1,16 +1,13 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ViewPatterns #-} -module Cardano.Wallet.Client where +module Cardano.Wallet.LocalClient where import Cardano.Api qualified import Cardano.Api.NetworkId.Extra (NetworkIdWrapper (..)) @@ -43,6 +40,7 @@ import Data.Text (pack) import Data.Text.Class (fromText) import Ledger (CardanoTx) import Ledger.Ada qualified as Ada +import Ledger.Constraints.OffChain (UnbalancedTx) import Ledger.Tx.CardanoAPI (SomeCardanoApiTx (..), ToCardanoError, toCardanoTxBody) import Ledger.Value (CurrencySymbol (..), TokenName (..), Value (..)) import Plutus.Contract.Wallet (export) @@ -70,7 +68,7 @@ handleWalletClient , Member (Reader Cardano.Api.ProtocolParameters) effs , Member (LogMsg WalletClientMsg) effs ) - => MockServerConfig + => MockServerConfig -- TODO: Rename. Not mock -> Wallet -> WalletEffect ~> Eff effs @@ -96,19 +94,25 @@ handleWalletClient config (Wallet (WalletId walletId)) event = do logWarn (WalletClientError $ show err) throwError err Right _ -> pure result - case event of - SubmitTxn tx -> do + + submitTxnH :: CardanoTx -> Eff effs () + submitTxnH tx = do sealedTx <- either (throwError . ToCardanoError) pure $ toSealedTx protocolParams networkId tx void . runClient $ C.postExternalTransaction C.transactionClient (C.ApiBytesT (C.SerialisedTx $ C.serialisedTx sealedTx)) - OwnPubKeyHash -> do + ownPubKeyHashH :: Eff effs PubKeyHash + ownPubKeyHashH = fmap (PubKeyHash . BuiltinByteString . fst . getApiVerificationKey) . runClient $ - getWalletKey (C.ApiT walletId) (C.ApiT C.UtxoExternal) (C.ApiT (C.DerivationIndex 0)) (Just True) + getWalletKey (C.ApiT walletId) + (C.ApiT C.UtxoExternal) + (C.ApiT (C.DerivationIndex 0)) + (Just True) - BalanceTx utx -> do + balanceTxH :: UnbalancedTx -> Eff effs (Either WalletAPIError CardanoTx) + balanceTxH utx = do case export protocolParams networkId utx of Left err -> do - logWarn $ BalanceTxError $ show $ pretty $ err + logWarn $ BalanceTxError $ show $ pretty err throwOtherError $ pretty err Right ex -> do res <- runClient' $ C.balanceTransaction C.transactionClient (C.ApiT walletId) (toJSON ex) @@ -118,19 +122,36 @@ handleWalletClient config (Wallet (WalletId walletId)) event = do Right r -> do pure (Right $ fromApiSerialisedTransaction r) - WalletAddSignature tx -> do + walletAddSignatureH :: CardanoTx -> Eff effs CardanoTx + walletAddSignatureH tx = do sealedTx <- either (throwError . ToCardanoError) pure $ toSealedTx protocolParams networkId tx passphrase <- maybe (throwError $ OtherError "Wallet passphrase required") pure mpassphrase lenientPP <- either throwOtherError pure $ fromText passphrase let postData = C.ApiSignTransactionPostData (C.ApiT sealedTx) (C.ApiT lenientPP) fmap fromApiSerialisedTransaction . runClient $ C.signTransaction C.transactionClient (C.ApiT walletId) postData - TotalFunds -> do + totalFundsH :: Eff effs Value + totalFundsH = do C.ApiWallet{balance, assets} <- runClient $ C.getWallet C.walletClient (C.ApiT walletId) let C.ApiWalletBalance (Quantity avAda) _ _ = balance C.ApiWalletAssetsBalance (C.ApiT avAssets) _ = assets pure $ Ada.lovelaceValueOf (fromIntegral avAda) <> tokenMapToValue avAssets + yieldUnbalancedTxH :: UnbalancedTx -> Eff effs () + yieldUnbalancedTxH utx = do + balancedTxM <- balanceTxH utx + case balancedTxM of + Left err -> throwError err + Right balancedTx -> walletAddSignatureH balancedTx >>= submitTxnH + + case event of + SubmitTxn tx -> submitTxnH tx + OwnPubKeyHash -> ownPubKeyHashH + BalanceTx utx -> balanceTxH utx + WalletAddSignature tx -> walletAddSignatureH tx + TotalFunds -> totalFundsH + YieldUnbalancedTx utx -> yieldUnbalancedTxH utx + tokenMapToValue :: C.TokenMap -> Value tokenMapToValue = Value . Map.fromList . fmap (bimap coerce (Map.fromList . fmap (bimap coerce (fromIntegral . C.unTokenQuantity)) . toList)) . C.toNestedList diff --git a/plutus-pab/src/Cardano/Wallet/Mock/Client.hs b/plutus-pab/src/Cardano/Wallet/Mock/Client.hs index 6c968e3991..538b923f34 100644 --- a/plutus-pab/src/Cardano/Wallet/Mock/Client.hs +++ b/plutus-pab/src/Cardano/Wallet/Mock/Client.hs @@ -15,9 +15,9 @@ import Control.Monad.Freer.Error (Error, throwError) import Control.Monad.Freer.Reader (Reader, ask) import Control.Monad.IO.Class (MonadIO (..)) import Data.Proxy (Proxy (Proxy)) -import Ledger (Value) +import Ledger (PubKeyHash, Value) import Ledger.Constraints.OffChain (UnbalancedTx) -import Ledger.Tx (Tx) +import Ledger.Tx (CardanoTx, Tx) import Servant ((:<|>) (..)) import Servant.Client (ClientEnv, ClientError, ClientM, client, runClientM) import Wallet.Effects (WalletEffect (..)) @@ -50,6 +50,7 @@ handleWalletClient :: ( LastMember m effs , MonadIO m , Member (Error ClientError) effs + , Member (Error WalletAPIError) effs , Member (Reader ClientEnv) effs ) => Wallet @@ -60,11 +61,35 @@ handleWalletClient wallet event = do let runClient :: forall a. ClientM a -> Eff effs a runClient a = (sendM $ liftIO $ runClientM a clientEnv) >>= either throwError pure + + submitTxnH :: CardanoTx -> Eff effs () + submitTxnH (Left _) = error "Cardano.Wallet.Mock.Client: Expecting a mock tx, not an Alonzo tx when submitting it." + submitTxnH (Right tx) = runClient (submitTxn wallet tx) + + ownPubKeyHashH :: Eff effs PubKeyHash + ownPubKeyHashH = wiPubKeyHash <$> runClient (ownPublicKey wallet) + + balanceTxH :: UnbalancedTx -> Eff effs (Either WalletAPIError CardanoTx) + balanceTxH utx = runClient (fmap (fmap Right) $ balanceTx wallet utx) + + walletAddSignatureH :: CardanoTx -> Eff effs CardanoTx + walletAddSignatureH (Left _) = error "Cardano.Wallet.Mock.Client: Expecting a mock tx, not an Alonzo tx when adding a signature." + walletAddSignatureH (Right tx) = runClient $ fmap Right $ sign wallet tx + + totalFundsH :: Eff effs Value + totalFundsH = runClient (totalFunds wallet) + + yieldUnbalancedTx :: UnbalancedTx -> Eff effs () + yieldUnbalancedTx utx = do + balancedTxM <- balanceTxH utx + case balancedTxM of + Left err -> throwError err + Right balancedTx -> walletAddSignatureH balancedTx >>= submitTxnH + case event of - SubmitTxn (Left _) -> error "Cardano.Wallet.Mock.Client: Expecting a mock tx, not an Alonzo tx when submitting it." - SubmitTxn (Right tx) -> runClient (submitTxn wallet tx) - OwnPubKeyHash -> wiPubKeyHash <$> runClient (ownPublicKey wallet) - BalanceTx utx -> runClient (fmap (fmap Right) $ balanceTx wallet utx) - WalletAddSignature (Left _) -> error "Cardano.Wallet.Mock.Client: Expection a mock tx, not an Alonzo tx when adding a signature." - WalletAddSignature (Right tx) -> runClient $ fmap Right $ sign wallet tx - TotalFunds -> runClient (totalFunds wallet) + SubmitTxn tx -> submitTxnH tx + OwnPubKeyHash -> ownPubKeyHashH + BalanceTx utx -> balanceTxH utx + WalletAddSignature tx -> walletAddSignatureH tx + TotalFunds -> totalFundsH + YieldUnbalancedTx utx -> yieldUnbalancedTx utx diff --git a/plutus-pab/src/Cardano/Wallet/Mock/Server.hs b/plutus-pab/src/Cardano/Wallet/Mock/Server.hs index ec1f2edcc4..dbaee37676 100644 --- a/plutus-pab/src/Cardano/Wallet/Mock/Server.hs +++ b/plutus-pab/src/Cardano/Wallet/Mock/Server.hs @@ -18,8 +18,8 @@ import Cardano.Node.Client as NodeClient import Cardano.Protocol.Socket.Mock.Client qualified as MockClient import Cardano.Wallet.Mock.API (API) import Cardano.Wallet.Mock.Handlers -import Cardano.Wallet.Mock.Types (Port (..), WalletConfig (..), WalletMsg (..), WalletUrl (..), Wallets, createWallet, - getWalletInfo, multiWallet) +import Cardano.Wallet.Mock.Types (Port (..), WalletMsg (..), Wallets, createWallet, getWalletInfo, multiWallet) +import Cardano.Wallet.Types (LocalWalletSettings (..), WalletUrl (..)) import Control.Concurrent.Availability (Availability, available) import Control.Concurrent.MVar (MVar, newMVar) import Control.Monad ((>=>)) @@ -66,8 +66,8 @@ app trace txSendHandle chainSyncHandle chainIndexEnv mVarState feeCfg slotCfg = (\w tx -> fmap (fromRight (error "Cardano.Wallet.Mock.Server: Expecting a mock tx, not an Alonzo tx when adding a signature.")) $ multiWallet (Wallet w) (walletAddSignature $ Right tx)) -main :: Trace IO WalletMsg -> WalletConfig -> FeeConfig -> FilePath -> SlotConfig -> ChainIndexUrl -> Availability -> IO () -main trace WalletConfig { baseUrl } feeCfg serverSocket slotCfg (ChainIndexUrl chainUrl) availability = LM.runLogEffects trace $ do +main :: Trace IO WalletMsg -> LocalWalletSettings -> FeeConfig -> FilePath -> SlotConfig -> ChainIndexUrl -> Availability -> IO () +main trace LocalWalletSettings { baseUrl } feeCfg serverSocket slotCfg (ChainIndexUrl chainUrl) availability = LM.runLogEffects trace $ do chainIndexEnv <- buildEnv chainUrl defaultManagerSettings let knownWallets = Map.fromList $ zip Wallet.knownWallets (Wallet.fromMockWallet <$> CW.knownWallets) mVarState <- liftIO $ newMVar knownWallets diff --git a/plutus-pab/src/Cardano/Wallet/Mock/Types.hs b/plutus-pab/src/Cardano/Wallet/Mock/Types.hs index e3626483c0..9ccca3a0ac 100644 --- a/plutus-pab/src/Cardano/Wallet/Mock/Types.hs +++ b/plutus-pab/src/Cardano/Wallet/Mock/Types.hs @@ -20,9 +20,6 @@ module Cardano.Wallet.Mock.Types ( , createWallet , multiWallet , getWalletInfo - -- * wallet configuration - , WalletConfig (..) - , defaultWalletConfig -- * wallet log messages , WalletMsg (..) @@ -31,7 +28,6 @@ module Cardano.Wallet.Mock.Types ( , Port (..) , NodeClient (..) , ChainClient (..) - , WalletUrl (..) , ChainIndexUrl -- * Wallet info , WalletInfo(..) @@ -47,7 +43,6 @@ import Control.Monad.Freer.Extras.Log (LogMsg) import Control.Monad.Freer.State (State) import Control.Monad.Freer.TH (makeEffect) import Data.Aeson (FromJSON, ToJSON) -import Data.Default (Default, def) import Data.Map.Strict (Map) import Data.Text (Text) import GHC.Generics (Generic) @@ -56,7 +51,7 @@ import Plutus.ChainIndex (ChainIndexQueryEffect) import Plutus.PAB.Arbitrary () import Prettyprinter (Pretty (..), (<+>)) import Servant (ServerError (..)) -import Servant.Client (BaseUrl (..), ClientError, Scheme (..)) +import Servant.Client (ClientError) import Servant.Client.Internal.HttpClient (ClientEnv) import Wallet.Effects (NodeClientEffect, WalletEffect) import Wallet.Emulator.Error (WalletAPIError) @@ -99,30 +94,10 @@ newtype NodeClient = NodeClient ClientEnv newtype ChainClient = ChainClient ClientEnv -newtype WalletUrl = WalletUrl BaseUrl - deriving (Eq, Show, ToJSON, FromJSON) via BaseUrl - newtype Port = Port Int deriving (Show) deriving (Eq, Num, ToJSON, FromJSON, Pretty) via Int -newtype WalletConfig = - WalletConfig - { baseUrl :: WalletUrl - } - deriving (Show, Eq, Generic) - deriving anyclass (FromJSON, ToJSON) - -defaultWalletConfig :: WalletConfig -defaultWalletConfig = - WalletConfig - -- See Note [pab-ports] in "test/full/Plutus/PAB/CliSpec.hs". - { baseUrl = WalletUrl $ BaseUrl Http "localhost" 9081 "" - } - -instance Default WalletConfig where - def = defaultWalletConfig - data WalletMsg = StartingWallet Port | ChainClientMsg Text | Balancing TxBalanceMsg diff --git a/plutus-pab/src/Cardano/Wallet/RemoteClient.hs b/plutus-pab/src/Cardano/Wallet/RemoteClient.hs new file mode 100644 index 0000000000..95fdd8140b --- /dev/null +++ b/plutus-pab/src/Cardano/Wallet/RemoteClient.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeOperators #-} + +module Cardano.Wallet.RemoteClient + ( handleWalletClient + ) where + +import Data.Text qualified as Text +import Cardano.Api.NetworkId.Extra (NetworkIdWrapper (..)) +import Cardano.Api.Shelley qualified as Cardano.Api +import Cardano.Node.Types (MockServerConfig (..)) +import Control.Concurrent.STM qualified as STM +import Control.Monad.Freer (Eff, LastMember, Member, type (~>)) +import Control.Monad.Freer.Reader (Reader, ask) +import Control.Monad.Freer.Error (Error, throwError) +import Wallet.Emulator.Error (WalletAPIError (..)) +import Control.Monad.IO.Class (MonadIO (..)) +import Plutus.Contract.Wallet (export) +import Plutus.PAB.Core.ContractInstance.STM (InstancesState) +import Plutus.PAB.Core.ContractInstance.STM qualified as Instances +import Wallet.Effects (WalletEffect (..)) +import Wallet.Types (ContractInstanceId) + +handleWalletClient + :: forall m effs. + ( LastMember m effs + , MonadIO m + , Member (Error WalletAPIError) effs + , Member (Reader Cardano.Api.ProtocolParameters) effs + , Member (Reader InstancesState) effs + ) + => MockServerConfig + -> Maybe ContractInstanceId + -> WalletEffect + ~> Eff effs +handleWalletClient config cidM event = do + let NetworkIdWrapper networkId = mscNetworkId config + protocolParams <- ask @Cardano.Api.ProtocolParameters + case event of + OwnPubKeyHash -> do + error "Cardano.Wallet.RemoteClient: OwnPubKeyHash not yet implemented" + + WalletAddSignature _ -> do + error "Cardano.Wallet.RemoteClient: WalletAddSignature not yet implemented" + + TotalFunds -> do + error "Cardano.Wallet.RemoteClient: TotalFunds not yet implemented" + + SubmitTxn _ -> do + error "Cardano.Wallet.RemoteClient: SubmitTxn not yet implemented" + + BalanceTx _ -> + error "Cardano.Wallet.BalanceTx: not yet implemented" + + YieldUnbalancedTx utx -> do + case export protocolParams networkId utx of + Left err -> throwError $ OtherError $ Text.pack $ show err + Right ex -> do + case cidM of + Nothing -> throwError $ OtherError "RemoteWalletClient: No contract instance id" + Just cid -> do + iss <- ask @InstancesState + liftIO $ STM.atomically $ do + is <- Instances.instanceState cid iss + STM.modifyTVar (Instances.issYieldedExportTxs is) (\txs -> txs ++ [ex]) diff --git a/plutus-pab/src/Cardano/Wallet/Types.hs b/plutus-pab/src/Cardano/Wallet/Types.hs new file mode 100644 index 0000000000..7640a75496 --- /dev/null +++ b/plutus-pab/src/Cardano/Wallet/Types.hs @@ -0,0 +1,53 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE TemplateHaskell #-} + +module Cardano.Wallet.Types + ( -- * Wallet configuration + WalletConfig (..) + , LocalWalletSettings (..) + , WalletUrl (..) + , defaultWalletConfig + -- * Lens and Prisms + , walletSettings + , baseUrlL + , _LocalWalletConfig + , _RemoteWalletConfig + ) where + +import Control.Lens (Lens', lens, makeLenses, makePrisms) +import Data.Aeson (FromJSON, ToJSON) +import Data.Default (Default (def)) +import GHC.Generics (Generic) +import Servant.Client (BaseUrl (..), Scheme (Http)) + +data WalletConfig = + LocalWalletConfig { _walletSettings :: LocalWalletSettings } + | RemoteWalletConfig { _walletSettings :: LocalWalletSettings } -- TODO: Remove wallet settings after refactoring the structure of the config file. + deriving (Show, Eq, Generic) + deriving anyclass (FromJSON, ToJSON) + +instance Default WalletConfig where + def = defaultWalletConfig + +defaultWalletConfig :: WalletConfig +defaultWalletConfig = + LocalWalletConfig $ LocalWalletSettings + -- See Note [pab-ports] in "test/full/Plutus/PAB/CliSpec.hs". + { baseUrl = WalletUrl $ BaseUrl Http "localhost" 9081 "" + } + +newtype LocalWalletSettings = LocalWalletSettings { baseUrl :: WalletUrl } + deriving (Show, Eq, Generic) + deriving anyclass (FromJSON, ToJSON) + +newtype WalletUrl = WalletUrl BaseUrl + deriving (Eq, Show, ToJSON, FromJSON) via BaseUrl + +baseUrlL :: Lens' LocalWalletSettings WalletUrl +baseUrlL = lens g s where + g = baseUrl + s settings url = settings { baseUrl = url } + +makeLenses ''WalletConfig +makePrisms ''WalletConfig diff --git a/plutus-pab/src/Plutus/PAB/App.hs b/plutus-pab/src/Plutus/PAB/App.hs index d2c0c3e4cb..07f1ad1c70 100644 --- a/plutus-pab/src/Plutus/PAB/App.hs +++ b/plutus-pab/src/Plutus/PAB/App.hs @@ -14,7 +14,6 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} -{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-partial-type-signatures #-} @@ -38,10 +37,13 @@ import Cardano.Node.Client (handleNodeClientClient) import Cardano.Node.Client qualified as NodeClient import Cardano.Node.Types (MockServerConfig (..), NodeMode (..)) import Cardano.Protocol.Socket.Mock.Client qualified as MockClient -import Cardano.Wallet.Client qualified as WalletClient +import Cardano.Wallet.LocalClient qualified as LocalWalletClient import Cardano.Wallet.Mock.Client qualified as WalletMockClient -import Cardano.Wallet.Mock.Types qualified as Wallet +import Cardano.Wallet.RemoteClient qualified as RemoteWalletClient +import Cardano.Wallet.Types (WalletConfig (LocalWalletConfig, RemoteWalletConfig)) +import Cardano.Wallet.Types qualified as Wallet import Control.Concurrent.STM qualified as STM +import Control.Lens ((^.)) import Control.Monad.Freer import Control.Monad.Freer.Error (Error, handleError, throwError) import Control.Monad.Freer.Extras.Beam (handleBeam) @@ -82,6 +84,7 @@ import Servant.Client (ClientEnv, ClientError, mkClientEnv) import Wallet.Effects (WalletEffect (..)) import Wallet.Emulator.Error (WalletAPIError) import Wallet.Emulator.Wallet (Wallet (..)) +import Wallet.Types (ContractInstanceId) ------------------------------------------------------------ @@ -154,7 +157,7 @@ appEffectHandlers storageBackend config trace BuiltinHandler{contractHandler} = . interpret (handleBeam (convertLog (SMultiAgent . BeamLogItem) trace)) . reinterpretN @'[_, _, _, _, _] handleContractDefinition - , handleServicesEffects = \wallet -> + , handleServicesEffects = \wallet cidM -> do -- handle 'NodeClientEffect' flip handleError (throwError . NodeClientError) . interpret (Core.handleUserEnvReader @(Builtin a) @(AppEnv a)) @@ -179,7 +182,8 @@ appEffectHandlers storageBackend config trace BuiltinHandler{contractHandler} = . reinterpret (Core.handleMappedReader @(AppEnv a) @ClientEnv walletClientEnv) . interpret (Core.handleUserEnvReader @(Builtin a) @(AppEnv a)) . reinterpret (Core.handleMappedReader @(AppEnv a) @ProtocolParameters protocolParameters) - . reinterpretN @'[_, _, _, _, _] (handleWalletEffect (nodeServerConfig config) wallet) + . interpret (Core.handleInstancesStateReader @(Builtin a) @(AppEnv a)) + . reinterpretN @'[_, _, _, _, _, _] (handleWalletEffect (walletServerConfig config) (nodeServerConfig config) cidM wallet) , onStartup = pure () @@ -194,13 +198,22 @@ handleWalletEffect , Member (Reader ClientEnv) effs , Member (Reader ProtocolParameters) effs , Member (LogMsg WalletClientMsg) effs + , Member (Reader InstancesState) effs ) - => MockServerConfig + => WalletConfig + -> MockServerConfig + -> Maybe ContractInstanceId -> Wallet -> WalletEffect ~> Eff effs -handleWalletEffect (mscNodeMode -> MockNode) = WalletMockClient.handleWalletClient @IO -handleWalletEffect config = WalletClient.handleWalletClient config +handleWalletEffect LocalWalletConfig {} MockServerConfig { mscNodeMode = MockNode } _ w eff = + WalletMockClient.handleWalletClient @IO w eff +handleWalletEffect LocalWalletConfig {} nodeCfg@MockServerConfig { mscNodeMode = AlonzoNode } _ w eff = + LocalWalletClient.handleWalletClient nodeCfg w eff +handleWalletEffect RemoteWalletConfig {} MockServerConfig { mscNodeMode = MockNode } _ _ _ = + error "Plutus.PAB.App.handleWalletEffect: Can't use remote wallet config with mock node." +handleWalletEffect RemoteWalletConfig {} nodeCfg@MockServerConfig { mscNodeMode = AlonzoNode } cidM _ eff = + RemoteWalletClient.handleWalletClient nodeCfg cidM eff runApp :: forall a b. @@ -215,7 +228,12 @@ runApp :: -> Config -- ^ Client configuration -> App a b -- ^ Action -> IO (Either PABError b) -runApp storageBackend trace contractHandler config@Config{pabWebserverConfig=WebserverConfig{endpointTimeout}} = Core.runPAB (Timeout endpointTimeout) (appEffectHandlers storageBackend config trace contractHandler) +runApp + storageBackend + trace + contractHandler + config@Config{pabWebserverConfig=WebserverConfig{endpointTimeout}} = + Core.runPAB (Timeout endpointTimeout) (appEffectHandlers storageBackend config trace contractHandler) type App a b = PABAction (Builtin a) (AppEnv a) b @@ -224,11 +242,11 @@ data StorageBackend = BeamSqliteBackend | InMemoryBackend mkEnv :: Trace IO (PABLogMsg (Builtin a)) -> Config -> IO (AppEnv a) mkEnv appTrace appConfig@Config { dbConfig - , nodeServerConfig = MockServerConfig{mscBaseUrl, mscSocketPath, mscSlotConfig, mscProtocolParametersJsonPath} + , nodeServerConfig = MockServerConfig{mscBaseUrl, mscSocketPath, mscSlotConfig, mscProtocolParametersJsonPath} , walletServerConfig , chainIndexConfig } = do - walletClientEnv <- clientEnv (Wallet.baseUrl walletServerConfig) + walletClientEnv <- clientEnv $ Wallet.baseUrl $ walletServerConfig ^. Wallet.walletSettings nodeClientEnv <- clientEnv mscBaseUrl chainIndexEnv <- clientEnv (ChainIndex.ciBaseUrl chainIndexConfig) dbConnection <- dbConnect appTrace dbConfig @@ -248,7 +266,8 @@ mkEnv appTrace appConfig@Config { dbConfig readPP path = do bs <- BSL.readFile path case eitherDecode bs of - Left err -> error $ "Error reading protocol parameters JSON file: " ++ show mscProtocolParametersJsonPath ++ " (" ++ err ++ ")" + Left err -> error $ "Error reading protocol parameters JSON file: " + ++ show mscProtocolParametersJsonPath ++ " (" ++ err ++ ")" Right params -> pure params diff --git a/plutus-pab/src/Plutus/PAB/Core.hs b/plutus-pab/src/Plutus/PAB/Core.hs index d50ffa2ce1..d766f2bb9c 100644 --- a/plutus-pab/src/Plutus/PAB/Core.hs +++ b/plutus-pab/src/Plutus/PAB/Core.hs @@ -60,6 +60,7 @@ module Plutus.PAB.Core , waitForTxOutStatusChange , activeEndpoints , waitForEndpoint + , yieldedExportTxs , currentSlot , waitUntilSlot , waitNSlots @@ -110,6 +111,7 @@ import Ledger.Value (Value) import Plutus.ChainIndex (ChainIndexQueryEffect, RollbackState (..), TxOutStatus, TxStatus) import Plutus.ChainIndex qualified as ChainIndex import Plutus.Contract.Effects (ActiveEndpoint (..), PABReq) +import Plutus.Contract.Wallet (ExportTx) import Plutus.PAB.Core.ContractInstance (ContractInstanceMsg, ContractInstanceState) import Plutus.PAB.Core.ContractInstance qualified as ContractInstance import Plutus.PAB.Core.ContractInstance.STM (Activity (Active), BlockchainEnv, InstancesState, OpenEndpoint (..)) @@ -250,11 +252,11 @@ activateContract' :: activateContract' state cid w contractDef = do PABRunner{runPABAction} <- pabRunner - let handler :: forall a. Eff (ContractInstanceEffects t env '[IO]) a -> IO a - handler x = fmap (either (error . show) id) (runPABAction $ handleAgentThread w x) + let handler :: forall a. ContractInstanceId -> Eff (ContractInstanceEffects t env '[IO]) a -> IO a + handler _ x = fmap (either (error . show) id) (runPABAction $ handleAgentThread w (Just cid) x) args :: ContractActivationArgs (ContractDef t) args = ContractActivationArgs{caWallet = Just w, caID = contractDef} - handleAgentThread w + handleAgentThread w (Just cid) $ ContractInstance.startContractInstanceThread' @t @IO @(ContractInstanceEffects t env '[IO]) state cid handler args -- | Start a new instance of a contract @@ -262,11 +264,11 @@ activateContract :: forall t env. PABContract t => Wallet -> ContractDef t -> PA activateContract w contractDef = do PABRunner{runPABAction} <- pabRunner - let handler :: forall a. Eff (ContractInstanceEffects t env '[IO]) a -> IO a - handler x = fmap (either (error . show) id) (runPABAction $ handleAgentThread w x) + let handler :: forall a. ContractInstanceId -> Eff (ContractInstanceEffects t env '[IO]) a -> IO a + handler cid x = fmap (either (error . show) id) (runPABAction $ handleAgentThread w (Just cid) x) args :: ContractActivationArgs (ContractDef t) args = ContractActivationArgs{caWallet = Just w, caID = contractDef} - handleAgentThread w + handleAgentThread w Nothing $ ContractInstance.activateContractSTM @t @IO @(ContractInstanceEffects t env '[IO]) handler args -- | Call a named endpoint on a contract instance. Waits if the endpoint is not @@ -329,10 +331,13 @@ callEndpointOnInstance' instanceID ep value = do $ STM.atomically $ Instances.callEndpointOnInstance state (EndpointDescription ep) (JSON.toJSON value) instanceID --- | Make a payment to a public key -payToPublicKey :: Wallet -> PubKeyHash -> Value -> PABAction t env CardanoTx -payToPublicKey source target amount = - handleAgentThread source +-- | Make a payment to a public key. +-- +-- Won't work in a remote wallet setting because we don't specify the contract +-- instance id. +payToPublicKey :: ContractInstanceId -> Wallet -> PubKeyHash -> Value -> PABAction t env CardanoTx +payToPublicKey cid source target amount = + handleAgentThread source (Just cid) $ Modify.wrapError WalletError $ WAPI.payToPublicKeyHash WAPI.defaultSlotRange amount target @@ -361,9 +366,10 @@ type ContractInstanceEffects t env effs = handleAgentThread :: forall t env a. Wallet + -> Maybe ContractInstanceId -> Eff (ContractInstanceEffects t env '[IO]) a -> PABAction t env a -handleAgentThread wallet action = do +handleAgentThread wallet cidM action = do PABEnvironment{effectHandlers, blockchainEnv, instancesState} <- ask @(PABEnvironment t env) let EffectHandlers{handleContractStoreEffect, handleContractEffect, handleServicesEffects} = effectHandlers let action' :: Eff (ContractInstanceEffects t env (IO ': PABEffects t env)) a = Modify.raiseEnd action @@ -381,7 +387,7 @@ handleAgentThread wallet action = do $ (interpret (mapLog @_ @(PABMultiAgentMsg t) EmulatorMsg) . reinterpret (timed @EmulatorEvent') . reinterpret (mapLog (WalletEvent wallet)) . reinterpret (mapLog RequestHandlerLog)) $ (interpret (mapLog @_ @(PABMultiAgentMsg t) EmulatorMsg) . reinterpret (timed @EmulatorEvent') . reinterpret (mapLog (WalletEvent wallet)) . reinterpret (mapLog TxBalanceLog)) $ handleUUIDEffect - $ handleServicesEffects wallet + $ handleServicesEffects wallet cidM $ handleContractStoreEffect $ handleContractEffect action' @@ -449,6 +455,7 @@ data EffectHandlers t env = , LastMember IO effs ) => Wallet + -> Maybe ContractInstanceId -> Eff (WalletEffect ': ChainIndexQueryEffect ': NodeClientEffect ': effs) ~> Eff effs @@ -486,7 +493,7 @@ timed = \case -- | Get the current state of the contract instance. instanceState :: forall t env. Wallet -> ContractInstanceId -> PABAction t env (Contract.State t) -instanceState wallet instanceId = handleAgentThread wallet (Contract.getState @t instanceId) +instanceState wallet instanceId = handleAgentThread wallet (Just instanceId) (Contract.getState @t instanceId) -- | An STM transaction that returns the observable state of the contract instance. observableState :: forall t env. ContractInstanceId -> PABAction t env (STM JSON.Value) @@ -530,6 +537,15 @@ waitForEndpoint instanceId endpointName = do eps <- tx guard $ any (\Instances.OpenEndpoint{Instances.oepName=ActiveEndpoint{aeDescription=EndpointDescription nm}} -> nm == endpointName) eps +-- | Get exported transactions waiting to be balanced, signed and submitted by +-- an external client. +yieldedExportTxs :: forall t env. ContractInstanceId -> PABAction t env [ExportTx] +yieldedExportTxs instanceId = do + instancesState <- asks @(PABEnvironment t env) instancesState + liftIO $ STM.atomically $ do + is <- Instances.instanceState instanceId instancesState + Instances.yieldedExportTxs is + currentSlot :: forall t env. PABAction t env (STM Slot) currentSlot = do Instances.BlockchainEnv{Instances.beCurrentSlot} <- asks @(PABEnvironment t env) blockchainEnv @@ -565,7 +581,7 @@ finalResult instanceId = do -- TODO: Change from 'Wallet' to 'Address' (see SCP-2208). valueAt :: Wallet -> PABAction t env Value valueAt wallet = do - handleAgentThread wallet $ do + handleAgentThread wallet Nothing $ do utxoRefs <- getAllUtxoRefs def txOutsM <- traverse ChainIndex.txOutFromRef utxoRefs pure $ foldMap (view ciTxOutValue) $ catMaybes txOutsM diff --git a/plutus-pab/src/Plutus/PAB/Core/ContractInstance.hs b/plutus-pab/src/Plutus/PAB/Core/ContractInstance.hs index f430eb6c06..aa082eb138 100644 --- a/plutus-pab/src/Plutus/PAB/Core/ContractInstance.hs +++ b/plutus-pab/src/Plutus/PAB/Core/ContractInstance.hs @@ -95,7 +95,7 @@ activateContractSTM' :: ) => ContractInstanceState t -> ContractInstanceId - -> (Eff appBackend ~> IO) + -> (ContractInstanceId -> Eff appBackend ~> IO) -> ContractActivationArgs (ContractDef t) -> Eff effs ContractInstanceId activateContractSTM' c@ContractInstanceState{contractState} activeContractInstanceId runAppBackend a@ContractActivationArgs{caID, caWallet} = do @@ -119,11 +119,12 @@ startContractInstanceThread' :: ) => ContractInstanceState t -> ContractInstanceId - -> (Eff appBackend ~> IO) + -> (ContractInstanceId -> Eff appBackend ~> IO) -> ContractActivationArgs (ContractDef t) -> Eff effs ContractInstanceId startContractInstanceThread' ContractInstanceState{stmState} activeContractInstanceId runAppBackend a = do - s <- startSTMInstanceThread' @t @m stmState runAppBackend a activeContractInstanceId + s <- startSTMInstanceThread' + @t @m stmState runAppBackend a activeContractInstanceId ask >>= void . liftIO . STM.atomically . InstanceState.insertInstance activeContractInstanceId s pure activeContractInstanceId @@ -140,7 +141,7 @@ activateContractSTM :: , LastMember m (Reader ContractInstanceId ': appBackend) , LastMember m effs ) - => (Eff appBackend ~> IO) + => (ContractInstanceId -> Eff appBackend ~> IO) -> ContractActivationArgs (ContractDef t) -> Eff effs ContractInstanceId activateContractSTM runAppBackend a = do @@ -263,6 +264,7 @@ stmRequestHandler = fmap sequence (wrapHandler (fmap pure nonBlockingRequests) < <> RequestHandler.handleOwnInstanceIdQueries @effs <> RequestHandler.handleCurrentSlotQueries @effs <> RequestHandler.handleCurrentTimeQueries @effs + <> RequestHandler.handleYieldedUnbalancedTx @effs -- requests that wait for changes to happen blockingRequests = @@ -283,7 +285,7 @@ startSTMInstanceThread' :: , LastMember m (Reader InstanceState ': Reader ContractInstanceId ': appBackend) ) => STM InstanceState - -> (Eff appBackend ~> IO) + -> (ContractInstanceId -> Eff appBackend ~> IO) -> ContractActivationArgs (ContractDef t) -> ContractInstanceId -> Eff effs InstanceState @@ -291,7 +293,7 @@ startSTMInstanceThread' stmState runAppBackend def instanceID = do state <- liftIO $ STM.atomically stmState _ <- liftIO $ forkIO - $ runAppBackend + $ runAppBackend instanceID $ runReader instanceID $ runReader state $ stmInstanceLoop @t @m @(Reader InstanceState ': Reader ContractInstanceId ': appBackend) def instanceID @@ -305,7 +307,7 @@ startSTMInstanceThread :: , AppBackendConstraints t m appBackend , LastMember m (Reader InstanceState ': Reader ContractInstanceId ': appBackend) ) - => (Eff appBackend ~> IO) + => (ContractInstanceId -> Eff appBackend ~> IO) -> ContractActivationArgs (ContractDef t) -> ContractInstanceId -> Eff effs InstanceState diff --git a/plutus-pab/src/Plutus/PAB/Core/ContractInstance/STM.hs b/plutus-pab/src/Plutus/PAB/Core/ContractInstance/STM.hs index 45cc182e1b..20aed0d30b 100644 --- a/plutus-pab/src/Plutus/PAB/Core/ContractInstance/STM.hs +++ b/plutus-pab/src/Plutus/PAB/Core/ContractInstance/STM.hs @@ -41,6 +41,7 @@ module Plutus.PAB.Core.ContractInstance.STM( , callEndpointOnInstance , callEndpointOnInstanceTimeout , observableContractState + , yieldedExportTxs , instanceState , instanceIDs , instancesWithStatuses @@ -68,6 +69,7 @@ import Plutus.ChainIndex.TxOutBalance (transactionOutputStatus) import Plutus.ChainIndex.UtxoState (UtxoIndex, UtxoState (..), utxoState) import Plutus.Contract.Effects (ActiveEndpoint (..)) import Plutus.Contract.Resumable (IterationID, Request (..), RequestID) +import Plutus.Contract.Wallet (ExportTx) import Wallet.Types (ContractInstanceId, EndpointDescription, EndpointValue (..), NotificationError (..)) import Wallet.Types qualified as Wallet (ContractActivityStatus (..)) @@ -195,12 +197,13 @@ data Activity = -- | The state of an active contract instance. data InstanceState = InstanceState - { issEndpoints :: TVar (Map (RequestID, IterationID) OpenEndpoint) -- ^ Open endpoints that can be responded to. - , issStatus :: TVar Activity -- ^ Whether the instance is still running. - , issObservableState :: TVar (Maybe Value) -- ^ Serialised observable state of the contract instance (if available) - , issStop :: TMVar () -- ^ Stop the instance if a value is written into the TMVar. - , issTxOutRefs :: TVar (Map (RequestID, IterationID) OpenTxOutSpentRequest) - , issAddressRefs :: TVar (Map (RequestID, IterationID) OpenTxOutProducedRequest) + { issEndpoints :: TVar (Map (RequestID, IterationID) OpenEndpoint) -- ^ Open endpoints that can be responded to. + , issStatus :: TVar Activity -- ^ Whether the instance is still running. + , issObservableState :: TVar (Maybe Value) -- ^ Serialised observable state of the contract instance (if available) + , issStop :: TMVar () -- ^ Stop the instance if a value is written into the TMVar. + , issTxOutRefs :: TVar (Map (RequestID, IterationID) OpenTxOutSpentRequest) + , issAddressRefs :: TVar (Map (RequestID, IterationID) OpenTxOutProducedRequest) + , issYieldedExportTxs :: TVar [ExportTx] -- ^ Partial tx that need to be balanced, signed and submitted by an external client. } -- | An 'InstanceState' value with empty fields @@ -213,6 +216,7 @@ emptyInstanceState = <*> STM.newEmptyTMVar <*> STM.newTVar mempty <*> STM.newTVar mempty + <*> STM.newTVar mempty -- | Events that the contract instances are waiting for, indexed by keys that are -- readily available in the node client (ie. that can be produced from just a @@ -337,6 +341,10 @@ callEndpointOnInstance' notAvailable (InstancesState m) endpointDescription valu [ep] -> callEndpoint ep (EndpointValue value) >> pure Nothing _ -> pure $ Just $ MoreThanOneEndpointAvailable instanceID endpointDescription +-- | The list of all partial txs that need to be balanced on the instance. +yieldedExportTxs :: InstanceState -> STM [ExportTx] +yieldedExportTxs = STM.readTVar . issYieldedExportTxs + -- | State of all contract instances that are currently running newtype InstancesState = InstancesState { getInstancesState :: TVar (Map ContractInstanceId InstanceState) } diff --git a/plutus-pab/src/Plutus/PAB/Run/Cli.hs b/plutus-pab/src/Plutus/PAB/Run/Cli.hs index 7a31720f0d..bbe99f3c3f 100644 --- a/plutus-pab/src/Plutus/PAB/Run/Cli.hs +++ b/plutus-pab/src/Plutus/PAB/Run/Cli.hs @@ -28,7 +28,8 @@ import Cardano.ChainIndex.Server qualified as ChainIndex import Cardano.Node.Server qualified as NodeServer import Cardano.Node.Types (MockServerConfig (..), NodeMode (..)) import Cardano.Wallet.Mock.Server qualified as WalletServer -import Cardano.Wallet.Mock.Types +import Cardano.Wallet.Mock.Types (WalletMsg) +import Cardano.Wallet.Types import Control.Concurrent (takeMVar) import Control.Concurrent.Async (Async, async, waitAny) import Control.Concurrent.Availability (Availability, available, starting) @@ -119,16 +120,20 @@ runConfigCommand _ ConfigCommandArgs{ccaTrace, ccaPABConfig=Config{dbConfig}} Mi App.migrate (toPABMsg ccaTrace) dbConfig -- Run mock wallet service -runConfigCommand _ ConfigCommandArgs{ccaTrace, ccaPABConfig = Config {nodeServerConfig, chainIndexConfig, walletServerConfig},ccaAvailability} MockWallet = +runConfigCommand _ ConfigCommandArgs{ccaTrace, ccaPABConfig = Config {nodeServerConfig, chainIndexConfig, walletServerConfig = LocalWalletConfig ws},ccaAvailability} MockWallet = liftIO $ WalletServer.main (toWalletLog ccaTrace) - walletServerConfig + ws (mscFeeConfig nodeServerConfig) (mscSocketPath nodeServerConfig) (mscSlotConfig nodeServerConfig) (ChainIndex.ciBaseUrl chainIndexConfig) ccaAvailability +-- Run mock wallet service +runConfigCommand _ ConfigCommandArgs{ccaPABConfig = Config {walletServerConfig = RemoteWalletConfig _}} MockWallet = + error "Plutus.PAB.Run.Cli.runConfigCommand: Can't run mock wallet in remote wallet config." + -- Run mock node server runConfigCommand _ ConfigCommandArgs{ccaTrace, ccaPABConfig = Config {nodeServerConfig},ccaAvailability} StartMockNode = case mscNodeMode nodeServerConfig of diff --git a/plutus-pab/src/Plutus/PAB/Simulator.hs b/plutus-pab/src/Plutus/PAB/Simulator.hs index 813189180b..c114328005 100644 --- a/plutus-pab/src/Plutus/PAB/Simulator.hs +++ b/plutus-pab/src/Plutus/PAB/Simulator.hs @@ -264,9 +264,10 @@ handleServicesSimulator :: => FeeConfig -> SlotConfig -> Wallet + -> Maybe ContractInstanceId -> Eff (WalletEffect ': ChainIndexQueryEffect ': NodeClientEffect ': effs) ~> Eff effs -handleServicesSimulator feeCfg slotCfg wallet = +handleServicesSimulator feeCfg slotCfg wallet _ = let makeTimedChainIndexEvent wllt = interpret (mapLog @_ @(PABMultiAgentMsg t) EmulatorMsg) . reinterpret (Core.timed @EmulatorEvent') @@ -719,6 +720,7 @@ blockchain = do handleAgentThread :: forall t a. Wallet + -> Maybe ContractInstanceId -> Eff (Core.ContractInstanceEffects t (SimulatorState t) '[IO]) a -> Simulation t a handleAgentThread = Core.handleAgentThread @@ -741,7 +743,7 @@ addWallet = do currentWallets <- STM.readTVar _agentStates let newWallets = currentWallets & at (Wallet.toMockWallet mockWallet) ?~ AgentState (Wallet.fromMockWallet mockWallet) mempty STM.writeTVar _agentStates newWallets - _ <- handleAgentThread (knownWallet 2) + _ <- handleAgentThread (knownWallet 2) Nothing $ Modify.wrapError WalletError $ MockWallet.distributeNewWalletFunds (CW.pubKeyHash mockWallet) pure (Wallet.toMockWallet mockWallet, CW.pubKeyHash mockWallet) @@ -777,6 +779,6 @@ payToWallet source target = payToPublicKeyHash source (Emulator.walletPubKeyHash -- | Make a payment from one wallet to a public key address payToPublicKeyHash :: forall t. Wallet -> PubKeyHash -> Value -> Simulation t CardanoTx payToPublicKeyHash source target amount = - handleAgentThread source + handleAgentThread source Nothing $ flip (handleError @WAPI.WalletAPIError) (throwError . WalletError) $ WAPI.payToPublicKeyHash WAPI.defaultSlotRange amount target diff --git a/plutus-pab/src/Plutus/PAB/Types.hs b/plutus-pab/src/Plutus/PAB/Types.hs index 3f10bb6331..12dd262e65 100644 --- a/plutus-pab/src/Plutus/PAB/Types.hs +++ b/plutus-pab/src/Plutus/PAB/Types.hs @@ -13,7 +13,7 @@ module Plutus.PAB.Types where import Cardano.ChainIndex.Types qualified as ChainIndex import Cardano.Node.Types (MockServerConfig (..)) -import Cardano.Wallet.Mock.Types qualified as Wallet +import Cardano.Wallet.Types qualified as Wallet import Control.Lens.TH (makePrisms) import Control.Monad.Freer.Extras.Beam (BeamError) import Data.Aeson (FromJSON, ToJSON (..)) diff --git a/plutus-pab/src/Plutus/PAB/Webserver/API.hs b/plutus-pab/src/Plutus/PAB/Webserver/API.hs index 037ebd5bb4..2146792f45 100644 --- a/plutus-pab/src/Plutus/PAB/Webserver/API.hs +++ b/plutus-pab/src/Plutus/PAB/Webserver/API.hs @@ -1,5 +1,4 @@ {-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} @@ -21,7 +20,7 @@ import Servant.API.WebSocket (WebSocketPending) import Servant.Swagger.UI (SwaggerSchemaUI) import Wallet.Types (ContractInstanceId) -type WalletProxy walletId = "wallet" :> (Wallet.API walletId) +type WalletProxy walletId = "wallet" :> Wallet.API walletId type WSAPI = "ws" :> diff --git a/plutus-pab/src/Plutus/PAB/Webserver/Handler.hs b/plutus-pab/src/Plutus/PAB/Webserver/Handler.hs index 7fa05e7a09..5c46610cfe 100644 --- a/plutus-pab/src/Plutus/PAB/Webserver/Handler.hs +++ b/plutus-pab/src/Plutus/PAB/Webserver/Handler.hs @@ -43,6 +43,7 @@ import Ledger (Value) import Ledger.Constraints.OffChain (UnbalancedTx) import Ledger.Tx (Tx) import Plutus.Contract.Effects (PABReq, _ExposeEndpointReq) +import Plutus.Contract.Wallet (ExportTx) import Plutus.PAB.Core (PABAction) import Plutus.PAB.Core qualified as Core import Plutus.PAB.Effects.Contract qualified as Contract @@ -114,22 +115,24 @@ apiHandler = swagger :: forall t api dir. (Servant.Server api ~ Servant.Handler JSON.Value, ToSchema (Contract.ContractDef t)) => Servant.Server (SwaggerSchemaUI' dir api) swagger = swaggerSchemaUIServer $ toOpenApi (Proxy @(API (Contract.ContractDef t) Integer)) -fromInternalState :: - t +fromInternalState + :: t -> ContractInstanceId -> ContractActivityStatus - -> Maybe Wallet + -> Wallet + -> [ExportTx] -> PartiallyDecodedResponse PABReq -> ContractInstanceClientState t -fromInternalState t i s wallet resp = +fromInternalState t i s wallet yieldedExportTxs resp = ContractInstanceClientState { cicContract = i , cicCurrentState = let hks' = mapMaybe (traverse (preview _ExposeEndpointReq)) (hooks resp) in resp { hooks = hks' } - , cicWallet = fromMaybe (knownWallet 1) wallet + , cicWallet = wallet , cicDefinition = t , cicStatus = s + , cicYieldedExportTxs = yieldedExportTxs } -- HANDLERS @@ -138,13 +141,21 @@ activateContract :: forall t env. Contract.PABContract t => ContractActivationAr activateContract ContractActivationArgs{caID, caWallet} = do Core.activateContract (fromMaybe (knownWallet 1) caWallet) caID -contractInstanceState :: forall t env. Contract.PABContract t => ContractInstanceId -> PABAction t env (ContractInstanceClientState (Contract.ContractDef t)) +contractInstanceState + :: forall t env. Contract.PABContract t + => ContractInstanceId + -> PABAction t env (ContractInstanceClientState (Contract.ContractDef t)) contractInstanceState i = do definition <- Contract.getDefinition @t i instWithStatuses <- Core.instancesWithStatuses case (definition, Map.lookup i instWithStatuses) of - (Just ContractActivationArgs{caWallet, caID}, Just s) -> - fromInternalState caID i s caWallet . fromResp . Contract.serialisableState (Proxy @t) <$> Contract.getState @t i + (Just ContractActivationArgs{caWallet, caID}, Just s) -> do + let wallet = fromMaybe (knownWallet 1) caWallet + yieldedExportedTxs <- Core.yieldedExportTxs i + fmap ( fromInternalState caID i s wallet yieldedExportedTxs + . fromResp + . Contract.serialisableState (Proxy @t) + ) $ Contract.getState @t i _ -> throwError @PABError (ContractInstanceNotFound i) callEndpoint :: forall t env. ContractInstanceId -> String -> JSON.Value -> PABAction t env () @@ -155,13 +166,21 @@ instancesForWallets wallet mStatus = filter ((==) (Wallet wallet) . cicWallet) < allInstanceStates :: forall t env. Contract.PABContract t => Maybe Text -> PABAction t env [ContractInstanceClientState (Contract.ContractDef t)] allInstanceStates mStatus = do + instWithStatuses <- Core.instancesWithStatuses let mActivityStatus = join $ parseContractActivityStatus <$> mStatus + isInstanceStatusMatch s = maybe True ((==) s) mActivityStatus + getStatus (i, args) = (i, args,) <$> Map.lookup i instWithStatuses + get (i, ContractActivationArgs{caWallet, caID}, s) = + let wallet = fromMaybe (knownWallet 1) caWallet + in do + yieldedExportedTxs <- Core.yieldedExportTxs i + fmap ( fromInternalState caID i s wallet yieldedExportedTxs + . fromResp + . Contract.serialisableState (Proxy @t) + ) $ Contract.getState @t i mp <- Contract.getContracts @t mActivityStatus - instWithStatuses <- Core.instancesWithStatuses - let isInstanceStatusMatch s = maybe True ((==) s) mActivityStatus - let getStatus (i, args) = (i, args,) <$> Map.lookup i instWithStatuses - let get (i, ContractActivationArgs{caWallet, caID}, s) = fromInternalState caID i s caWallet . fromResp . Contract.serialisableState (Proxy @t) <$> Contract.getState @t i - filter (isInstanceStatusMatch . cicStatus) <$> traverse get (mapMaybe getStatus $ Map.toList mp) + filter (isInstanceStatusMatch . cicStatus) + <$> traverse get (mapMaybe getStatus $ Map.toList mp) availableContracts :: forall t env. Contract.PABContract t => PABAction t env [ContractSignatureResponse (Contract.ContractDef t)] availableContracts = do @@ -206,13 +225,13 @@ walletProxy :: :<|> (WalletId -> Tx -> PABAction t env Tx)) walletProxy createNewWallet = createNewWallet - :<|> (\w tx -> fmap (const NoContent) (Core.handleAgentThread (Wallet w) $ Wallet.Effects.submitTxn $ Right tx)) + :<|> (\w tx -> fmap (const NoContent) (Core.handleAgentThread (Wallet w) Nothing $ Wallet.Effects.submitTxn $ Right tx)) :<|> (\w -> (\pkh -> WalletInfo{wiWallet=Wallet w, wiPubKeyHash = pkh }) - <$> Core.handleAgentThread (Wallet w) Wallet.Effects.ownPubKeyHash) + <$> Core.handleAgentThread (Wallet w) Nothing Wallet.Effects.ownPubKeyHash) :<|> (\w -> fmap (fmap (fromRight (error "Plutus.PAB.Webserver.Handler: Expecting a mock tx, not an Alonzo tx when submitting it."))) - . Core.handleAgentThread (Wallet w) . Wallet.Effects.balanceTx) - :<|> (\w -> Core.handleAgentThread (Wallet w) Wallet.Effects.totalFunds) + . Core.handleAgentThread (Wallet w) Nothing . Wallet.Effects.balanceTx) + :<|> (\w -> Core.handleAgentThread (Wallet w) Nothing Wallet.Effects.totalFunds) :<|> (\w tx -> fmap (fromRight (error "Plutus.PAB.Webserver.Handler: Expecting a mock tx, not an Alonzo tx when adding a signature.")) - $ Core.handleAgentThread (Wallet w) + $ Core.handleAgentThread (Wallet w) Nothing $ Wallet.Effects.walletAddSignature $ Right tx) diff --git a/plutus-pab/src/Plutus/PAB/Webserver/Server.hs b/plutus-pab/src/Plutus/PAB/Webserver/Server.hs index dd97e54da4..0bc0f85b05 100644 --- a/plutus-pab/src/Plutus/PAB/Webserver/Server.hs +++ b/plutus-pab/src/Plutus/PAB/Webserver/Server.hs @@ -79,15 +79,15 @@ app :: app fp walletClient pabRunner = do let apiServer :: ServerT (CombinedAPI t) Handler apiServer = - (Servant.hoistServer + Servant.hoistServer (Proxy @(BaseCombinedAPI t)) (asHandler pabRunner) - (apiHandler :<|> WS.wsHandler)) :<|> (swagger @t) + (apiHandler :<|> WS.wsHandler) :<|> (swagger @t) case fp of Nothing -> do let wp = either walletProxyClientEnv walletProxy walletClient - rest = Proxy @(CombinedAPI t :<|> (WalletProxy WalletId)) + rest = Proxy @(CombinedAPI t :<|> WalletProxy WalletId) wpServer = Servant.hoistServer (Proxy @(WalletProxy WalletId)) @@ -103,7 +103,7 @@ app fp walletClient pabRunner = do wp fileServer :: ServerT Raw Handler fileServer = serveDirectoryFileServer filePath - rest = Proxy @(CombinedAPI t :<|> (WalletProxy WalletId) :<|> Raw) + rest = Proxy @(CombinedAPI t :<|> WalletProxy WalletId :<|> Raw) Servant.serve rest (apiServer :<|> wpServer :<|> fileServer) -- | Start the server using the config. Returns an action that shuts it down diff --git a/plutus-pab/src/Plutus/PAB/Webserver/Types.hs b/plutus-pab/src/Plutus/PAB/Webserver/Types.hs index e8a3981ab2..16022fa915 100644 --- a/plutus-pab/src/Plutus/PAB/Webserver/Types.hs +++ b/plutus-pab/src/Plutus/PAB/Webserver/Types.hs @@ -20,6 +20,7 @@ import Ledger.Index (UtxoIndex) import Ledger.Slot (Slot) import Playground.Types (FunctionSchema) import Plutus.Contract.Effects (ActiveEndpoint, PABReq) +import Plutus.Contract.Wallet (ExportTx) import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse) import Prettyprinter (Pretty, pretty, (<+>)) import Schema (FormSchema) @@ -89,6 +90,7 @@ data ContractInstanceClientState t = , cicWallet :: Wallet , cicDefinition :: t , cicStatus :: ContractActivityStatus + , cicYieldedExportTxs :: [ExportTx] } deriving stock (Eq, Show, Generic) deriving anyclass (ToJSON, FromJSON) @@ -99,6 +101,7 @@ deriving instance OpenApi.ToSchema t => OpenApi.ToSchema (ContractInstanceClient data InstanceStatusToClient = NewObservableState JSON.Value -- ^ The observable state of the contract has changed. | NewActiveEndpoints [ActiveEndpoint] -- ^ The set of active endpoints has changed. + | NewYieldedExportTxs [ExportTx] -- ^ Partial txs that need to be balanced, signed and submitted by an external client. | ContractFinished (Maybe JSON.Value) -- ^ Contract instance is done with an optional error message. deriving stock (Generic, Eq, Show) deriving anyclass (ToJSON, FromJSON) diff --git a/plutus-pab/src/Plutus/PAB/Webserver/WebSocket.hs b/plutus-pab/src/Plutus/PAB/Webserver/WebSocket.hs index dbbd1cc8e3..2e2e34905c 100644 --- a/plutus-pab/src/Plutus/PAB/Webserver/WebSocket.hs +++ b/plutus-pab/src/Plutus/PAB/Webserver/WebSocket.hs @@ -47,6 +47,7 @@ import Ledger.Slot (Slot) import Network.WebSockets qualified as WS import Network.WebSockets.Connection (Connection, PendingConnection) import Plutus.Contract.Effects (ActiveEndpoint (..)) +import Plutus.Contract.Wallet (ExportTx) import Plutus.PAB.Core (PABAction) import Plutus.PAB.Core qualified as Core import Plutus.PAB.Core.ContractInstance.STM (BlockchainEnv, InstancesState, OpenEndpoint (..)) @@ -108,6 +109,12 @@ openEndpoints contractInstanceId instancesState = do instanceState <- Instances.instanceState contractInstanceId instancesState fmap (oepName . snd) . Map.toList <$> Instances.openEndpoints instanceState +yieldedExportTxsChange :: ContractInstanceId -> InstancesState -> STMStream [ExportTx] +yieldedExportTxsChange contractInstanceId instancesState = + unfold $ do + instanceState <- Instances.instanceState contractInstanceId instancesState + Instances.yieldedExportTxs instanceState + finalValue :: ContractInstanceId -> InstancesState -> STMStream (Maybe JSON.Value) finalValue contractInstanceId instancesState = singleton $ Instances.finalResult contractInstanceId instancesState @@ -118,6 +125,7 @@ instanceUpdates instanceId instancesState = fold [ NewObservableState <$> observableStateChange instanceId instancesState , NewActiveEndpoints <$> openEndpoints instanceId instancesState + , NewYieldedExportTxs <$> yieldedExportTxsChange instanceId instancesState , ContractFinished <$> finalValue instanceId instancesState ] diff --git a/plutus-pab/test/full/Plutus/PAB/CliSpec.hs b/plutus-pab/test/full/Plutus/PAB/CliSpec.hs index 3daff7027a..f3711b679e 100644 --- a/plutus-pab/test/full/Plutus/PAB/CliSpec.hs +++ b/plutus-pab/test/full/Plutus/PAB/CliSpec.hs @@ -24,10 +24,11 @@ import Cardano.Node.Types (NodeMode (..)) import Cardano.Node.Types qualified as Node.Types import Cardano.Wallet.Mock.Client qualified as Wallet.Client import Cardano.Wallet.Mock.Types (WalletInfo (..)) -import Cardano.Wallet.Mock.Types qualified as Wallet.Types +import Cardano.Wallet.Types qualified as Wallet.Types import Control.Concurrent (threadDelay) import Control.Concurrent.Async (async, cancel) import Control.Concurrent.Availability (available, newToken, starting) +import Control.Lens (over) import Control.Monad (forM_, void, when) import Data.Aeson (FromJSON, ToJSON, toJSON) import Data.Coerce (coerce) @@ -113,7 +114,7 @@ bumpConfig -> Config -- ^ Config to bump. -> Config -- ^ Bumped config! bumpConfig x dbName conf@Config{ pabWebserverConfig = p@PAB.Types.WebserverConfig{PAB.Types.baseUrl=p_u} - , walletServerConfig = w@Wallet.Types.WalletConfig{Wallet.Types.baseUrl=w_u} + , walletServerConfig , nodeServerConfig = n@Node.Types.MockServerConfig{Node.Types.mscBaseUrl=n_u,Node.Types.mscSocketPath=soc} , chainIndexConfig = c@ChainIndex.Types.ChainIndexConfig{ChainIndex.Types.ciBaseUrl=c_u} , dbConfig = db@PAB.Types.DbConfig{PAB.Types.dbConfigFile=dbFile} @@ -122,7 +123,7 @@ bumpConfig x dbName conf@Config{ pabWebserverConfig = p@PAB.Types.WebserverCon bump (BaseUrl scheme url port path) = BaseUrl scheme url (port + x) path newConf = conf { pabWebserverConfig = p { PAB.Types.baseUrl = bump p_u } - , walletServerConfig = w { Wallet.Types.baseUrl = coerce $ bump $ coerce w_u } + , walletServerConfig = over (Wallet.Types.walletSettings . Wallet.Types.baseUrlL) (coerce . bump . coerce) walletServerConfig , nodeServerConfig = n { Node.Types.mscBaseUrl = bump n_u, Node.Types.mscSocketPath = soc ++ "." ++ show x } , chainIndexConfig = c { ChainIndex.Types.ciBaseUrl = coerce $ bump $ coerce c_u } , dbConfig = db { PAB.Types.dbConfigFile = "file::" <> dbName <> "?mode=memory&cache=shared" } diff --git a/plutus-pab/test/full/Plutus/PAB/CoreSpec.hs b/plutus-pab/test/full/Plutus/PAB/CoreSpec.hs index 6108c96230..50a9ffe249 100644 --- a/plutus-pab/test/full/Plutus/PAB/CoreSpec.hs +++ b/plutus-pab/test/full/Plutus/PAB/CoreSpec.hs @@ -325,17 +325,19 @@ guessingGameTest = runScenario $ do let openingBalance = 100_000_000_000 lockAmount = 15_000_000 - pubKeyHashFundsChange msg delta = do - address <- pubKeyHashAddress <$> Simulator.handleAgentThread defaultWallet ownPubKeyHash + pubKeyHashFundsChange cid msg delta = do + address <- pubKeyHashAddress <$> Simulator.handleAgentThread defaultWallet (Just cid) ownPubKeyHash balance <- Simulator.valueAt address fees <- Simulator.walletFees defaultWallet assertEqual msg (openingBalance + delta) (valueOf (balance <> fees) adaSymbol adaToken) + + instanceId <- Simulator.activateContract defaultWallet GameStateMachine + initialTxCounts <- Simulator.txCounts - pubKeyHashFundsChange "Check our opening balance." 0 + pubKeyHashFundsChange instanceId "Check our opening balance." 0 -- need to add contract address to wallet's watched addresses - instanceId <- Simulator.activateContract defaultWallet GameStateMachine assertTxCounts "Activating the game does not generate transactions." @@ -350,7 +352,7 @@ guessingGameTest = assertTxCounts "Locking the game state machine should produce two transactions" (initialTxCounts & Simulator.txValidated +~ 2) - pubKeyHashFundsChange "Locking the game should reduce our balance." (negate lockAmount) + pubKeyHashFundsChange instanceId "Locking the game should reduce our balance." (negate lockAmount) game1Id <- Simulator.activateContract defaultWallet GameStateMachine guess @@ -378,7 +380,7 @@ guessingGameTest = assertTxCounts "A correct guess creates a third transaction." (initialTxCounts & Simulator.txValidated +~ 3) - pubKeyHashFundsChange "The wallet should now have its money back." 0 + pubKeyHashFundsChange instanceId "The wallet should now have its money back." 0 blocks <- Simulator.blockchain assertBool "We have some confirmed blocks in this test." diff --git a/plutus-pab/test/light/Cardano/Wallet/RemoteClientSpec.hs b/plutus-pab/test/light/Cardano/Wallet/RemoteClientSpec.hs new file mode 100644 index 0000000000..4d8a683eb8 --- /dev/null +++ b/plutus-pab/test/light/Cardano/Wallet/RemoteClientSpec.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TupleSections #-} + +module Cardano.Wallet.RemoteClientSpec + ( tests + ) where + +import Test.QuickCheck.Arbitrary (Arbitrary, arbitrary) +import Test.Tasty (TestTree, testGroup) +import Control.Monad.Freer (runM, interpret) +import Control.Monad.Freer.Reader (runReader) +import Plutus.Contract (WalletAPIError) +import Control.Monad.Freer.Error (runError) +import Cardano.Wallet.RemoteClient (handleWalletClient) +import Data.Default (Default(def)) +import Plutus.PAB.Core.ContractInstance.STM (InstancesState, emptyInstancesState, emptyInstanceState, insertInstance, instanceState, yieldedExportTxs) +import Cardano.Api.ProtocolParameters (ProtocolParameters) +import Gen.Cardano.Api.Typed qualified as Gen +import Ledger.Constraints.OffChain (emptyUnbalancedTx) +import Wallet.Effects (WalletEffect (..)) +import Hedgehog qualified +import Test.Tasty.Hedgehog (testProperty) +import Hedgehog (Property, (===)) +import Control.Monad.IO.Class (liftIO) +import Control.Concurrent.STM qualified as STM +import Wallet.Emulator.Error (WalletAPIError (OtherError)) +import Wallet.Types (ContractInstanceId, randomID) +import Data.List qualified as List + +tests :: TestTree +tests = testGroup "Cardano.Wallet.RemoteClient" + [ testGroup "yieldUnbalancedTx" + [ testProperty "should put partial tx in contract instance state" yieldToInstanceState + , testProperty "should throw error when no contract instance id is provided" yieldNoCid + ] + ] + +-- | Verify that a yielded unbalanced (partial) tx will appear in the contract's +-- instance status. +yieldToInstanceState :: Property +yieldToInstanceState = Hedgehog.property $ do + pp <- Hedgehog.forAll Gen.genProtocolParameters + cid <- liftIO randomID + + let utx = emptyUnbalancedTx + result <- liftIO $ do + iss <- STM.atomically $ do + iss <- emptyInstancesState + is <- emptyInstanceState + insertInstance cid is iss + pure iss + yieldedRes <- runRemoteWalletEffects pp iss (Just cid) (YieldUnbalancedTx utx) + pure $ fmap (,iss) yieldedRes + + case result of + Left _ -> Hedgehog.assert False + Right ((), iss) -> do + txs <- liftIO $ STM.atomically $ instanceState cid iss >>= yieldedExportTxs + List.length txs === 1 + +-- | An error should be thrown when no contract instance id is provided. +yieldNoCid :: Property +yieldNoCid = Hedgehog.property $ do + pp <- Hedgehog.forAll Gen.genProtocolParameters + result <- liftIO $ do + iss <- STM.atomically emptyInstancesState + runRemoteWalletEffects pp iss Nothing (YieldUnbalancedTx emptyUnbalancedTx) + case result of + Left (OtherError _) -> Hedgehog.assert True + _ -> Hedgehog.assert False + +-- | Run the wallet effects in a remote wallet scenario. +runRemoteWalletEffects + :: ProtocolParameters + -> InstancesState + -> Maybe ContractInstanceId + -> WalletEffect () + -> IO (Either WalletAPIError ()) +runRemoteWalletEffects protocolParams is cidM action = do + runM + $ runError @WalletAPIError + $ runReader protocolParams + $ runReader is + $ handleWalletClient def cidM action diff --git a/plutus-pab/test/light/Spec.hs b/plutus-pab/test/light/Spec.hs index 2ec864014a..de3471c57a 100644 --- a/plutus-pab/test/light/Spec.hs +++ b/plutus-pab/test/light/Spec.hs @@ -4,6 +4,7 @@ module Main import Cardano.Api.NetworkId.ExtraSpec qualified import Cardano.Wallet.ServerSpec qualified +import Cardano.Wallet.RemoteClientSpec qualified import Control.Concurrent.STM.ExtrasSpec qualified import Test.Tasty (defaultMain, testGroup) @@ -13,6 +14,7 @@ main = testGroup "all tests" [ Cardano.Api.NetworkId.ExtraSpec.tests + , Cardano.Wallet.RemoteClientSpec.tests , Cardano.Wallet.ServerSpec.tests , Control.Concurrent.STM.ExtrasSpec.tests ]