From 7d124596f3d46c1223d6936a5cde9583c60f6591 Mon Sep 17 00:00:00 2001
From: Sebastian Nagel <sebastian.nagel@ncoding.at>
Date: Mon, 23 May 2022 12:28:48 +0200
Subject: [PATCH] Factor some CardanoClient queries to drop hydra-cluster
 dependency in hydra-tui

---
 hydra-cluster/src/CardanoClient.hs          | 131 ++----------------
 hydra-node/hydra-node.cabal                 |   1 +
 hydra-node/src/Hydra/Chain/CardanoClient.hs | 141 ++++++++++++++++++++
 hydra-node/src/Hydra/Chain/Direct.hs        |  73 +---------
 hydra-tui/hydra-tui.cabal                   |   1 -
 hydra-tui/src/Hydra/TUI.hs                  |  14 +-
 6 files changed, 164 insertions(+), 197 deletions(-)
 create mode 100644 hydra-node/src/Hydra/Chain/CardanoClient.hs

diff --git a/hydra-cluster/src/CardanoClient.hs b/hydra-cluster/src/CardanoClient.hs
index 43eb633129b..5398f099f6e 100644
--- a/hydra-cluster/src/CardanoClient.hs
+++ b/hydra-cluster/src/CardanoClient.hs
@@ -1,35 +1,22 @@
--- | A basic cardano-node client that can talk to a local cardano-node.
+-- | A cardano-node client used in end-to-end tests and benchmarks.
 --
--- The idea of this module is to provide a Haskell interface on top of cardano-cli's API,
--- using cardano-api types.
-module CardanoClient where
+-- This modules contains some more functions besides the re-exported basic
+-- querying of hydra-node's 'Hydra.Chain.CardanoClient'.
+module CardanoClient (
+  module Hydra.Chain.CardanoClient,
+  module CardanoClient,
+) where
 
 import Hydra.Prelude
 
 import Hydra.Cardano.Api hiding (Block)
+import Hydra.Chain.CardanoClient
 
 import qualified Cardano.Api.UTxO as UTxO
-import Cardano.Slotting.Time (SystemStart)
 import qualified Data.Map as Map
-import qualified Data.Set as Set
-import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (EraMismatch)
 import Ouroboros.Network.Protocol.LocalTxSubmission.Client (SubmitResult (..))
 
-type NodeSocket = FilePath
-
-data CardanoClient = CardanoClient
-  { queryUTxOByAddress :: [Address ShelleyAddr] -> IO UTxO
-  , networkId :: NetworkId
-  }
-
-mkCardanoClient :: NetworkId -> FilePath -> CardanoClient
-mkCardanoClient networkId filePath =
-  CardanoClient
-    { queryUTxOByAddress = queryUTxO networkId filePath
-    , networkId
-    }
-
--- TODO(SN): DRY with Hydra.Ledger.Cardano module
+-- TODO(SN): DRY with Hydra.Cardano.Api
 
 -- | Build an address give a key.
 --
@@ -44,61 +31,6 @@ buildScriptAddress script networkId =
   let hashed = hashScript script
    in makeShelleyAddress networkId (PaymentCredentialByScript hashed) NoStakeAddress
 
