From 20ceaa78c4992ea50fd740eaa0fd557fa7456202 Mon Sep 17 00:00:00 2001 From: Sebastian Nagel Date: Sun, 8 Sep 2024 13:02:27 +0200 Subject: [PATCH] TUI: Add recipient address entry (#1607) * Adds a new option when selecting the recipient to allow for a "manual entry" * Validates entered addresses to be valid bech32 * Fixes event handling to a point where copy & paste works - Before the event handlers for quitting and showing/disabling logs were interfering - Only display and handle log controls when no modal open --- CHANGELOG.md | 4 + hydra-tui/hydra-tui.cabal | 3 +- hydra-tui/src/Hydra/TUI/Drawing.hs | 16 +- hydra-tui/src/Hydra/TUI/Handlers.hs | 394 +++++++++++---------- hydra-tui/src/Hydra/TUI/Handlers/Global.hs | 19 - hydra-tui/src/Hydra/TUI/Model.hs | 35 +- 6 files changed, 257 insertions(+), 214 deletions(-) delete mode 100644 hydra-tui/src/Hydra/TUI/Handlers/Global.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b55bf77f07..766e742d701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 As a minor extension, we also keep a semantic version for the `UNRELEASED` changes. +## [0.18.2] - UNRELEASED + +- Adds a manual recipient address entry to `hydra-tui` and fixes event handling. [#1607](https://github.com/cardano-scaling/hydra/pull/1607) + ## [0.18.1] - 2024-08-15 - New landing page and updated documentation style. [#1560](https://github.com/cardano-scaling/hydra/pull/1560) diff --git a/hydra-tui/hydra-tui.cabal b/hydra-tui/hydra-tui.cabal index 87c52a86f62..c541e596dc2 100644 --- a/hydra-tui/hydra-tui.cabal +++ b/hydra-tui/hydra-tui.cabal @@ -1,6 +1,6 @@ cabal-version: 2.2 name: hydra-tui -version: 0.18.1 +version: 0.18.2 synopsis: TUI for managing a Hydra node description: TUI for managing a Hydra node author: IOG @@ -46,7 +46,6 @@ library Hydra.TUI.Drawing.Utils Hydra.TUI.Forms Hydra.TUI.Handlers - Hydra.TUI.Handlers.Global Hydra.TUI.Logging.Handlers Hydra.TUI.Logging.Types Hydra.TUI.Model diff --git a/hydra-tui/src/Hydra/TUI/Drawing.hs b/hydra-tui/src/Hydra/TUI/Drawing.hs index 831bfea5d53..157d597bb1f 100644 --- a/hydra-tui/src/Hydra/TUI/Drawing.hs +++ b/hydra-tui/src/Hydra/TUI/Drawing.hs @@ -77,11 +77,16 @@ drawScreenShortLog CardanoClient{networkId} Client{sk} s = drawCommandPanel :: RootState -> Widget n drawCommandPanel s = - vBox - [ drawCommandList s - , hBorder - , drawLogCommandList (s ^. logStateL . logVerbosityL) - ] + drawCommandList s + <=> maybeDrawLogCommandList + where + maybeDrawLogCommandList + | not (isModalOpen s) = + vBox + [ hBorder + , drawLogCommandList (s ^. logStateL . logVerbosityL) + ] + | otherwise = emptyWidget drawScreenFullLog :: RootState -> [Widget Name] drawScreenFullLog s = @@ -143,6 +148,7 @@ drawFocusPanelOpen networkId vk utxo pendingUTxOToDecommit = \case SelectingUTxOToDecommit x -> renderForm x EnteringAmount _ x -> renderForm x SelectingRecipient _ _ x -> renderForm x + EnteringRecipientAddress _ _ x -> renderForm x ConfirmingClose x -> vBox [txt "Confirm Close action:", renderForm x] where ownAddress = mkVkAddress networkId vk diff --git a/hydra-tui/src/Hydra/TUI/Handlers.hs b/hydra-tui/src/Hydra/TUI/Handlers.hs index 75055e45543..a4d286415a4 100644 --- a/hydra-tui/src/Hydra/TUI/Handlers.hs +++ b/hydra-tui/src/Hydra/TUI/Handlers.hs @@ -11,75 +11,161 @@ import Brick import Hydra.Cardano.Api hiding (Active) import Hydra.Chain (PostTxError (InternalWalletError, NotEnoughFuel), reason) -import Brick.Forms (Form (formState), editShowableFieldWithValidate, handleFormEvent, newForm) +import Brick.Forms (Form (formState), editField, editShowableFieldWithValidate, handleFormEvent, newForm) import Cardano.Api.UTxO qualified as UTxO import Data.List (nub, (\\)) import Data.Map qualified as Map import Graphics.Vty ( Event (EvKey), Key (..), + Modifier (MCtrl), ) import Graphics.Vty qualified as Vty import Hydra.API.ClientInput (ClientInput (..)) import Hydra.API.ServerOutput (ServerOutput (..), TimedServerOutput (..)) +import Hydra.Cardano.Api.Prelude () import Hydra.Chain.CardanoClient (CardanoClient (..)) import Hydra.Chain.Direct.State () import Hydra.Client (Client (..), HydraEvent (..)) import Hydra.Ledger.Cardano (mkSimpleTx) import Hydra.TUI.Forms -import Hydra.TUI.Handlers.Global (handleVtyGlobalEvents) import Hydra.TUI.Logging.Handlers (info, report, warn) import Hydra.TUI.Logging.Types (LogMessage, LogState, LogVerbosity (..), Severity (..), logMessagesL, logVerbosityL) import Hydra.TUI.Model import Hydra.TUI.Style (own) import Hydra.Tx (IsTx (..), Party, Snapshot (..), balance) import Lens.Micro.Mtl (use, (%=), (.=)) -import Prelude qualified handleEvent :: CardanoClient -> Client Tx IO -> BrickEvent Name (HydraEvent Tx) -> EventM Name RootState () -handleEvent cardanoClient client e = do - zoom logStateL $ handleVtyEventVia handleVtyEventsLogState () e - handleAppEventVia handleTick () e - zoom connectedStateL $ do - handleAppEventVia handleHydraEventsConnectedState () e - zoom connectionL $ handleBrickEventsConnection cardanoClient client e - zoom (logStateL . logMessagesL) $ - handleAppEventVia handleHydraEventsInfo () e - -- XXX: Global events must be handled as the very last step. - -- Any `EventM` that decides to `Continue` would override the `Halt` decision. - handleGlobalEvents e +handleEvent cardanoClient client = \case + AppEvent e -> do + handleTick e + zoom connectedStateL $ do + handleHydraEventsConnectedState e + zoom connectionL $ handleHydraEventsConnection e + zoom (logStateL . logMessagesL) $ + handleHydraEventsInfo e + MouseDown{} -> pure () + MouseUp{} -> pure () + VtyEvent e -> do + modalOpen <- gets isModalOpen + case e of + EvKey (KChar 'c') [MCtrl] -> halt + EvKey (KChar 'd') [MCtrl] -> halt + EvKey (KChar 'q') [] + | not modalOpen -> halt + EvKey (KChar 'Q') [] + | not modalOpen -> halt + _ -> do + zoom (connectedStateL . connectionL . headStateL) $ + handleVtyEventsHeadState cardanoClient client e + + unless modalOpen $ do + zoom logStateL $ handleVtyEventsLogState e + +-- * AppEvent handlers handleTick :: HydraEvent Tx -> EventM Name RootState () handleTick = \case Tick now -> nowL .= now _ -> pure () -handleAppEventVia :: (e -> EventM n s a) -> a -> BrickEvent w e -> EventM n s a -handleAppEventVia f x = \case - AppEvent e -> f e - _ -> pure x +handleHydraEventsConnectedState :: HydraEvent Tx -> EventM Name ConnectedState () +handleHydraEventsConnectedState = \case + ClientConnected -> put $ Connected emptyConnection + ClientDisconnected -> put Disconnected + _ -> pure () -handleVtyEventVia :: (Vty.Event -> EventM n s a) -> a -> BrickEvent w e -> EventM n s a -handleVtyEventVia f x = \case - VtyEvent e -> f e - _ -> pure x +handleHydraEventsConnection :: HydraEvent Tx -> EventM Name Connection () +handleHydraEventsConnection = \case + Update TimedServerOutput{output = Greetings{me}} -> meL .= Identified me + Update TimedServerOutput{output = PeerConnected p} -> peersL %= \cp -> nub $ cp <> [p] + Update TimedServerOutput{output = PeerDisconnected p} -> peersL %= \cp -> cp \\ [p] + e -> zoom headStateL $ handleHydraEventsHeadState e -handleGlobalEvents :: BrickEvent Name (HydraEvent Tx) -> EventM Name RootState () -handleGlobalEvents = \case - AppEvent _ -> pure () - VtyEvent e -> handleVtyGlobalEvents e - _ -> pure () +handleHydraEventsHeadState :: HydraEvent Tx -> EventM Name HeadState () +handleHydraEventsHeadState e = do + case e of + Update TimedServerOutput{time, output = HeadIsInitializing{parties, headId}} -> + put $ Active (newActiveLink (toList parties) headId) + Update TimedServerOutput{time, output = HeadIsAborted{}} -> + put Idle + _ -> pure () + zoom activeLinkL $ handleHydraEventsActiveLink e -handleHydraEventsConnectedState :: HydraEvent Tx -> EventM Name ConnectedState () -handleHydraEventsConnectedState = \case - ClientConnected -> id .= Connected emptyConnection - ClientDisconnected -> id .= Disconnected +handleHydraEventsActiveLink :: HydraEvent Tx -> EventM Name ActiveLink () +handleHydraEventsActiveLink e = do + case e of + Update TimedServerOutput{output = Committed{party, utxo}} -> do + partyCommitted party utxo + Update TimedServerOutput{time, output = HeadIsOpen{utxo}} -> do + activeHeadStateL .= Open OpenHome + Update TimedServerOutput{time, output = SnapshotConfirmed{snapshot = Snapshot{utxo}}} -> + utxoL .= utxo + Update TimedServerOutput{time, output = HeadIsClosed{headId, snapshotNumber, contestationDeadline}} -> do + activeHeadStateL .= Closed{closedState = ClosedState{contestationDeadline}} + Update TimedServerOutput{time, output = ReadyToFanout{}} -> + activeHeadStateL .= FanoutPossible + Update TimedServerOutput{time, output = HeadIsFinalized{utxo}} -> do + utxoL .= utxo + activeHeadStateL .= Final + Update TimedServerOutput{time, output = DecommitRequested{utxoToDecommit}} -> + pendingUTxOToDecommitL .= utxoToDecommit + Update TimedServerOutput{time, output = DecommitFinalized{}} -> + pendingUTxOToDecommitL .= mempty + _ -> pure () + +handleHydraEventsInfo :: HydraEvent Tx -> EventM Name [LogMessage] () +handleHydraEventsInfo = \case + Update TimedServerOutput{time, output = HeadIsInitializing{parties, headId}} -> + info time "Head is initializing" + Update TimedServerOutput{time, output = Committed{party, utxo}} -> do + info time $ show party <> " committed " <> renderValue (balance @Tx utxo) + Update TimedServerOutput{time, output = HeadIsOpen{utxo}} -> do + info time "Head is now open!" + Update TimedServerOutput{time, output = HeadIsAborted{}} -> do + info time "Head aborted, back to square one." + Update TimedServerOutput{time, output = SnapshotConfirmed{snapshot = Snapshot{number}}} -> + info time ("Snapshot #" <> show number <> " confirmed.") + Update TimedServerOutput{time, output = CommandFailed{clientInput}} -> do + warn time $ "Invalid command: " <> show clientInput + Update TimedServerOutput{time, output = HeadIsClosed{snapshotNumber}} -> do + info time $ "Head closed with snapshot number " <> show snapshotNumber + Update TimedServerOutput{time, output = HeadIsContested{snapshotNumber, contestationDeadline}} -> do + info time ("Head contested with snapshot number " <> show snapshotNumber <> " and deadline " <> show contestationDeadline) + Update TimedServerOutput{time, output = TxValid{}} -> + report Success time "Transaction submitted successfully" + Update TimedServerOutput{time, output = TxInvalid{transaction, validationError}} -> + warn time ("Transaction with id " <> show (txId transaction) <> " is not applicable: " <> show validationError) + Update TimedServerOutput{time, output = DecommitApproved{}} -> + report Success time "Decommit approved and submitted to Cardano" + Update TimedServerOutput{time, output = DecommitInvalid{decommitTx, decommitInvalidReason}} -> + warn time ("Decommit Transaction with id " <> show (txId decommitTx) <> " is not applicable: " <> show decommitInvalidReason) + Update TimedServerOutput{time, output = HeadIsFinalized{utxo}} -> do + info time "Head is finalized" + Update TimedServerOutput{time, output = InvalidInput{reason}} -> + warn time ("Invalid input error: " <> toText reason) + Update TimedServerOutput{time, output = PostTxOnChainFailed{postTxError}} -> + case postTxError of + NotEnoughFuel -> do + warn time "Not enough Fuel. Please provide more to the internal wallet and try again." + InternalWalletError{reason} -> + warn time reason + _ -> warn time ("An error happened while trying to post a transaction on-chain: " <> show postTxError) _ -> pure () +partyCommitted :: Party -> UTxO -> EventM n ActiveLink () +partyCommitted party commit = do + zoom (activeHeadStateL . initializingStateL) $ do + remainingPartiesL %= (\\ [party]) + utxoL %= (<> commit) + +-- * VtyEvent handlers + handleVtyEventsHeadState :: CardanoClient -> Client Tx IO -> Vty.Event -> EventM Name HeadState () handleVtyEventsHeadState cardanoClient hydraClient e = do h <- use id @@ -109,78 +195,73 @@ handleVtyEventsInitializingScreen :: CardanoClient -> Client Tx IO -> Vty.Event handleVtyEventsInitializingScreen cardanoClient hydraClient e = do case e of EvKey (KChar 'a') [] -> - id .= ConfirmingAbort confirmRadioField + put $ ConfirmingAbort confirmRadioField _ -> pure () initializingScreen <- use id case initializingScreen of InitializingHome -> case e of EvKey (KChar 'c') [] -> do utxo <- liftIO $ queryUTxOByAddress cardanoClient [mkMyAddress cardanoClient hydraClient] - id .= CommitMenu (utxoCheckboxField $ UTxO.toMap utxo) + put $ CommitMenu (utxoCheckboxField $ UTxO.toMap utxo) _ -> pure () CommitMenu i -> do case e of - EvKey KEsc [] -> id .= InitializingHome + EvKey KEsc [] -> put InitializingHome EvKey KEnter [] -> do let u = formState i let commitUTxO = UTxO $ Map.mapMaybe (\(v, p) -> if p then Just v else Nothing) u liftIO $ externalCommit hydraClient commitUTxO - id .= InitializingHome + put InitializingHome _ -> pure () zoom commitMenuL $ handleFormEvent (VtyEvent e) ConfirmingAbort i -> do case e of - EvKey KEsc [] -> id .= InitializingHome + EvKey KEsc [] -> put InitializingHome EvKey KEnter [] -> do let selected = formState i if selected then liftIO $ sendInput hydraClient Abort - else id .= InitializingHome + else put InitializingHome _ -> pure () zoom confirmingAbortFormL $ handleFormEvent (VtyEvent e) handleVtyEventsOpen :: CardanoClient -> Client Tx IO -> UTxO -> Vty.Event -> EventM Name OpenScreen () -handleVtyEventsOpen cardanoClient hydraClient utxo e = do - case e of - EvKey (KChar 'c') [] -> - id .= ConfirmingClose confirmRadioField - _ -> pure () - k <- use id - case k of - ConfirmingClose i -> do - case e of - EvKey KEsc [] -> id .= OpenHome - EvKey KEnter [] -> do - let selected = formState i - if selected - then liftIO $ sendInput hydraClient Close - else id .= OpenHome - _ -> pure () - zoom confirmingCloseFormL $ handleFormEvent (VtyEvent e) +handleVtyEventsOpen cardanoClient hydraClient utxo e = + get >>= \case OpenHome -> do case e of EvKey (KChar 'n') [] -> do let utxo' = myAvailableUTxO (networkId cardanoClient) (getVerificationKey $ sk hydraClient) utxo - id .= SelectingUTxO (utxoRadioField utxo') + put $ SelectingUTxO (utxoRadioField utxo') EvKey (KChar 'd') [] -> do let utxo' = myAvailableUTxO (networkId cardanoClient) (getVerificationKey $ sk hydraClient) utxo - id .= SelectingUTxOToDecommit (utxoRadioField utxo') + put $ SelectingUTxOToDecommit (utxoRadioField utxo') + EvKey (KChar 'c') [] -> + put $ ConfirmingClose confirmRadioField _ -> pure () - SelectingUTxO i -> do + ConfirmingClose i -> + case e of + EvKey KEsc [] -> put OpenHome + EvKey KEnter [] -> do + let selected = formState i + if selected + then liftIO $ sendInput hydraClient Close + else put OpenHome + _ -> zoom confirmingCloseFormL $ handleFormEvent (VtyEvent e) + SelectingUTxO i -> case e of - EvKey KEsc [] -> id .= OpenHome + EvKey KEsc [] -> put OpenHome EvKey KEnter [] -> do let utxoSelected@(_, TxOut{txOutValue = v}) = formState i let Coin limit = selectLovelace v let enteringAmountForm = let field = editShowableFieldWithValidate id "amount" (\n -> n > 0 && n <= limit) in newForm [field] limit - id .= EnteringAmount{utxoSelected, enteringAmountForm} - _ -> pure () - zoom selectingUTxOFormL $ handleFormEvent (VtyEvent e) - SelectingUTxOToDecommit i -> do + put EnteringAmount{utxoSelected, enteringAmountForm} + _ -> zoom selectingUTxOFormL $ handleFormEvent (VtyEvent e) + SelectingUTxOToDecommit i -> case e of - EvKey KEsc [] -> id .= OpenHome + EvKey KEsc [] -> put OpenHome EvKey KEnter [] -> do let utxoSelected@(_, TxOut{txOutValue = v}) = formState i let recipient = mkVkAddress @Era (networkId cardanoClient) (getVerificationKey $ sk hydraClient) @@ -188,42 +269,78 @@ handleVtyEventsOpen cardanoClient hydraClient utxo e = do Left _ -> pure () Right tx -> do liftIO (sendInput hydraClient (Decommit tx)) - id .= OpenHome - _ -> pure () - zoom selectingUTxOToDecommitFormL $ handleFormEvent (VtyEvent e) - EnteringAmount utxoSelected i -> do + put OpenHome + _ -> zoom selectingUTxOToDecommitFormL $ handleFormEvent (VtyEvent e) + EnteringAmount utxoSelected i -> case e of - EvKey KEsc [] -> id .= OpenHome + EvKey KEsc [] -> put OpenHome EvKey KEnter [] -> do - let amountEntered = formState i - let ownAddress = mkVkAddress @Era (networkId cardanoClient) (getVerificationKey $ sk hydraClient) - let field = - customRadioField '[' 'X' ']' id $ - [ (u, show u, decodeUtf8 $ encodePretty u) - | u <- nub addresses - ] - addresses = getRecipientAddress <$> Map.elems (UTxO.toMap utxo) - getRecipientAddress TxOut{txOutAddress = addr} = addr - decorator a _ _ = - if a == ownAddress - then withAttr own - else id - let selectingRecipientForm = newForm [field decorator] (Prelude.head addresses) - id .= SelectingRecipient{utxoSelected, amountEntered, selectingRecipientForm} - _ -> pure () - zoom enteringAmountFormL $ handleFormEvent (VtyEvent e) - SelectingRecipient utxoSelected amountEntered i -> do + let + field = + customRadioField '[' 'X' ']' id $ + [ (u, show u, show $ pretty u) + | u <- nub $ toList addresses + ] + decorator a _ _ = + if a == SelectAddress ownAddress + then withAttr own + else id + addresses = + ManualEntry + :| (SelectAddress . txOutAddress <$> toList utxo) + put + SelectingRecipient + { utxoSelected + , amountEntered = formState i + , selectingRecipientForm = newForm [field decorator] ManualEntry + } + _ -> zoom enteringAmountFormL $ handleFormEvent (VtyEvent e) + SelectingRecipient utxoSelected amountEntered i -> case e of - EvKey KEsc [] -> id .= OpenHome + EvKey KEsc [] -> put OpenHome + EvKey KEnter [] -> do + case formState i of + SelectAddress recipient -> do + case mkSimpleTx utxoSelected (recipient, lovelaceToValue $ Coin amountEntered) (sk hydraClient) of + Left _ -> pure () + Right tx -> do + liftIO (sendInput hydraClient (NewTx tx)) + put OpenHome + ManualEntry -> + put $ + EnteringRecipientAddress + { utxoSelected + , amountEntered + , enteringRecipientAddressForm = + newForm + [ editField + id + "manual address entry" + (Just 1) + serialiseAddress + (nonEmpty >=> parseAddress . head) + (txt . fold) + id + ] + ownAddress + } + _ -> zoom selectingRecipientFormL $ handleFormEvent (VtyEvent e) + EnteringRecipientAddress utxoSelected amountEntered i -> + case e of + EvKey KEsc [] -> put OpenHome EvKey KEnter [] -> do let recipient = formState i case mkSimpleTx utxoSelected (recipient, lovelaceToValue $ Coin amountEntered) (sk hydraClient) of Left _ -> pure () Right tx -> do liftIO (sendInput hydraClient (NewTx tx)) - id .= OpenHome - _ -> pure () - zoom selectingRecipientFormL $ handleFormEvent (VtyEvent e) + put OpenHome + _ -> zoom enteringRecipientAddressFormL $ handleFormEvent (VtyEvent e) + where + ownAddress = mkVkAddress @Era (networkId cardanoClient) (getVerificationKey $ sk hydraClient) + + parseAddress = + fmap ShelleyAddressInEra . deserialiseAddress (AsAddress AsShelleyAddr) handleVtyEventsFanoutPossible :: Client Tx IO -> Vty.Event -> EventM Name s () handleVtyEventsFanoutPossible hydraClient e = do @@ -239,100 +356,6 @@ handleVtyEventsFinal hydraClient e = do liftIO (sendInput hydraClient Init) _ -> pure () -handleHydraEventsConnection :: HydraEvent Tx -> EventM Name Connection () -handleHydraEventsConnection = \case - Update TimedServerOutput{output = Greetings{me}} -> meL .= Identified me - Update TimedServerOutput{output = PeerConnected p} -> peersL %= \cp -> nub $ cp <> [p] - Update TimedServerOutput{output = PeerDisconnected p} -> peersL %= \cp -> cp \\ [p] - e -> zoom headStateL $ handleHydraEventsHeadState e - -handleHydraEventsHeadState :: HydraEvent Tx -> EventM Name HeadState () -handleHydraEventsHeadState e = do - case e of - Update TimedServerOutput{time, output = HeadIsInitializing{parties, headId}} -> - id .= Active (newActiveLink (toList parties) headId) - Update TimedServerOutput{time, output = HeadIsAborted{}} -> - id .= Idle - _ -> pure () - zoom activeLinkL $ handleHydraEventsActiveLink e - -handleHydraEventsActiveLink :: HydraEvent Tx -> EventM Name ActiveLink () -handleHydraEventsActiveLink e = do - case e of - Update TimedServerOutput{output = Committed{party, utxo}} -> do - partyCommitted party utxo - Update TimedServerOutput{time, output = HeadIsOpen{utxo}} -> do - activeHeadStateL .= Open OpenHome - Update TimedServerOutput{time, output = SnapshotConfirmed{snapshot = Snapshot{utxo}}} -> - utxoL .= utxo - Update TimedServerOutput{time, output = HeadIsClosed{headId, snapshotNumber, contestationDeadline}} -> do - activeHeadStateL .= Closed{closedState = ClosedState{contestationDeadline}} - Update TimedServerOutput{time, output = ReadyToFanout{}} -> - activeHeadStateL .= FanoutPossible - Update TimedServerOutput{time, output = HeadIsFinalized{utxo}} -> do - utxoL .= utxo - activeHeadStateL .= Final - Update TimedServerOutput{time, output = DecommitRequested{utxoToDecommit}} -> - pendingUTxOToDecommitL .= utxoToDecommit - Update TimedServerOutput{time, output = DecommitFinalized{}} -> - pendingUTxOToDecommitL .= mempty - _ -> pure () - -handleHydraEventsInfo :: HydraEvent Tx -> EventM Name [LogMessage] () -handleHydraEventsInfo = \case - Update TimedServerOutput{time, output = HeadIsInitializing{parties, headId}} -> - info time "Head is initializing" - Update TimedServerOutput{time, output = Committed{party, utxo}} -> do - info time $ show party <> " committed " <> renderValue (balance @Tx utxo) - Update TimedServerOutput{time, output = HeadIsOpen{utxo}} -> do - info time "Head is now open!" - Update TimedServerOutput{time, output = HeadIsAborted{}} -> do - info time "Head aborted, back to square one." - Update TimedServerOutput{time, output = SnapshotConfirmed{snapshot = Snapshot{number}}} -> - info time ("Snapshot #" <> show number <> " confirmed.") - Update TimedServerOutput{time, output = CommandFailed{clientInput}} -> do - warn time $ "Invalid command: " <> show clientInput - Update TimedServerOutput{time, output = HeadIsClosed{snapshotNumber}} -> do - info time $ "Head closed with snapshot number " <> show snapshotNumber - Update TimedServerOutput{time, output = HeadIsContested{snapshotNumber, contestationDeadline}} -> do - info time ("Head contested with snapshot number " <> show snapshotNumber <> " and deadline " <> show contestationDeadline) - Update TimedServerOutput{time, output = TxValid{}} -> - report Success time "Transaction submitted successfully" - Update TimedServerOutput{time, output = TxInvalid{transaction, validationError}} -> - warn time ("Transaction with id " <> show (txId transaction) <> " is not applicable: " <> show validationError) - Update TimedServerOutput{time, output = DecommitApproved{}} -> - report Success time "Decommit approved and submitted to Cardano" - Update TimedServerOutput{time, output = DecommitInvalid{decommitTx, decommitInvalidReason}} -> - warn time ("Decommit Transaction with id " <> show (txId decommitTx) <> " is not applicable: " <> show decommitInvalidReason) - Update TimedServerOutput{time, output = HeadIsFinalized{utxo}} -> do - info time "Head is finalized" - Update TimedServerOutput{time, output = InvalidInput{reason}} -> - warn time ("Invalid input error: " <> toText reason) - Update TimedServerOutput{time, output = PostTxOnChainFailed{postTxError}} -> - case postTxError of - NotEnoughFuel -> do - warn time "Not enough Fuel. Please provide more to the internal wallet and try again." - InternalWalletError{reason} -> - warn time reason - _ -> warn time ("An error happened while trying to post a transaction on-chain: " <> show postTxError) - _ -> pure () - -partyCommitted :: Party -> UTxO -> EventM n ActiveLink () -partyCommitted party commit = do - zoom (activeHeadStateL . initializingStateL) $ do - remainingPartiesL %= (\\ [party]) - utxoL %= (<> commit) - -handleBrickEventsConnection :: - CardanoClient -> - Client Tx IO -> - BrickEvent w (HydraEvent Tx) -> - EventM Name Connection () -handleBrickEventsConnection cardanoClient hydraClient x = case x of - AppEvent e -> handleHydraEventsConnection e - VtyEvent e -> handleVtyEventsConnection cardanoClient hydraClient e - _ -> pure () - handleVtyEventsConnection :: CardanoClient -> Client Tx IO -> @@ -349,9 +372,6 @@ handleVtyEventsLogState = \case EvKey (KChar 's') [] -> logVerbosityL .= Short _ -> pure () --- --- View --- scroll :: Direction -> EventM Name LogState () scroll direction = do x <- use logVerbosityL diff --git a/hydra-tui/src/Hydra/TUI/Handlers/Global.hs b/hydra-tui/src/Hydra/TUI/Handlers/Global.hs deleted file mode 100644 index 6aacebeabb2..00000000000 --- a/hydra-tui/src/Hydra/TUI/Handlers/Global.hs +++ /dev/null @@ -1,19 +0,0 @@ -module Hydra.TUI.Handlers.Global where - -import Brick (EventM) -import Brick.Main (halt) -import Graphics.Vty (Event (..), Key (..), Modifier (..)) -import Graphics.Vty qualified as Vty -import Hydra.Prelude - -handleVtyQuitEvents :: Vty.Event -> EventM n s () -handleVtyQuitEvents = \case - EvKey (KChar 'c') [MCtrl] -> halt - EvKey (KChar 'd') [MCtrl] -> halt - EvKey (KChar 'q') [] -> halt - EvKey (KChar 'Q') [] -> halt - _ -> pure () - -handleVtyGlobalEvents :: Vty.Event -> EventM n s () -handleVtyGlobalEvents e = do - handleVtyQuitEvents e diff --git a/hydra-tui/src/Hydra/TUI/Model.hs b/hydra-tui/src/Hydra/TUI/Model.hs index a33ac2d21cd..219cfd7a6a9 100644 --- a/hydra-tui/src/Hydra/TUI/Model.hs +++ b/hydra-tui/src/Hydra/TUI/Model.hs @@ -14,6 +14,7 @@ import Hydra.Client (HydraEvent (..)) import Hydra.Network (Host (..), NodeId) import Hydra.TUI.Logging.Types (LogState) import Hydra.Tx (HeadId, Party (..)) +import Lens.Micro ((^?)) import Lens.Micro.TH (makeLensesFor) data RootState = RootState @@ -58,9 +59,28 @@ data OpenScreen | SelectingUTxO {selectingUTxOForm :: UTxORadioFieldForm (HydraEvent Tx) Name} | SelectingUTxOToDecommit {selectingUTxOToDecommitForm :: UTxORadioFieldForm (HydraEvent Tx) Name} | EnteringAmount {utxoSelected :: (TxIn, TxOut CtxUTxO), enteringAmountForm :: Form Integer (HydraEvent Tx) Name} - | SelectingRecipient {utxoSelected :: (TxIn, TxOut CtxUTxO), amountEntered :: Integer, selectingRecipientForm :: Form AddressInEra (HydraEvent Tx) Name} + | SelectingRecipient + { utxoSelected :: (TxIn, TxOut CtxUTxO) + , amountEntered :: Integer + , selectingRecipientForm :: Form SelectAddressItem (HydraEvent Tx) Name + } + | EnteringRecipientAddress + { utxoSelected :: (TxIn, TxOut CtxUTxO) + , amountEntered :: Integer + , enteringRecipientAddressForm :: Form AddressInEra (HydraEvent Tx) Name + } | ConfirmingClose {confirmingCloseForm :: ConfirmingRadioFieldForm (HydraEvent Tx) Name} +data SelectAddressItem + = ManualEntry + | SelectAddress AddressInEra + deriving (Eq, Show) + +instance Pretty SelectAddressItem where + pretty = \case + ManualEntry -> "Manual entry" + SelectAddress addr -> pretty $ serialiseAddress addr + newtype ClosedState = ClosedState {contestationDeadline :: UTCTime} data HeadState @@ -89,6 +109,7 @@ makeLensesFor , ("selectingUTxOToDecommitForm", "selectingUTxOToDecommitFormL") , ("enteringAmountForm", "enteringAmountFormL") , ("selectingRecipientForm", "selectingRecipientFormL") + , ("enteringRecipientAddressForm", "enteringRecipientAddressFormL") , ("confirmingCloseForm", "confirmingCloseFormL") ] ''OpenScreen @@ -176,3 +197,15 @@ newActiveLink parties headId = , pendingUTxOToDecommit = mempty , headId } + +isModalOpen :: RootState -> Bool +isModalOpen s = + case s + ^? connectedStateL + . connectionL + . headStateL + . activeLinkL + . activeHeadStateL + . openStateL of + Just OpenHome -> False + _ -> True