diff --git a/cabal.project b/cabal.project index cbce595..22bab83 100644 --- a/cabal.project +++ b/cabal.project @@ -42,9 +42,10 @@ source-repository-package source-repository-package type: git location: https://github.com/j-mueller/sc-tools - tag: 956eb259e22d5a73fa5f67bc8aceec5df144d170 + tag: e2759559324e172f12b11ab815323c48ed8922b0 subdir: src/devnet + src/blockfrost src/coin-selection src/mockchain src/optics diff --git a/nix/project.nix b/nix/project.nix index fe107e8..46438a2 100644 --- a/nix/project.nix +++ b/nix/project.nix @@ -2,7 +2,7 @@ let sha256map = { - "https://github.com/j-mueller/sc-tools"."956eb259e22d5a73fa5f67bc8aceec5df144d170" = "sha256-5qc4MbB6GuQyjA+Y+tlqixf6UvhyDgEBgj23aKgpSAg="; + "https://github.com/j-mueller/sc-tools"."e2759559324e172f12b11ab815323c48ed8922b0" = "sha256-5qc4MbB6GuQyjA+Y+tlqixf6UvhyDgEBgj23aKgpSAg="; "https://github.com/colll78/plutarch-plutus"."b2379767c7f1c70acf28206bf922f128adc02f28" = "sha256-mhuW2CHxnc6FDWuMcjW/51PKuPOdYc4yxz+W5RmlQew="; "https://github.com/input-output-hk/catalyst-onchain-libs"."650a3435f8efbd4bf36e58768fac266ba5beede4" = "sha256-NUh+l97+eO27Ppd8Bx0yMl0E5EV+p7+7GuFun1B8gRc="; }; diff --git a/src/lib/SmartTokens/Types/ProtocolParams.hs b/src/lib/SmartTokens/Types/ProtocolParams.hs index cddae7f..bdc8dbb 100644 --- a/src/lib/SmartTokens/Types/ProtocolParams.hs +++ b/src/lib/SmartTokens/Types/ProtocolParams.hs @@ -1,22 +1,29 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-deferred-type-errors #-} +{-# LANGUAGE InstanceSigs #-} module SmartTokens.Types.ProtocolParams ( ProgrammableLogicGlobalParams (..), PProgrammableLogicGlobalParams (..), ) where -import Plutarch.Core.PlutusDataList - ( DerivePConstantViaDataList(..), - PlutusTypeDataList, - ProductIsData(..) ) +import Cardano.Api.Shelley qualified as C +import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) +import Data.Aeson qualified as Aeson +import Data.Bifunctor (Bifunctor (..)) import Generics.SOP qualified as SOP -import Plutarch.LedgerApi.V3 (PCurrencySymbol, PCredential) -import Plutarch.Prelude +import Plutarch.Core.PlutusDataList (DerivePConstantViaDataList (..), + PlutusTypeDataList, ProductIsData (..)) import Plutarch.DataRepr (PDataFields) -import PlutusTx qualified -import PlutusLedgerApi.V3 (Credential, CurrencySymbol) +import Plutarch.LedgerApi.V3 (PCredential, PCurrencySymbol) import Plutarch.Lift (PConstantDecl, PUnsafeLiftDecl (PLifted)) +import Plutarch.Prelude (DerivePlutusType (..), Generic, PDataRecord, PEq, + PIsData, PLabeledType ((:=)), PShow, PlutusType, S, + Term) +import PlutusLedgerApi.V3 (Credential, CurrencySymbol) +import PlutusTx qualified -- TODO: -- Figure out why deriving PlutusType breaks when I uncomment this @@ -48,4 +55,28 @@ instance DerivePlutusType PProgrammableLogicGlobalParams where type DPTStrat _ = PlutusTypeDataList instance PUnsafeLiftDecl PProgrammableLogicGlobalParams where - type PLifted PProgrammableLogicGlobalParams = ProgrammableLogicGlobalParams \ No newline at end of file + type PLifted PProgrammableLogicGlobalParams = ProgrammableLogicGlobalParams + +-- We're using the Data representation of the PlutusLedgerApi types here +-- Because it is somewhat human-readable (more so than the hex representation) + +plutusDataToJSON :: forall a. (PlutusTx.ToData a) => a -> Aeson.Value +plutusDataToJSON = C.scriptDataToJson C.ScriptDataJsonNoSchema . C.unsafeHashableScriptData . C.fromPlutusData . PlutusTx.toData + +plutusDataFromJSON :: forall a. (PlutusTx.FromData a) => Aeson.Value -> Either String a +plutusDataFromJSON val = do + k <- bimap show C.getScriptData $ C.scriptDataFromJson C.ScriptDataJsonNoSchema val + maybe (Left "fromData failed") Right (PlutusTx.fromData $ C.toPlutusData k) + +instance ToJSON ProgrammableLogicGlobalParams where + toJSON ProgrammableLogicGlobalParams{directoryNodeCS, progLogicCred} = + object + [ "directory_node_currency_symbol" .= plutusDataToJSON directoryNodeCS + , "programmable_logic_credential" .= plutusDataToJSON progLogicCred + ] + +instance FromJSON ProgrammableLogicGlobalParams where + parseJSON = withObject "ProgrammableLogicGlobalParams" $ \obj -> + ProgrammableLogicGlobalParams + <$> (obj .: "directory_node_currency_symbol" >>= either fail pure . plutusDataFromJSON) + <*> (obj .: "programmable_logic_credential" >>= either fail pure . plutusDataFromJSON) diff --git a/src/lib/Wst/App.hs b/src/lib/Wst/App.hs new file mode 100644 index 0000000..256581e --- /dev/null +++ b/src/lib/Wst/App.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-| Application monad used by CLI and server +-} +module Wst.App ( + WstApp(..), + runWstApp, + runWstAppServant +) where + +import Blammo.Logging.Simple (MonadLogger, MonadLoggerIO, WithLogger (..)) +import Cardano.Api qualified as C +import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError) +import Control.Monad.IO.Class (MonadIO (..)) +import Control.Monad.Reader (MonadReader, ReaderT, runReaderT) +import Convex.Blockfrost (BlockfrostT (..), evalBlockfrostT) +import Convex.Class (MonadBlockchain, MonadUtxoQuery) +import Data.String (IsString (..)) +import Servant.Server (Handler (..)) +import Servant.Server qualified as S +import Wst.AppError (AppError (BlockfrostErr)) +import Wst.Offchain.Env (RuntimeEnv (..)) +import Wst.Offchain.Env qualified as Env + +newtype WstApp env era a = WstApp { unWstApp :: ReaderT env (ExceptT (AppError era) (BlockfrostT IO)) a } + deriving newtype (Monad, Applicative, Functor, MonadIO, MonadReader env, MonadError (AppError era), MonadUtxoQuery, MonadBlockchain C.ConwayEra) + deriving + (MonadLogger, MonadLoggerIO) + via (WithLogger env (ExceptT (AppError era) (BlockfrostT IO))) + +runWstApp :: forall env era a. (Env.HasRuntimeEnv env) => env -> WstApp env era a -> IO (Either (AppError era) a) +runWstApp env WstApp{unWstApp} = do + let RuntimeEnv{envBlockfrost} = Env.runtimeEnv env + evalBlockfrostT envBlockfrost (runExceptT (runReaderT unWstApp env)) >>= \case + Left e -> pure (Left $ BlockfrostErr e) + Right a -> pure a + +{-| Interpret the 'WstApp' in a servant handler +-} +runWstAppServant :: forall env era a. (Env.HasRuntimeEnv env) => env -> WstApp env era a -> Handler a +runWstAppServant env action = liftIO (runWstApp env action) >>= \case + Left err -> do + let err_ = S.err500 { S.errBody = fromString (show err) } + throwError err_ + Right a -> pure a diff --git a/src/lib/Wst/AppError.hs b/src/lib/Wst/AppError.hs new file mode 100644 index 0000000..50de554 --- /dev/null +++ b/src/lib/Wst/AppError.hs @@ -0,0 +1,15 @@ +{-| Error type for endpoints and queries +-} +module Wst.AppError( + AppError(..) +) where + +import Blockfrost.Client.Core (BlockfrostError) +import Convex.CoinSelection qualified as CoinSelection + +data AppError era = + OperatorNoUTxOs -- ^ The operator does not have any UTxOs + | GlobalParamsNodeNotFound -- ^ The node with the global parameters was not found + | BalancingError (CoinSelection.BalanceTxError era) + | BlockfrostErr BlockfrostError + deriving stock (Show) diff --git a/src/lib/Wst/Cli.hs b/src/lib/Wst/Cli.hs index fe7ebde..5535ac3 100644 --- a/src/lib/Wst/Cli.hs +++ b/src/lib/Wst/Cli.hs @@ -1,20 +1,18 @@ -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Wst.Cli(runMain) where -import Blammo.Logging.Simple (MonadLogger, MonadLoggerIO, WithLogger (..), - logError, logInfo, runLoggerLoggingT) -import Control.Monad.Except (ExceptT, MonadError, runExceptT) +import Blammo.Logging.Simple (MonadLogger, logError, logInfo, runLoggerLoggingT) import Control.Monad.IO.Class (MonadIO (..)) -import Control.Monad.Reader (MonadReader, ReaderT, runReaderT) import Convex.Wallet.Operator (OperatorConfigSigning) import Convex.Wallet.Operator qualified as Operator import Data.String (IsString (..)) import Options.Applicative (customExecParser, disambiguate, helper, idm, info, prefs, showHelpOnEmpty, showHelpOnError) -import Wst.Cli.Command (Command (..), parseCommand) -import Wst.Cli.RuntimeEnv (RuntimeEnv) -import Wst.Cli.RuntimeEnv qualified as RuntimeEnv +import Wst.App (runWstApp) +import Wst.Cli.Command (Command (..), ManageCommand (StartServer, Status), + parseCommand) +import Wst.Offchain.Env qualified as Env +import Wst.Server qualified as Server runMain :: IO () runMain = do @@ -25,30 +23,23 @@ runMain = do runCommand :: Command -> IO () runCommand com = do - env <- RuntimeEnv.loadEnv - result <- runWstApp env $ case com of - Deploy config -> deploy config - Manage _txIn _com -> - -- TODO: - -- * Implement status check (call the query endpoints and print out a summary of the results) - -- * Start the server - logInfo "Manage" + env <- Env.addRuntimeEnv <$> Env.loadRuntimeEnv <*> pure Env.empty + result <- case com of + Deploy config -> runWstApp env (deploy config) + Manage txIn com -> do + let env' = Env.addDirectoryEnvFor txIn env + runWstApp env' $ case com of + Status -> do + -- TODO: status check (call the query endpoints and print out a summary of the results) + logInfo "Manage" + StartServer -> do + logInfo "starting server" + liftIO (Server.runServer env') + case result of Left err -> runLoggerLoggingT env $ logError (fromString $ show err) Right a -> pure a -data AppError = AppError - deriving stock Show - -newtype WstApp a = WstApp { unWstApp :: ReaderT RuntimeEnv (ExceptT AppError IO) a } - deriving newtype (Monad, Applicative, Functor, MonadIO, MonadReader RuntimeEnv, MonadError AppError) - deriving - (MonadLogger, MonadLoggerIO) - via (WithLogger RuntimeEnv (ExceptT AppError IO)) - -runWstApp :: RuntimeEnv -> WstApp a -> IO (Either AppError a) -runWstApp env WstApp{unWstApp} = runExceptT (runReaderT unWstApp env) - deploy :: (MonadLogger m, MonadIO m) => OperatorConfigSigning -> m () deploy config = do logInfo "Loading operator files" diff --git a/src/lib/Wst/Cli/RuntimeEnv.hs b/src/lib/Wst/Cli/RuntimeEnv.hs deleted file mode 100644 index 51e7464..0000000 --- a/src/lib/Wst/Cli/RuntimeEnv.hs +++ /dev/null @@ -1,39 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -{-| Data that we need when running the CLI --} -module Wst.Cli.RuntimeEnv( - RuntimeEnv(..), - loadEnv, -) where - -import Blammo.Logging (Logger) -import Blammo.Logging.Logger (HasLogger (..), newLogger) -import Blammo.Logging.LogSettings.Env qualified as LogSettingsEnv -import Blockfrost.Auth (mkProject) -import Blockfrost.Client.Auth qualified as Blockfrost -import Control.Lens (makeLensesFor) -import Data.Text qualified as Text -import System.Environment qualified - -data RuntimeEnv - = RuntimeEnv - { envLogger :: Logger - , envBlockfrost :: Blockfrost.Project - - } - -makeLensesFor - [ ("envLogger", "logger") - , ("envBlockfrostProject", "blockfrostProject") - ] - 'RuntimeEnv - -instance HasLogger RuntimeEnv where - loggerL = logger - --- | Load the 'RuntimeEnv' from environment variables -loadEnv :: IO RuntimeEnv -loadEnv = - RuntimeEnv - <$> (LogSettingsEnv.parse >>= newLogger) - <*> fmap (mkProject . Text.pack) (System.Environment.getEnv "WST_BLOCKFROST_TOKEN") diff --git a/src/lib/Wst/Client.hs b/src/lib/Wst/Client.hs index a1d448f..6ff262c 100644 --- a/src/lib/Wst/Client.hs +++ b/src/lib/Wst/Client.hs @@ -4,51 +4,35 @@ -} module Wst.Client ( getHealthcheck, - postInitMerkleTree, - postUpdateMerkleTree, - postTransferToUser, - postTransferToIssuer, - getAddress, - getAllSanctionedAddresses + + -- * Query routes + getGlobalParams, + + -- * Build tx + postIssueProgrammableTokenTx, ) where +import Cardano.Api qualified as C import Data.Data (Proxy (..)) import Servant.API (NoContent, (:<|>) ((:<|>))) import Servant.Client (ClientEnv, client, runClientM) import Servant.Client.Core (ClientError) -import Wst.Server.Types (API) +import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams) +import Wst.Offchain.Query (UTxODat) +import Wst.Server.Types (API, APIInEra, IssueProgrammableTokenArgs (..), + TextEnvelopeJSON) getHealthcheck :: ClientEnv -> IO (Either ClientError NoContent) getHealthcheck env = do - let healthcheck :<|> _ = client (Proxy @API) + let healthcheck :<|> _ = client (Proxy @APIInEra) runClientM healthcheck env -postInitMerkleTree :: ClientEnv -> String -> IO (Either ClientError String) -postInitMerkleTree env name = do - let _ :<|> initMerkleTree :<|> _ = client (Proxy @API) - runClientM (initMerkleTree name) env - -postUpdateMerkleTree :: ClientEnv -> String -> IO (Either ClientError String) -postUpdateMerkleTree env name = do - let _ :<|> _ :<|> updateMerkleTree :<|> _ = client (Proxy @API) - runClientM (updateMerkleTree name) env - -postTransferToUser :: ClientEnv -> String -> IO (Either ClientError String) -postTransferToUser env name = do - let _ :<|> _ :<|> _ :<|> transferToUser :<|> _ = client (Proxy @API) - runClientM (transferToUser name) env - -postTransferToIssuer :: ClientEnv -> String -> IO (Either ClientError String) -postTransferToIssuer env name = do - let _ :<|> _ :<|> _ :<|> _ :<|> transferToIssuer :<|> _ = client (Proxy @API) - runClientM (transferToIssuer name) env - -getAddress :: ClientEnv -> String -> IO (Either ClientError String) -getAddress env name = do - let _ :<|> _ :<|> _ :<|> _ :<|> address :<|> _ = client (Proxy @API) - runClientM (address name) env +getGlobalParams :: forall era. C.IsShelleyBasedEra era => ClientEnv -> IO (Either ClientError (UTxODat era ProgrammableLogicGlobalParams)) +getGlobalParams env = do + let _ :<|> globalParams :<|> _ = client (Proxy @(API era)) + runClientM globalParams env -getAllSanctionedAddresses :: ClientEnv -> IO (Either ClientError String) -getAllSanctionedAddresses env = do - let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> allSanctionedAddresses = client (Proxy @API) - runClientM allSanctionedAddresses env +postIssueProgrammableTokenTx :: forall era. C.IsShelleyBasedEra era => ClientEnv -> IssueProgrammableTokenArgs -> IO (Either ClientError (TextEnvelopeJSON (C.Tx era))) +postIssueProgrammableTokenTx env args = do + let _ :<|> _ :<|> issueProgrammableTokenTx = client (Proxy @(API era)) + runClientM (issueProgrammableTokenTx args) env diff --git a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs index 6e74ca6..b422559 100644 --- a/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs +++ b/src/lib/Wst/Offchain/BuildTx/TransferLogic.hs @@ -114,7 +114,7 @@ seizeStablecoins seizingTxo issuerTxo directoryList destinationCred = Utils.inBa addIssueWitness :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => m () addIssueWitness = Utils.inBabbage @era $ do - opPkh <- asks (C.verificationKeyHash . verificationKey . oPaymentKey . Env.bteOperator . Env.operatorEnv) + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) mintingScript <- asks (Env.tleMintingScript . Env.transferLogicEnv) let sh = C.hashScript $ C.PlutusScript C.PlutusScriptV3 mintingScript addRequiredSignature opPkh @@ -153,7 +153,7 @@ addTransferWitness blacklistNodes clientCred = Utils.inBabbage @era $ do addSeizeWitness :: forall env era m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasTransferLogicEnv env, C.IsBabbageBasedEra era, MonadBlockchain era m, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadBuildTx era m) => m () addSeizeWitness = Utils.inBabbage @era $ do - opPkh <- asks (C.verificationKeyHash . verificationKey . oPaymentKey . Env.bteOperator . Env.operatorEnv) + opPkh <- asks (fst . Env.bteOperator . Env.operatorEnv) seizeScript <- asks (Env.tleIssuerScript . Env.transferLogicEnv) let sh = C.hashScript $ C.PlutusScript C.PlutusScriptV3 seizeScript addRequiredSignature opPkh diff --git a/src/lib/Wst/Offchain/Endpoints/Deployment.hs b/src/lib/Wst/Offchain/Endpoints/Deployment.hs index 1a4c483..7b7fd81 100644 --- a/src/lib/Wst/Offchain/Endpoints/Deployment.hs +++ b/src/lib/Wst/Offchain/Endpoints/Deployment.hs @@ -9,39 +9,40 @@ module Wst.Offchain.Endpoints.Deployment( import Cardano.Api (Quantity) import Cardano.Api.Shelley qualified as C import Control.Monad.Except (MonadError) -import Control.Monad.Reader (MonadReader) +import Control.Monad.Reader (MonadReader, asks) import Convex.BuildTx qualified as BuildTx import Convex.Class (MonadBlockchain, MonadUtxoQuery) import Convex.CoinSelection qualified +import Wst.AppError (AppError) import Wst.Offchain.BuildTx.DirectorySet (InsertNodeArgs) import Wst.Offchain.BuildTx.DirectorySet qualified as BuildTx import Wst.Offchain.BuildTx.ProgrammableLogic qualified as BuildTx import Wst.Offchain.BuildTx.ProtocolParams qualified as BuildTx -import Wst.Offchain.Env (BuildTxError) import Wst.Offchain.Env qualified as Env import Wst.Offchain.Query qualified as Query {-| Build a transaction that deploys the directory and global params. Returns the transaction and the 'TxIn' that was selected for the one-shot NFTs. -} -deployTx :: (MonadReader env m, Env.HasOperatorEnv era env, MonadBlockchain era m, MonadError (BuildTxError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era) => m (C.Tx era, C.TxIn) +deployTx :: (MonadReader env m, Env.HasOperatorEnv era env, MonadBlockchain era m, MonadError (AppError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era) => m (C.Tx era, C.TxIn) deployTx = do (txi, _) <- Env.selectOperatorOutput - (tx, _) <- Env.withDirectoryFor txi + opEnv <- asks Env.operatorEnv + (tx, _) <- Env.withEnv $ Env.withOperator opEnv $ Env.withDirectoryFor txi $ Env.balanceTxEnv $ BuildTx.mintProtocolParams >> BuildTx.initDirectorySet pure (Convex.CoinSelection.signBalancedTxBody [] tx, txi) {-| Build a transaction that inserts a node into the directory -} -insertNodeTx :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasDirectoryEnv env, MonadBlockchain era m, MonadError (BuildTxError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadUtxoQuery m) => InsertNodeArgs -> m (C.Tx era) +insertNodeTx :: forall era env m. (MonadReader env m, Env.HasOperatorEnv era env, Env.HasDirectoryEnv env, MonadBlockchain era m, MonadError (AppError era) m, C.IsBabbageBasedEra era, C.HasScriptLanguageInEra C.PlutusScriptV3 era, MonadUtxoQuery m) => InsertNodeArgs -> m (C.Tx era) insertNodeTx args = do -- 1. Find the head node -- FIXME: Error handling. And how can we actually identify the head node if the query returns more than one? headNode <- head <$> Query.registryNodes @era -- 2. Find the global parameter node - paramsNode <- head <$> Query.globalParamsNode @era + paramsNode <- Query.globalParamsNode @era (tx, _) <- Env.balanceTxEnv (BuildTx.insertDirectoryNode paramsNode headNode args) pure (Convex.CoinSelection.signBalancedTxBody [] tx) @@ -52,7 +53,7 @@ issueProgrammableTokenTx :: forall era env m. , Env.HasOperatorEnv era env , Env.HasDirectoryEnv env , MonadBlockchain era m - , MonadError (BuildTxError era) m + , MonadError (AppError era) m , C.IsBabbageBasedEra era , C.HasScriptLanguageInEra C.PlutusScriptV3 era , MonadUtxoQuery m @@ -63,7 +64,7 @@ issueProgrammableTokenTx :: forall era env m. -> m (C.Tx era) issueProgrammableTokenTx issueTokenArgs assetName quantity = do directory <- Query.registryNodes @era - paramsNode <- head <$> Query.globalParamsNode @era + paramsNode <- Query.globalParamsNode @era (tx, _) <- Env.balanceTxEnv $ do _ <- BuildTx.issueProgrammableToken paramsNode (assetName, quantity) issueTokenArgs directory diff --git a/src/lib/Wst/Offchain/Env.hs b/src/lib/Wst/Offchain/Env.hs index ffaf8d9..4e598da 100644 --- a/src/lib/Wst/Offchain/Env.hs +++ b/src/lib/Wst/Offchain/Env.hs @@ -1,6 +1,7 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} {-| Transaction building environment -} @@ -8,8 +9,8 @@ module Wst.Offchain.Env( -- * Operator environment HasOperatorEnv(..), OperatorEnv(..), - loadEnv, - BuildTxError(..), + loadOperatorEnv, + loadOperatorEnvFromAddress, -- ** Using the operator environment selectOperatorOutput, @@ -30,14 +31,35 @@ module Wst.Offchain.Env( TransferLogicEnv(..), HasTransferLogicEnv(..), + -- * Runtime data + RuntimeEnv(..), + HasRuntimeEnv(..), + loadRuntimeEnv, + -- * Combined environment CombinedEnv(..), - withDirectoryFor + empty, + withEnv, + addDirectoryEnvFor, + addDirectoryEnv, + withDirectory, + withDirectoryFor, + addRuntimeEnv, + withRuntime, + addOperatorEnv, + withOperator ) where +import Blammo.Logging (Logger) +import Blammo.Logging.Logger (HasLogger (..), newLogger) +import Blammo.Logging.LogSettings.Env qualified as LogSettingsEnv +import Blockfrost.Auth (mkProject) +import Blockfrost.Client.Auth qualified as Blockfrost import Cardano.Api (PlutusScript, PlutusScriptV3, UTxO) import Cardano.Api qualified as C import Cardano.Api.Shelley qualified as C +import Control.Lens (makeLensesFor) +import Control.Lens qualified as L import Control.Monad.Except (MonadError, throwError) import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT) import Convex.BuildTx (BuildTxT) @@ -49,12 +71,15 @@ import Convex.PlutusLedger.V1 (transCredential, transPolicyId) import Convex.Utils (mapError) import Convex.Utxos (BalanceChanges) import Convex.Utxos qualified as Utxos -import Convex.Wallet.Operator (Operator (..), PaymentExtendedKey (..), - Verification, operatorPaymentCredential, - operatorReturnOutput, verificationKey) +import Convex.Wallet.Operator (returnOutputFor) +import Data.Functor.Identity (Identity (..)) import Data.Map qualified as Map import Data.Maybe (listToMaybe) +import Data.Proxy (Proxy (..)) +import Data.Text qualified as Text import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams (..)) +import System.Environment qualified +import Wst.AppError (AppError (..)) import Wst.Offchain.Scripts (blacklistMintingScript, blacklistSpendingScript, directoryNodeMintingScript, directoryNodeSpendingScript, @@ -62,7 +87,6 @@ import Wst.Offchain.Scripts (blacklistMintingScript, blacklistSpendingScript, permissionedTransferScript, programmableLogicBaseScript, programmableLogicGlobalScript, - programmableLogicMintingScript, protocolParamsMintingScript, scriptPolicyIdV3) {-| Environments that have an 'OperatorEnv' @@ -77,42 +101,41 @@ instance HasOperatorEnv era (OperatorEnv era) where -} data OperatorEnv era = OperatorEnv - { bteOperator :: Operator Verification + { bteOperator :: (C.Hash C.PaymentKey, C.StakeAddressReference) -- ^ Payment and stake credential, used for generating return outputs , bteOperatorUtxos :: UTxO era -- ^ UTxOs owned by the operator, available for spending } -{-| Populate the 'OperatorEnv' with UTxOs locked by the verification key +{-| Populate the 'OperatorEnv' with UTxOs locked by the payment credential -} -loadEnv :: (MonadUtxoQuery m, C.IsBabbageBasedEra era) => C.VerificationKey C.PaymentKey -> Maybe (C.VerificationKey C.StakeKey) -> m (OperatorEnv era) -loadEnv verificationKey oStakeKey = do - let bteOperator - = Operator - { oPaymentKey = PEVerification verificationKey - , oStakeKey - } - bteOperatorUtxos <- Utxos.toApiUtxo <$> utxosByPaymentCredential (operatorPaymentCredential bteOperator) +loadOperatorEnv :: (MonadUtxoQuery m, C.IsBabbageBasedEra era) => C.Hash C.PaymentKey -> C.StakeAddressReference -> m (OperatorEnv era) +loadOperatorEnv paymentCredential stakeCredential = do + let bteOperator = (paymentCredential, stakeCredential) + bteOperatorUtxos <- Utxos.toApiUtxo <$> utxosByPaymentCredential (C.PaymentCredentialByKey paymentCredential) pure OperatorEnv{bteOperator, bteOperatorUtxos} -data BuildTxError era = - OperatorNoUTxOs -- ^ The operator does not have any UTxOs - | BalancingError (CoinSelection.BalanceTxError era) - deriving stock (Show) +loadOperatorEnvFromAddress :: (MonadUtxoQuery m, C.IsBabbageBasedEra era) => C.Address C.ShelleyAddr -> m (OperatorEnv era) +loadOperatorEnvFromAddress = \case + (C.ShelleyAddress _ntw (C.fromShelleyPaymentCredential -> C.PaymentCredentialByKey pmt) stakeRef) -> + loadOperatorEnv pmt (C.fromShelleyStakeReference stakeRef) + _ -> error "Expected public key address" -- FIXME: proper error {-| Select an output owned by the operator -} -selectOperatorOutput :: (MonadReader env m, HasOperatorEnv era env, MonadError (BuildTxError era) m) => m (C.TxIn, C.TxOut C.CtxUTxO era) +selectOperatorOutput :: (MonadReader env m, HasOperatorEnv era env, MonadError (AppError era) m) => m (C.TxIn, C.TxOut C.CtxUTxO era) selectOperatorOutput = asks (listToMaybe . Map.toList . C.unUTxO . bteOperatorUtxos . operatorEnv) >>= \case Nothing -> throwError OperatorNoUTxOs Just k -> pure k {-| Balance a transaction using the operator's funds and return output -} -balanceTxEnv :: forall era env a m. (MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError (BuildTxError era) m, C.IsBabbageBasedEra era) => BuildTxT era m a -> m (C.BalancedTxBody era, BalanceChanges) +balanceTxEnv :: forall era env a m. (MonadBlockchain era m, MonadReader env m, HasOperatorEnv era env, MonadError (AppError era) m, C.IsBabbageBasedEra era) => BuildTxT era m a -> m (C.BalancedTxBody era, BalanceChanges) balanceTxEnv btx = do OperatorEnv{bteOperatorUtxos, bteOperator} <- asks operatorEnv params <- queryProtocolParameters txBuilder <- BuildTx.execBuildTxT $ btx >> BuildTx.setMinAdaDepositAll params - output <- operatorReturnOutput bteOperator + -- TODO: change returnOutputFor to consider the stake address reference + -- (needs to be done in sc-tools) + output <- returnOutputFor (C.PaymentCredentialByKey $ fst bteOperator) mapError BalancingError (CoinSelection.balanceTx mempty output (Utxos.fromApiUtxo bteOperatorUtxos) txBuilder CoinSelection.TrailingChange) class HasDirectoryEnv e where @@ -205,26 +228,114 @@ mkTransferLogicEnv cred = , tleIssuerScript = permissionedTransferScript cred } -data CombinedEnv era = +data RuntimeEnv + = RuntimeEnv + { envLogger :: Logger + , envBlockfrost :: Blockfrost.Project + } + +makeLensesFor + [ ("envLogger", "logger") + , ("envBlockfrostProject", "blockfrostProject") + ] + 'RuntimeEnv + +instance HasLogger RuntimeEnv where + loggerL = logger + +-- | Load the 'RuntimeEnv' from environment variables +loadRuntimeEnv :: IO RuntimeEnv +loadRuntimeEnv = + RuntimeEnv + <$> (LogSettingsEnv.parse >>= newLogger) + <*> fmap (mkProject . Text.pack) (System.Environment.getEnv "WST_BLOCKFROST_TOKEN") + +class HasRuntimeEnv e where + runtimeEnv :: e -> RuntimeEnv + +instance HasRuntimeEnv RuntimeEnv where + runtimeEnv = id + +data CombinedEnv operatorF directoryF runtimeF era = CombinedEnv - { ceOperator :: OperatorEnv era - , ceDirectory :: DirectoryEnv + { ceOperator :: operatorF (OperatorEnv era) + , ceDirectory :: directoryF DirectoryEnv + , ceRuntime :: runtimeF RuntimeEnv } -instance HasOperatorEnv era (CombinedEnv era) where - operatorEnv = ceOperator +makeLensesFor + [("ceRuntime", "runtime")] + ''CombinedEnv + +{-| 'CombinedEnv' with no values +-} +empty :: forall era. CombinedEnv Proxy Proxy Proxy era +empty = + CombinedEnv + { ceOperator = Proxy + , ceDirectory = Proxy + , ceRuntime = Proxy + } + +instance HasOperatorEnv era (CombinedEnv Identity d r era) where + operatorEnv = runIdentity . ceOperator + +instance HasDirectoryEnv (CombinedEnv o Identity r era) where + directoryEnv = runIdentity . ceDirectory -instance HasDirectoryEnv (CombinedEnv era) where - directoryEnv = ceDirectory +instance HasTransferLogicEnv (CombinedEnv Identity d r era) where + transferLogicEnv = mkTransferLogicEnv . fst . bteOperator . operatorEnv -instance HasTransferLogicEnv (CombinedEnv era) where - transferLogicEnv = mkTransferLogicEnv . C.verificationKeyHash . verificationKey . oPaymentKey . bteOperator . ceOperator +instance HasRuntimeEnv (CombinedEnv o d Identity era) where + runtimeEnv = runIdentity . ceRuntime -{-| Add a 'DirectoryEnv' to the environment +_Identity :: L.Iso' (Identity a) a +_Identity = L.iso runIdentity Identity + +instance HasLogger (CombinedEnv o d Identity era) where + loggerL = runtime . _Identity . loggerL + +{-| Add a 'DirectoryEnv' for the 'C.TxIn' in to the environment +-} +addDirectoryEnvFor :: C.TxIn -> CombinedEnv o d r era -> CombinedEnv o Identity r era +addDirectoryEnvFor txi = addDirectoryEnv (mkDirectoryEnv txi) + +{-| Add a 'DirectoryEnv' for the 'C.TxIn' in to the environment -} -withDirectoryFor :: (MonadReader env m, HasOperatorEnv era env) => C.TxIn -> ReaderT (CombinedEnv era) m a -> m a -withDirectoryFor txi action = do - asks (CombinedEnv . operatorEnv) <*> pure (mkDirectoryEnv txi) +addDirectoryEnv :: DirectoryEnv -> CombinedEnv o d r era -> CombinedEnv o Identity r era +addDirectoryEnv de env = + env{ceDirectory = Identity de } + +withDirectory :: MonadReader (CombinedEnv o d r era) m => DirectoryEnv -> ReaderT (CombinedEnv o Identity r era) m a -> m a +withDirectory dir action = do + asks (addDirectoryEnv dir) >>= runReaderT action +withDirectoryFor :: MonadReader (CombinedEnv o d r era) m => C.TxIn -> ReaderT (CombinedEnv o Identity r era) m a -> m a +withDirectoryFor txi = withDirectory (mkDirectoryEnv txi) + +{-| Add a 'DirectoryEnv' for the 'C.TxIn' in to the environment and run the +action with the modified environment +-} +withEnv :: forall era m a. ReaderT (CombinedEnv Proxy Proxy Proxy era) m a -> m a +withEnv = flip runReaderT empty + +{-| Add a 'RuntimeEnv' to the environment +-} +addRuntimeEnv :: RuntimeEnv -> CombinedEnv o d r era -> CombinedEnv o d Identity era +addRuntimeEnv env e = + e{ceRuntime = Identity env } + +withRuntime :: MonadReader (CombinedEnv o d r era) m => RuntimeEnv -> ReaderT (CombinedEnv o d Identity era) m a -> m a +withRuntime runtime action = + asks (addRuntimeEnv runtime) + >>= runReaderT action + +{-| Add an 'OperatorEnv' to the environment +-} +addOperatorEnv :: OperatorEnv era -> CombinedEnv o d r era2 -> CombinedEnv Identity d r era +addOperatorEnv op e = + e{ceOperator = Identity op } +withOperator :: MonadReader (CombinedEnv o d r era1) m => OperatorEnv era -> ReaderT (CombinedEnv Identity d r era) m a -> m a +withOperator op action = asks (addOperatorEnv op) >>= runReaderT action diff --git a/src/lib/Wst/Offchain/Query.hs b/src/lib/Wst/Offchain/Query.hs index ad9c840..a5b244c 100644 --- a/src/lib/Wst/Offchain/Query.hs +++ b/src/lib/Wst/Offchain/Query.hs @@ -15,16 +15,20 @@ module Wst.Offchain.Query( import Cardano.Api qualified as C import Control.Lens qualified as L import Control.Monad ((>=>)) +import Control.Monad.Except (MonadError, throwError) import Control.Monad.Reader (MonadReader, asks) import Convex.CardanoApi.Lenses qualified as L import Convex.Class (MonadUtxoQuery, utxosByPaymentCredential) import Convex.Scripts (fromHashableScriptData) import Convex.Utxos (UtxoSet, toApiUtxo) +import Data.Aeson (FromJSON, ToJSON) import Data.Map qualified as Map -import Data.Maybe (mapMaybe) +import Data.Maybe (listToMaybe, mapMaybe) +import GHC.Generics (Generic) import PlutusTx qualified import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams) import SmartTokens.Types.PTokenDirectory (DirectorySetNode (..)) +import Wst.AppError (AppError (GlobalParamsNodeNotFound)) import Wst.Offchain.Env (DirectoryEnv (dsDirectorySpendingScript, dsProgrammableLogicBaseScript), HasDirectoryEnv (directoryEnv)) import Wst.Offchain.Scripts (protocolParamsSpendingScript) @@ -39,6 +43,8 @@ data UTxODat era a = , uOut :: C.TxOut C.CtxUTxO era , uDatum :: a } + deriving stock (Eq, Show, Generic) + deriving anyclass (ToJSON, FromJSON) {-| Find all UTxOs that make up the registry -} @@ -49,10 +55,11 @@ registryNodes = {-| Find the UTxO with the global params -} -globalParamsNode :: forall era m. (MonadUtxoQuery m, C.IsBabbageBasedEra era) => m [UTxODat era ProgrammableLogicGlobalParams] +globalParamsNode :: forall era m. (MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadError (AppError era) m) => m (UTxODat era ProgrammableLogicGlobalParams) globalParamsNode = do let cred = C.PaymentCredentialByScript . C.hashScript . C.PlutusScript C.PlutusScriptV3 $ protocolParamsSpendingScript - fmap (extractUTxO @era) (utxosByPaymentCredential cred) + utxosByPaymentCredential cred + >>= maybe (throwError GlobalParamsNodeNotFound) pure . listToMaybe . extractUTxO @era {-| Outputs that are locked by the programmable logic base script. -} diff --git a/src/lib/Wst/Server.hs b/src/lib/Wst/Server.hs index 993cc58..1612da0 100644 --- a/src/lib/Wst/Server.hs +++ b/src/lib/Wst/Server.hs @@ -1,33 +1,66 @@ +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} {-| servant server for stablecoin POC -} module Wst.Server(runServer) where +import Cardano.Api qualified as C +import Control.Monad.Except (MonadError) +import Control.Monad.Reader (MonadReader, asks) +import Convex.Class (MonadBlockchain, MonadUtxoQuery) import Data.Data (Proxy (..)) import Network.Wai.Handler.Warp qualified as Warp -import Servant (Server) -import Servant.API ((:<|>) (..)) -import Servant.Server (serve) -import Wst.Server.Endpoints (healthcheck, initMerkleTree, queryAddress, - queryAllSanctionedAddresses, transferToIssuer, - transferToUser, updateMerkleTree) -import Wst.Server.Types (API) - -server :: Server API -server = - healthcheck - :<|> initMerkleTree - :<|> updateMerkleTree - :<|> transferToUser - :<|> transferToIssuer - :<|> queryAddress - :<|> queryAllSanctionedAddresses - -runServer :: IO () -runServer = do - let app = serve (Proxy @API) server +import Servant (Server, ServerT) +import Servant.API (NoContent (..), (:<|>) (..)) +import Servant.Server (hoistServer, serve) +import Wst.App (WstApp, runWstAppServant) +import Wst.AppError (AppError) +import Wst.Offchain.BuildTx.ProgrammableLogic (alwaysSucceedsArgs) +import Wst.Offchain.Endpoints.Deployment qualified as Endpoints +import Wst.Offchain.Env qualified as C +import Wst.Offchain.Env qualified as Env +import Wst.Offchain.Query qualified as Query +import Wst.Server.Types (APIInEra, BuildTxAPI, IssueProgrammableTokenArgs (..), + QueryAPI, TextEnvelopeJSON (..)) + +runServer :: (Env.HasRuntimeEnv env, Env.HasDirectoryEnv env) => env -> IO () +runServer env = do + let app = serve (Proxy @APIInEra) (server env) port = 8081 Warp.run port app +server :: forall env. (Env.HasRuntimeEnv env, C.HasDirectoryEnv env) => env -> Server APIInEra +server env = hoistServer (Proxy @APIInEra) (runWstAppServant env) $ + healthcheck + :<|> queryApi @env @C.ConwayEra + :<|> txApi @env + +healthcheck :: Applicative m => m NoContent +healthcheck = pure NoContent + +queryApi :: forall env era. C.IsBabbageBasedEra era => ServerT (QueryAPI era) (WstApp env era) +queryApi = Query.globalParamsNode + +txApi :: forall env. (C.HasDirectoryEnv env) => ServerT (BuildTxAPI C.ConwayEra) (WstApp env C.ConwayEra) +txApi = + issueProgrammableTokenEndpoint @C.ConwayEra @env + +issueProgrammableTokenEndpoint :: forall era env m. + ( MonadReader env m + , Env.HasDirectoryEnv env + , MonadBlockchain era m + , MonadError (AppError era) m + , C.IsBabbageBasedEra era + , C.HasScriptLanguageInEra C.PlutusScriptV3 era + , MonadUtxoQuery m + ) + => IssueProgrammableTokenArgs -> m (TextEnvelopeJSON (C.Tx era)) +issueProgrammableTokenEndpoint IssueProgrammableTokenArgs{itaAssetName, itaQuantity, itaOperatorAddress} = do + operatorEnv <- Env.loadOperatorEnvFromAddress itaOperatorAddress + dirEnv <- asks Env.directoryEnv + + -- FIXME: Replace alwaysSucceedsArgs with blacklist monetary policy as soon as it is finished + let tokenArgs = alwaysSucceedsArgs + Env.withEnv $ Env.withOperator operatorEnv $ Env.withDirectory dirEnv $ do + TextEnvelopeJSON <$> Endpoints.issueProgrammableTokenTx tokenArgs itaAssetName itaQuantity diff --git a/src/lib/Wst/Server/Endpoints.hs b/src/lib/Wst/Server/Endpoints.hs index ca89c02..8949dfd 100644 --- a/src/lib/Wst/Server/Endpoints.hs +++ b/src/lib/Wst/Server/Endpoints.hs @@ -1,37 +1,31 @@ -{-# LANGUAGE TypeApplications #-} -{-# LANGUAGE TypeOperators #-} {- | This module contains the endpoints of the server. -} module Wst.Server.Endpoints ( healthcheck, - initMerkleTree, - updateMerkleTree, - transferToUser, - transferToIssuer, - queryAddress, - queryAllSanctionedAddresses + -- * Query endpoints + queryGlobalParams, + + -- * Build tx endpoints + issueProgrammableTokens ) where + +import Cardano.Api qualified as C +import Control.Monad.Except (MonadError) +import Convex.Class (MonadUtxoQuery) import Servant (Handler) import Servant.API (NoContent (..)) +import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams) +import Wst.AppError (AppError) +import Wst.Offchain.Query (UTxODat) +import Wst.Offchain.Query qualified as Query +import Wst.Server.Types (IssueProgrammableTokenArgs, TextEnvelopeJSON) healthcheck :: Handler NoContent healthcheck = pure NoContent -initMerkleTree :: String -> Handler String -initMerkleTree _arg = pure "initMerkleTree" - -updateMerkleTree :: String -> Handler String -updateMerkleTree _arg = pure "updateMerkleTree" - -transferToUser :: String -> Handler String -transferToUser _arg = pure "transferToUser" - -transferToIssuer :: String -> Handler String -transferToIssuer _arg = pure "transferToIssuer" - -queryAddress :: String -> Handler String -queryAddress name = pure name +queryGlobalParams :: forall era m. (MonadUtxoQuery m, C.IsBabbageBasedEra era, MonadError (AppError era) m) => m (UTxODat era ProgrammableLogicGlobalParams) +queryGlobalParams = Query.globalParamsNode -queryAllSanctionedAddresses :: Handler String -queryAllSanctionedAddresses = pure "allSanctionedAddresses" +issueProgrammableTokens :: forall era m. IssueProgrammableTokenArgs -> m (TextEnvelopeJSON (C.Tx era)) +issueProgrammableTokens = undefined diff --git a/src/lib/Wst/Server/Types.hs b/src/lib/Wst/Server/Types.hs index c683417..3aa2e5b 100644 --- a/src/lib/Wst/Server/Types.hs +++ b/src/lib/Wst/Server/Types.hs @@ -8,34 +8,51 @@ -} module Wst.Server.Types ( API, + APIInEra, + QueryAPI, + BuildTxAPI, + IssueProgrammableTokenArgs(..), + TextEnvelopeJSON(..), ) where -import Servant.API (Capture, Description, Get, JSON, NoContent, Post, ReqBody, - type (:>), (:<|>) (..)) +import Cardano.Api (AssetName, Quantity) +import Cardano.Api qualified as C +import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Proxy (Proxy (..)) +import GHC.Generics (Generic) +import Servant.API (Description, Get, JSON, NoContent, Post, ReqBody, type (:>), + (:<|>) (..)) +import SmartTokens.Types.ProtocolParams (ProgrammableLogicGlobalParams) +import Wst.Offchain.Query (UTxODat (..)) -type API = - "healthcheck" :> Description "Is the server alive?" :> Get '[JSON] NoContent - :<|> "init-merkle-tree" :> Description "Initialize a new Merkle tree." :> ReqBody '[JSON] String :> Post '[JSON] String - -- creates empty directory - -- initialize the programmable token params, dir node minting policy - -- init head of linked list - - :<|> "update-merkle-tree" :> Description "Update the Merkle tree." :> ReqBody '[JSON] String :> Post '[JSON] String -- This might need to be broken down further - -- dir 1 - -- the programmable script to execute - -- add program (registers staking scripts as well) - -- modify program - -- remove program (maybe not needed) - - -- dir 2 (specific to the program) - -- add blacklist - -- remove blacklist - - -- should be user-transfer (invoking spending program) - :<|> "transfer-to-user" :> Description "Transfer tokens to a user." :> ReqBody '[JSON] String :> Post '[JSON] String - - -- should be issuer-transfe (invoking issuer program) - :<|> "transfer-to-issuer" :> Description "Transfer tokens to an issuer." :> ReqBody '[JSON] String :> Post '[JSON] String - :<|> "address" :> Description "Query the balance of an address." :> Capture "address" String :> Get '[JSON] String - :<|> "all-sanctioned-addresses" :> Description "Query all sanctioned addresses." :> Get '[JSON] String +type APIInEra = API C.ConwayEra + +newtype TextEnvelopeJSON a = TextEnvelopeJSON{ unTextEnvelopeJSON :: a } + +instance C.HasTextEnvelope a => ToJSON (TextEnvelopeJSON a) where + toJSON = toJSON . C.serialiseToTextEnvelope Nothing . unTextEnvelopeJSON +instance C.HasTextEnvelope a => FromJSON (TextEnvelopeJSON a) where + parseJSON val = parseJSON val >>= either (fail . show) (pure . TextEnvelopeJSON) . C.deserialiseFromTextEnvelope (C.proxyToAsType Proxy) + +type API era = + "healthcheck" :> Description "Is the server alive?" :> Get '[JSON] NoContent + :<|> "query" :> QueryAPI era + :<|> "tx" :> BuildTxAPI era + +type QueryAPI era = + "global-params" :> Description "The UTxO with the global parameters" :> Get '[JSON] (UTxODat era ProgrammableLogicGlobalParams) + +{-| Arguments for the programmable-token endpoint. The asset name can be something like "USDW" for the regulated stablecoin. +-} +data IssueProgrammableTokenArgs = + IssueProgrammableTokenArgs + { itaOperatorAddress :: C.Address C.ShelleyAddr + , itaAssetName :: AssetName + , itaQuantity :: Quantity + } + deriving stock (Eq, Show, Generic) + deriving anyclass (ToJSON, FromJSON) + +type BuildTxAPI era = + "programmable-token" :> "issue" :> Description "Create some programamble tokens" :> ReqBody '[JSON] IssueProgrammableTokenArgs :> Post '[JSON] (TextEnvelopeJSON (C.Tx era)) diff --git a/src/test/Wst/Test/Env.hs b/src/test/Wst/Test/Env.hs index 1f160dc..1294d33 100644 --- a/src/test/Wst/Test/Env.hs +++ b/src/test/Wst/Test/Env.hs @@ -5,14 +5,14 @@ module Wst.Test.Env( asAdmin ) where -import Cardano.Api qualified as C -import Control.Monad.Reader (ReaderT, runReaderT) +import Cardano.Api.Shelley qualified as C +import Control.Monad.Reader (MonadReader, ReaderT) import Convex.Class (MonadUtxoQuery) import Convex.Wallet qualified as Wallet import Convex.Wallet.MockWallet (w1) import Convex.Wallet.Operator (Operator (..), PaymentExtendedKey (..), Signing) import Convex.Wallet.Operator qualified as Operator -import Wst.Offchain.Env (OperatorEnv) +import Data.Functor.Identity (Identity) import Wst.Offchain.Env qualified as Env {-| Key used for actions of the stableoin issuer / operator. @@ -26,7 +26,14 @@ admin = {-| Run an action using the "admin" key. Deploying the system, minting stablecoins, etc. -} -asAdmin :: forall era m a. (MonadUtxoQuery m, C.IsBabbageBasedEra era) => ReaderT (OperatorEnv era) m a -> m a +asAdmin :: forall era o d r m a. + ( MonadUtxoQuery m + , C.IsBabbageBasedEra era + , MonadReader (Env.CombinedEnv o d r era) m + ) + => ReaderT (Env.CombinedEnv Identity d r era) m a -> m a asAdmin action = do - env <- Env.loadEnv (Operator.verificationKey $ Operator.oPaymentKey admin) (Operator.oStakeKey admin) - runReaderT action env + env <- Env.loadOperatorEnv + (C.verificationKeyHash . Operator.verificationKey . oPaymentKey $ admin) + (maybe C.NoStakeAddress (C.StakeAddressByValue . C.StakeCredentialByKey . C.verificationKeyHash) $ Operator.oStakeKey admin) + Env.withOperator env action diff --git a/src/test/Wst/Test/UnitTest.hs b/src/test/Wst/Test/UnitTest.hs index 965f07f..8b52004 100644 --- a/src/test/Wst/Test/UnitTest.hs +++ b/src/test/Wst/Test/UnitTest.hs @@ -38,18 +38,17 @@ tests = testGroup "unit tests" ] deployDirectorySet :: (MonadUtxoQuery m, MonadBlockchain C.ConwayEra m, MonadFail m) => m C.TxIn -deployDirectorySet = failOnError $ asAdmin @C.ConwayEra $ do +deployDirectorySet = failOnError $ Env.withEnv $ asAdmin @C.ConwayEra $ do (tx, txI) <- Endpoints.deployTx void $ sendTx $ signTxOperator admin tx Env.withDirectoryFor txI $ do Query.registryNodes @C.ConwayEra >>= void . expectSingleton "registry output" - Query.globalParamsNode @C.ConwayEra - >>= void . expectSingleton "global params output" + void $ Query.globalParamsNode @C.ConwayEra pure txI insertDirectoryNode :: (MonadUtxoQuery m, MonadBlockchain C.ConwayEra m, MonadFail m) => m () -insertDirectoryNode = failOnError $ do +insertDirectoryNode = failOnError $ Env.withEnv $ do txI <- deployDirectorySet asAdmin @C.ConwayEra $ Env.withDirectoryFor txI $ do Endpoints.insertNodeTx dummyNodeArgs >>= void . sendTx . signTxOperator admin @@ -59,7 +58,7 @@ insertDirectoryNode = failOnError $ do {-| Issue some tokens with the "always succeeds" validator -} issueAlwaysSucceedsValidator :: (MonadUtxoQuery m, MonadFail m, MonadMockchain C.ConwayEra m) => m () -issueAlwaysSucceedsValidator = failOnError $ do +issueAlwaysSucceedsValidator = failOnError $ Env.withEnv $ do -- Register the stake validator -- Oddly, the tests passes even if we don't do this. diff --git a/src/wst-poc.cabal b/src/wst-poc.cabal index 702133b..a9fbec1 100644 --- a/src/wst-poc.cabal +++ b/src/wst-poc.cabal @@ -68,9 +68,10 @@ library SmartTokens.Types.ProtocolParams SmartTokens.Types.PTokenDirectory Types.Constants + Wst.App + Wst.AppError Wst.Cli Wst.Cli.Command - Wst.Cli.RuntimeEnv Wst.Client Wst.Offchain Wst.Offchain.BuildTx.Blacklist @@ -101,6 +102,7 @@ library , cardano-ledger-api , containers , convex-base + , convex-blockfrost , convex-coin-selection , convex-mockchain , convex-node-client