--- |Query UTxO for all given addresses.
---
--- This query is specialised for Shelley addresses in Alonzo era.
--- Throws 'CardanoClientException' if query fails.
-queryUTxO :: NetworkId -> FilePath -> [Address ShelleyAddr] -> IO UTxO
-queryUTxO networkId socket addresses =
-  let query =
-        QueryInEra
-          AlonzoEraInCardanoMode
-          ( QueryInShelleyBasedEra
-              ShelleyBasedEraAlonzo
-              ( QueryUTxO
-                  (QueryUTxOByAddress (Set.fromList $ map AddressShelley addresses))
-              )
-          )
-   in UTxO.fromApi <$> runQuery networkId socket query
-
-queryUTxOByTxIn :: NetworkId -> FilePath -> [TxIn] -> IO UTxO
-queryUTxOByTxIn networkId socket inputs =
-  let query =
-        QueryInEra
-          AlonzoEraInCardanoMode
-          ( QueryInShelleyBasedEra
-              ShelleyBasedEraAlonzo
-              (QueryUTxO (QueryUTxOByTxIn (Set.fromList inputs)))
-          )
-   in UTxO.fromApi <$> runQuery networkId socket query
-
--- | Query the whole UTxO from node. Useful for debugging, but should obviously
--- not be used in production code.
-queryUTxOWhole :: NetworkId -> FilePath -> IO UTxO
-queryUTxOWhole networkId socket =
-  let query =
-        QueryInEra
-          AlonzoEraInCardanoMode
-          ( QueryInShelleyBasedEra
-              ShelleyBasedEraAlonzo
-              (QueryUTxO QueryUTxOWhole)
-          )
-   in UTxO.fromApi <$> runQuery networkId socket query
-
--- | Query current protocol parameters.
---
--- Throws 'CardanoClientException' if query fails.
-queryProtocolParameters :: NetworkId -> FilePath -> IO ProtocolParameters
-queryProtocolParameters networkId socket =
-  let query =
-        QueryInEra
-          AlonzoEraInCardanoMode
-          ( QueryInShelleyBasedEra
-              ShelleyBasedEraAlonzo
-              QueryProtocolParameters
-          )
-   in runQuery networkId socket query
-
 queryStakePools :: NetworkId -> FilePath -> IO (Set PoolId)
 queryStakePools networkId socket =
   let query =
@@ -110,45 +42,6 @@ queryStakePools networkId socket =
           )
    in runQuery networkId socket query
 
-runQuery :: NetworkId -> FilePath -> QueryInMode CardanoMode (Either EraMismatch a) -> IO a
-runQuery networkId socket query =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing query >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right (Left eraMismatch) -> throwIO $ QueryException (show eraMismatch)
-    Right (Right result) -> pure result
-
-localNodeConnectInfo :: NetworkId -> FilePath -> LocalNodeConnectInfo CardanoMode
-localNodeConnectInfo = LocalNodeConnectInfo cardanoModeParams
-
-cardanoModeParams :: ConsensusModeParams CardanoMode
-cardanoModeParams = CardanoModeParams $ EpochSlots defaultByronEpochSlots
- where
-  -- NOTE(AB): extracted from Parsers in cardano-cli, this is needed to run in 'cardanoMode' which
-  -- is the default for cardano-cli
-  defaultByronEpochSlots = 21600 :: Word64
-
-queryTipSlotNo :: NetworkId -> FilePath -> IO SlotNo
-queryTipSlotNo networkId socket =
-  getLocalChainTip (localNodeConnectInfo networkId socket) >>= \case
-    ChainTipAtGenesis -> pure 0
-    ChainTip slotNo _ _ -> pure slotNo
-
-queryTip :: NetworkId -> FilePath -> IO ChainPoint
-queryTip networkId socket =
-  chainTipToChainPoint <$> getLocalChainTip (localNodeConnectInfo networkId socket)
-
-querySystemStart :: NetworkId -> FilePath -> IO SystemStart
-querySystemStart networkId socket =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing QuerySystemStart >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right result -> pure result
-
-queryEraHistory :: NetworkId -> FilePath -> IO (EraHistory CardanoMode)
-queryEraHistory networkId socket =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing (QueryEraHistory CardanoModeIsMultiEra) >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right result -> pure result
-
 -- | Build a "raw" transaction from a bunch of inputs, outputs and fees.
 buildRaw :: [TxIn] -> [TxOut CtxTx] -> Lovelace -> Either TxBodyError TxBody
 buildRaw ins outs fee =
@@ -273,11 +166,11 @@ submit :: NetworkId -> FilePath -> Tx -> IO ()
 submit networkId socket tx =
   submitTxToNodeLocal (localNodeConnectInfo networkId socket) (TxInMode tx AlonzoEraInCardanoMode) >>= \case
     SubmitSuccess -> pure ()
-    SubmitFail err -> throwIO $ SubmitException{reason = show err, tx}
+    SubmitFail err -> throwIO $ ClientSubmitException{reason = show err, tx}
 
 data CardanoClientException
-  = QueryException Text
-  | SubmitException {reason :: Text, tx :: Tx}
+  = ClientQueryException QueryException
+  | ClientSubmitException {reason :: Text, tx :: Tx}
   deriving (Show)
 
 instance Exception CardanoClientException
diff --git a/hydra-node/hydra-node.cabal b/hydra-node/hydra-node.cabal
index 3ad0cfc4d0f..3b401389395 100644
--- a/hydra-node/hydra-node.cabal
+++ b/hydra-node/hydra-node.cabal
@@ -76,6 +76,7 @@ library
   exposed-modules:
     Hydra.API.Server
     Hydra.Chain
+    Hydra.Chain.CardanoClient
     Hydra.Chain.Direct
     Hydra.Chain.Direct.Context
     Hydra.Chain.Direct.State
diff --git a/hydra-node/src/Hydra/Chain/CardanoClient.hs b/hydra-node/src/Hydra/Chain/CardanoClient.hs
new file mode 100644
index 00000000000..b7e04f907cc
--- /dev/null
+++ b/hydra-node/src/Hydra/Chain/CardanoClient.hs
@@ -0,0 +1,141 @@
+-- | A basic cardano-node client that can talk to a local cardano-node.
+--
+-- The idea of this module is to provide a Haskell interface on top of
+-- cardano-cli's API, using cardano-api types.
+module Hydra.Chain.CardanoClient where
+
+import Hydra.Prelude
+
+import Hydra.Cardano.Api hiding (Block)
+
+import qualified Cardano.Api.UTxO as UTxO
+import Cardano.Slotting.Time (SystemStart)
+import qualified Data.Set as Set
+import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (EraMismatch)
+
+type NodeSocket = FilePath
+
+newtype QueryException
+  = QueryException Text
+  deriving (Eq, Show)
+
+instance Exception QueryException
+
+-- * CardanoClient handle
+
+-- | Handle interface for abstract querying of a cardano node.
+data CardanoClient = CardanoClient
+  { queryUTxOByAddress :: [Address ShelleyAddr] -> IO UTxO
+  , networkId :: NetworkId
+  }
+
+-- | Construct a 'CardanoClient' handle.
+mkCardanoClient :: NetworkId -> NodeSocket -> CardanoClient
+mkCardanoClient networkId nodeSocket =
+  CardanoClient
+    { queryUTxOByAddress = queryUTxO networkId nodeSocket
+    , networkId
+    }
+
+-- * Individual functions
+
+queryTip :: NetworkId -> FilePath -> IO ChainPoint
+queryTip networkId socket =
+  chainTipToChainPoint <$> getLocalChainTip (localNodeConnectInfo networkId socket)
+
+queryTipSlotNo :: NetworkId -> FilePath -> IO SlotNo
+queryTipSlotNo networkId socket =
+  getLocalChainTip (localNodeConnectInfo networkId socket) >>= \case
+    ChainTipAtGenesis -> pure 0
+    ChainTip slotNo _ _ -> pure slotNo
+
+-- | Throws at least 'QueryException' if query fails.
+querySystemStart :: NetworkId -> FilePath -> IO SystemStart
+querySystemStart networkId socket =
+  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing QuerySystemStart >>= \case
+    Left err -> throwIO $ QueryException (show err)
+    Right result -> pure result
+
+-- | Throws at least 'QueryException' if query fails.
+queryEraHistory :: NetworkId -> FilePath -> IO (EraHistory CardanoMode)
+queryEraHistory networkId socket =
+  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing (QueryEraHistory CardanoModeIsMultiEra) >>= \case
+    Left err -> throwIO $ QueryException (show err)
+    Right result -> pure result
+
+-- | Query current protocol parameters.
+--
+-- Throws at least 'QueryException' if query fails.
+queryProtocolParameters :: NetworkId -> FilePath -> IO ProtocolParameters
+queryProtocolParameters networkId socket =
+  let query =
+        QueryInEra
+          AlonzoEraInCardanoMode
+          ( QueryInShelleyBasedEra
+              ShelleyBasedEraAlonzo
+              QueryProtocolParameters
+          )
+   in runQuery networkId socket query
+
+-- | Query UTxO for all given addresses.
+--
+-- Throws at least 'QueryException' if query fails.
+queryUTxO :: NetworkId -> FilePath -> [Address ShelleyAddr] -> IO UTxO
+queryUTxO networkId socket addresses =
+  let query =
+        QueryInEra
+          AlonzoEraInCardanoMode
+          ( QueryInShelleyBasedEra
+              ShelleyBasedEraAlonzo
+              ( QueryUTxO
+                  (QueryUTxOByAddress (Set.fromList $ map AddressShelley addresses))
+              )
+          )
+   in UTxO.fromApi <$> runQuery networkId socket query
+
+-- | Query UTxO for given tx inputs.
+--
+-- Throws at least 'QueryException' if query fails.
+queryUTxOByTxIn :: NetworkId -> FilePath -> [TxIn] -> IO UTxO
+queryUTxOByTxIn networkId socket inputs =
+  let query =
+        QueryInEra
+          AlonzoEraInCardanoMode
+          ( QueryInShelleyBasedEra
+              ShelleyBasedEraAlonzo
+              (QueryUTxO (QueryUTxOByTxIn (Set.fromList inputs)))
+          )
+   in UTxO.fromApi <$> runQuery networkId socket query
+
+-- | Query the whole UTxO from node. Useful for debugging, but should obviously
+-- not be used in production code.
+--
+-- Throws at least 'QueryException' if query fails.
+queryUTxOWhole :: NetworkId -> FilePath -> IO UTxO
+queryUTxOWhole networkId socket =
+  let query =
+        QueryInEra
+          AlonzoEraInCardanoMode
+          ( QueryInShelleyBasedEra
+              ShelleyBasedEraAlonzo
+              (QueryUTxO QueryUTxOWhole)
+          )
+   in UTxO.fromApi <$> runQuery networkId socket query
+
+-- | Throws at least 'QueryException' if query fails.
+runQuery :: NetworkId -> FilePath -> QueryInMode CardanoMode (Either EraMismatch a) -> IO a
+runQuery networkId socket query =
+  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing query >>= \case
+    Left err -> throwIO $ QueryException (show err)
+    Right (Left eraMismatch) -> throwIO $ QueryException (show eraMismatch)
+    Right (Right result) -> pure result
+
+localNodeConnectInfo :: NetworkId -> FilePath -> LocalNodeConnectInfo CardanoMode
+localNodeConnectInfo = LocalNodeConnectInfo cardanoModeParams
+
+cardanoModeParams :: ConsensusModeParams CardanoMode
+cardanoModeParams = CardanoModeParams $ EpochSlots defaultByronEpochSlots
+ where
+  -- NOTE(AB): extracted from Parsers in cardano-cli, this is needed to run in 'cardanoMode' which
+  -- is the default for cardano-cli
+  defaultByronEpochSlots = 21600 :: Word64
diff --git a/hydra-node/src/Hydra/Chain/Direct.hs b/hydra-node/src/Hydra/Chain/Direct.hs
index 239d6948bd9..b2ca935608e 100644
--- a/hydra-node/src/Hydra/Chain/Direct.hs
+++ b/hydra-node/src/Hydra/Chain/Direct.hs
@@ -29,7 +29,6 @@ import Cardano.Ledger.Shelley.Rules.Ledger (LedgerPredicateFailure (UtxowFailure
 import Cardano.Ledger.Shelley.Rules.Utxow (UtxowPredicateFailure (UtxoFailure))
 import Cardano.Ledger.Slot (EpochInfo)
 import Cardano.Slotting.EpochInfo (hoistEpochInfo)
-import Cardano.Slotting.Time (SystemStart)
 import Control.Exception (IOException)
 import Control.Monad (foldM)
 import Control.Monad.Class.MonadSTM (
@@ -53,20 +52,10 @@ import Data.Sequence.Strict (StrictSeq)
 import Hydra.Cardano.Api (
   CardanoMode,
   ChainPoint (..),
-  ChainTip (..),
-  ConsensusModeIsMultiEra (CardanoModeIsMultiEra),
-  ConsensusModeParams (CardanoModeParams),
-  EpochSlots (EpochSlots),
   EraHistory (EraHistory),
-  EraInMode (AlonzoEraInCardanoMode),
   LedgerEra,
-  LocalNodeConnectInfo (LocalNodeConnectInfo),
   NetworkId,
   PaymentKey,
-  ProtocolParameters,
-  QueryInEra (QueryInShelleyBasedEra),
-  QueryInMode (QueryEraHistory, QueryInEra, QuerySystemStart),
-  QueryInShelleyBasedEra (QueryProtocolParameters),
   ShelleyBasedEra (ShelleyBasedEraAlonzo),
   SigningKey,
   SlotNo,
@@ -76,8 +65,6 @@ import Hydra.Cardano.Api (
   fromLedgerTx,
   fromLedgerTxIn,
   fromLedgerUTxO,
-  getLocalChainTip,
-  queryNodeLocalState,
   toConsensusPointHF,
   toLedgerPParams,
   toLedgerTx,
@@ -91,6 +78,7 @@ import Hydra.Chain (
   PostChainTx (..),
   PostTxError (..),
  )
+import Hydra.Chain.CardanoClient (queryEraHistory, queryProtocolParameters, querySystemStart, queryTipSlotNo)
 import Hydra.Chain.Direct.State (
   SomeOnChainHeadState (..),
   TokHeadState (..),
@@ -127,7 +115,7 @@ import Hydra.Chain.Direct.Wallet (
 import Hydra.Data.ContestationPeriod (posixToUTCTime)
 import Hydra.Logging (Tracer, traceWith)
 import Hydra.Party (Party)
-import Ouroboros.Consensus.Cardano.Block (EraMismatch, GenTx (..), HardForkApplyTxErr (ApplyTxErrAlonzo), HardForkBlock (BlockAlonzo))
+import Ouroboros.Consensus.Cardano.Block (GenTx (..), HardForkApplyTxErr (ApplyTxErrAlonzo), HardForkBlock (BlockAlonzo))
 import qualified Ouroboros.Consensus.HardFork.History as Consensus
 import Ouroboros.Consensus.Ledger.SupportsMempool (ApplyTxErr)
 import Ouroboros.Consensus.Network.NodeToClient (Codecs' (..))
@@ -733,60 +721,3 @@ instance ToJSON DirectChainLog where
         [ "tag" .= String "Wallet"
         , "contents" .= log
         ]
-
--- * Querying the cardano node
-
-newtype QueryException
-  = QueryException Text
-  deriving (Eq, Show)
-
-instance Exception QueryException
-
-queryTipSlotNo :: NetworkId -> FilePath -> IO SlotNo
-queryTipSlotNo networkId socket =
-  getLocalChainTip (localNodeConnectInfo networkId socket) >>= \case
-    ChainTipAtGenesis -> pure 0
-    ChainTip slotNo _ _ -> pure slotNo
-
-querySystemStart :: NetworkId -> FilePath -> IO SystemStart
-querySystemStart networkId socket =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing QuerySystemStart >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right result -> pure result
-
-queryEraHistory :: NetworkId -> FilePath -> IO (EraHistory CardanoMode)
-queryEraHistory networkId socket =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing (QueryEraHistory CardanoModeIsMultiEra) >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right result -> pure result
-
--- | Query current protocol parameters.
---
--- Throws 'CardanoClientException' if query fails.
-queryProtocolParameters :: NetworkId -> FilePath -> IO ProtocolParameters
-queryProtocolParameters networkId socket =
-  let query =
-        QueryInEra
-          AlonzoEraInCardanoMode
-          ( QueryInShelleyBasedEra
-              ShelleyBasedEraAlonzo
-              QueryProtocolParameters
-          )
-   in runQuery networkId socket query
-
-runQuery :: NetworkId -> FilePath -> QueryInMode CardanoMode (Either EraMismatch a) -> IO a
-runQuery networkId socket query =
-  queryNodeLocalState (localNodeConnectInfo networkId socket) Nothing query >>= \case
-    Left err -> throwIO $ QueryException (show err)
-    Right (Left eraMismatch) -> throwIO $ QueryException (show eraMismatch)
-    Right (Right result) -> pure result
-
-localNodeConnectInfo :: NetworkId -> FilePath -> LocalNodeConnectInfo CardanoMode
-localNodeConnectInfo = LocalNodeConnectInfo cardanoModeParams
-
-cardanoModeParams :: ConsensusModeParams CardanoMode
-cardanoModeParams = CardanoModeParams $ EpochSlots defaultByronEpochSlots
- where
-  -- NOTE(AB): extracted from Parsers in cardano-cli, this is needed to run in 'cardanoMode' which
-  -- is the default for cardano-cli
-  defaultByronEpochSlots = 21600 :: Word64
diff --git a/hydra-tui/hydra-tui.cabal b/hydra-tui/hydra-tui.cabal
index d06882652fb..f39e421f1e3 100644
--- a/hydra-tui/hydra-tui.cabal
+++ b/hydra-tui/hydra-tui.cabal
@@ -89,7 +89,6 @@ library
     , cardano-ledger-shelley-ma
     , containers
     , hydra-cardano-api
-    , hydra-cluster
     , hydra-node
     , hydra-prelude
     , io-classes
diff --git a/hydra-tui/src/Hydra/TUI.hs b/hydra-tui/src/Hydra/TUI.hs
index c047ffae948..5b12ee54591 100644
--- a/hydra-tui/src/Hydra/TUI.hs
+++ b/hydra-tui/src/Hydra/TUI.hs
@@ -16,11 +16,6 @@ import Brick.Forms (Form, FormFieldState, checkboxField, editShowableFieldWithVa
 import Brick.Widgets.Border (hBorder, vBorder)
 import Brick.Widgets.Border.Style (ascii)
 import qualified Cardano.Api.UTxO as UTxO
-import CardanoClient (
-  CardanoClient (..),
-  buildAddress,
-  mkCardanoClient,
- )
 import Data.List (nub, (\\))
 import qualified Data.Map.Strict as Map
 import qualified Data.Text as Text
@@ -39,6 +34,7 @@ import Graphics.Vty (
  )
 import qualified Graphics.Vty as Vty
 import Graphics.Vty.Attributes (defAttr)
+import Hydra.Chain.CardanoClient (CardanoClient (..), mkCardanoClient)
 import Hydra.Chain.Direct.Util (isMarkedOutput)
 import Hydra.Client (Client (..), HydraEvent (..), withClient)
 import Hydra.ClientInput (ClientInput (..))
@@ -309,7 +305,7 @@ handleCommitEvent ::
   EventM n (Next State)
 handleCommitEvent Client{sendInput, sk} CardanoClient{queryUTxOByAddress, networkId} s = case s ^? headStateL of
   Just Initializing{} -> do
-    utxo <- liftIO $ queryUTxOByAddress [buildAddress (getVerificationKey sk) networkId]
+    utxo <- liftIO $ queryUTxOByAddress [ourAddress]
     -- XXX(SN): this is a hydra implementation detail and should be moved
     -- somewhere hydra specific
     let utxoWithoutFuel = Map.filter (not . isMarkedOutput) (UTxO.toMap utxo)
@@ -317,6 +313,12 @@ handleCommitEvent Client{sendInput, sk} CardanoClient{queryUTxOByAddress, networ
   _ ->
     continue $ s & feedbackL ?~ UserFeedback Error "Invalid command."
  where
+  ourAddress =
+    makeShelleyAddress
+      networkId
+      (PaymentCredentialByKey . verificationKeyHash $ getVerificationKey sk)
+      NoStakeAddress
+
   commitDialog u =
     Dialog title form submit
    where