Reflex-ify Hydra network managemement + make start async

This commit is contained in:
Adriaan Leijnse 2022-10-26 17:35:33 +01:00
parent 25c2914293
commit e2c2cd0070

View File

@ -42,7 +42,6 @@ import qualified Hydra.Types as HT
import Data.Maybe (fromJust, fromMaybe) import Data.Maybe (fromJust, fromMaybe)
import Data.Aeson.Types (parseMaybe) import Data.Aeson.Types (parseMaybe)
import System.IO (IOMode(WriteMode), openFile) import System.IO (IOMode(WriteMode), openFile)
import Data.IORef (readIORef, writeIORef, IORef, newIORef)
import Hydra.Types import Hydra.Types
import Data.Text (Text) import Data.Text (Text)
import qualified Data.ByteString.Lazy.Char8 as ByteString.Char8 import qualified Data.ByteString.Lazy.Char8 as ByteString.Char8
@ -181,10 +180,7 @@ cardanoNodeCreateProcess =
runHydraDemo :: (MonadIO m) runHydraDemo :: (MonadIO m)
=> HydraDemo => HydraDemo
-> m (Map Text ( ProcessHandle -> m RunningNodes
, Address -- Cardano address
, HydraNodeInfo
))
runHydraDemo nodes = do runHydraDemo nodes = do
keysAddresses <- forM nodes $ \(actorSeed, fuelSeed) -> do keysAddresses <- forM nodes $ \(actorSeed, fuelSeed) -> do
keys@(HydraKeyInfo (KeyPair _ vk) _) <- generateKeys keys@(HydraKeyInfo (KeyPair _ vk) _) <- generateKeys
@ -196,14 +192,9 @@ runHydraDemo nodes = do
hstxid <- publishReferenceScripts hstxid <- publishReferenceScripts
handles <- standupDemoHydraNetwork hstxid (fmap fst keysAddresses) handles <- standupDemoHydraNetwork hstxid (fmap fst keysAddresses)
liftIO . putStrLn $ [i|Hydra Network Running for nodes #{Map.keys nodes}|] liftIO . putStrLn $ [i|Hydra Network Running for nodes #{Map.keys nodes}|]
pure $ Map.merge Map.dropMissing Map.dropMissing (Map.zipWithMatched (\_ addr (handle, nodeInfo) -> (handle, addr, nodeInfo))) (fmap snd keysAddresses) handles pure $ Map.merge Map.dropMissing Map.dropMissing (Map.zipWithMatched (\_ addr (handle, nodeInfo) -> RunningNode handle addr nodeInfo)) (fmap snd keysAddresses) handles
type State = Map Text ( ProcessHandle
, Address -- Cardano address
, HydraNodeInfo
)
headElement :: forall t m. ( TriggerEvent t m, DomBuilder t m) =>m () headElement :: forall t m. ( TriggerEvent t m, DomBuilder t m) =>m ()
headElement = do headElement = do
el "title" $ text "Hydra Head Demo" el "title" $ text "Hydra Head Demo"
@ -217,42 +208,71 @@ main = liftIO $ do
threadDelay $ seconds 3 threadDelay $ seconds 3
mainWidgetWithHead headElement app mainWidgetWithHead headElement app
makeTx :: () => IORef State -> Text makeTx :: () => RunningNodes -> Text
-> Map TxIn TxInInfo -> Lovelace -> Text -> IO Text -> Map TxIn TxInInfo -> Lovelace -> Text -> IO Text
makeTx hydraProcessHandlesRef fromName utxos lovelace toName = do makeTx actors fromName utxos lovelace toName = do
let lovelaceUtxos = mapMaybe (Map.lookup "lovelace" . HT.value) utxos let lovelaceUtxos = mapMaybe (Map.lookup "lovelace" . HT.value) utxos
actors <- readIORef hydraProcessHandlesRef
jsonStr <- jsonStr <-
buildSignedHydraTx buildSignedHydraTx
(_signingKey . _cardanoKeys . _keys . (\(_, _, hn) -> hn) $ actors ! fromName) (_signingKey . _cardanoKeys . _keys . _rnNodeInfo $ actors ! fromName)
((\(_, addr, _) -> addr) $ actors ! fromName) (_rnAddress $ actors ! fromName)
((\(_, addr, _) -> addr) $ actors ! toName) (_rnAddress $ actors ! toName)
lovelaceUtxos lovelaceUtxos
lovelace lovelace
let jsonTx :: Aeson.Value = fromMaybe (error "Failed to parse TX") . Aeson.decode . ByteString.Char8.pack $ jsonStr let jsonTx :: Aeson.Value = fromMaybe (error "Failed to parse TX") . Aeson.decode . ByteString.Char8.pack $ jsonStr
pure . fromJust . parseMaybe (withObject "signed tx" (.: "cborHex")) $ jsonTx pure . fromJust . parseMaybe (withObject "signed tx" (.: "cborHex")) $ jsonTx
startDemo :: MonadIO m => IORef State -> HydraDemo -> m RunningNodes
startDemo hydraProcessHandlesRef demo = do -- | Stopped demo desired state.
liftIO (mapM (terminateProcess . (\(hndl, _, _) -> hndl)) =<< readIORef hydraProcessHandlesRef) stoppedDemo :: HydraDemo
nodeInfos <- runHydraDemo demo stoppedDemo = mempty
liftIO . writeIORef hydraProcessHandlesRef $ nodeInfos
actorList :: RunningNodes <- forM nodeInfos $ \(_, addr, nInfo) -> do data HydraNetStatus = HNNotRunning | HNStarting | HNRunning { _hnRunningNodes :: RunningNodes }
pure
( addr, -- | Start stop demo. When demo is 'mempty' the demo is stopped. If
[iii|ws://localhost:#{_apiPort nInfo}|] -- status is 'HNStarting' nothing is done.
) manageDemo ::
pure actorList forall t m.
( MonadIO (Performable m),
PerformEvent t m,
TriggerEvent t m,
MonadHold t m,
MonadFix m
) =>
Event t HydraDemo ->
m (Dynamic t HydraNetStatus)
manageDemo desiredStateE = mdo
let startStopDemoE = attachWithMaybe (\status demo ->
case status of
HNStarting -> Nothing
_ -> Just demo)
(current statusDyn)
desiredStateE
newRunningE <- performEventAsync . ffor (attach (current statusDyn) startStopDemoE) $ \(status, demo) returnAction -> do
case status of
HNRunning ns -> liftIO . mapM_ (terminateProcess . _rnProcessHandle) $ ns
_ -> pure ()
unless (demo == stoppedDemo) $
liftIO $ void $ forkIO $ returnAction <=< runHydraDemo $ demo
statusDyn <- holdDyn HNNotRunning $ leftmost [ bool HNStarting HNNotRunning . (== stoppedDemo) <$> desiredStateE
, HNRunning <$> newRunningE
]
pure statusDyn
apiAddress :: HydraNodeInfo -> Text
apiAddress nInfo = [__i|ws://localhost:#{_apiPort nInfo}|]
-- | Friendly name for a Hydra node. -- | Friendly name for a Hydra node.
type DemoNodeName = Text type DemoNodeName = Text
-- | WebSocket URL data RunningNode = RunningNode
type ApiUrl = Text { _rnProcessHandle :: ProcessHandle
, _rnAddress :: Address
, _rnNodeInfo :: HydraNodeInfo
}
type RunningNodes = Map DemoNodeName ( Address -- Cardano address type RunningNodes = Map DemoNodeName RunningNode
, ApiUrl
)
type HydraDemo = Map type HydraDemo = Map
DemoNodeName DemoNodeName
@ -260,7 +280,6 @@ type HydraDemo = Map
, Lovelace -- Seed for fuel , Lovelace -- Seed for fuel
) )
seconds :: Int -> Int seconds :: Int -> Int
seconds = (* 1000000) seconds = (* 1000000)
@ -352,33 +371,32 @@ startStopDemoControls ::
( DomBuilder t m, ( DomBuilder t m,
MonadFix m, MonadFix m,
PostBuild t m, PostBuild t m,
MonadHold t m, MonadIO (Performable m), PerformEvent t m) => MonadHold t m,
IORef State -> MonadIO (Performable m),
m (Event t RunningNodes) PerformEvent t m,
startStopDemoControls hydraProcessHandlesRef = mdo TriggerEvent t m
headRunning <- toggle False headStartedOrStoppedE ) =>
((), demoConfig) <- runDynamicWriterT $ dyn_ (bool (tellDyn =<< demoSettings alicebobcarolDemo) blank <$> headRunning) m (Dynamic t HydraNetStatus)
startStopHeadE <- buttonClass ((\running -> startStopDemoControls = mdo
let color :: Text = bool "green" "red" running demoStatusDyn <- manageDemo desiredDemoStateE
in [__i|bg-#{color}-500 hover:bg-#{color}-400 active:bg-#{color}-300 ((), demoConfigDyn) <- runDynamicWriterT $ dyn_ . ffor demoStatusDyn $ \case
text-white font-bold text-xl m-4 px-4 py-2 rounded-md|] HNNotRunning -> tellDyn =<< demoSettings alicebobcarolDemo
:: Text) _ -> blank
<$> headRunning) let btnBaseCls = "text-white font-bold text-xl m-4 px-4 py-2 rounded-md" :: Text
$ dynText (bool "Start head" "Stop head" <$> headRunning) let btnStartStopCls (color :: Text) =
let startStopWithConfE = current demoConfig <@ startStopHeadE [__i|bg-#{color}-500 hover:bg-#{color}-400 active:bg-#{color}-300 #{btnBaseCls}|]
headStartedOrStoppedE <- performEvent $ desiredDemoStateE <- switchHold never <=< dyn . ffor demoStatusDyn $ \case
-- Start with mempty to stop the demo: HNNotRunning ->
(\running conf -> startDemo hydraProcessHandlesRef $ bool conf mempty running) fmap (pushAlways (const (sample (current demoConfigDyn)))) $
<$> current headRunning buttonClass (pure $ btnStartStopCls "green") $ text "Start head"
<@> startStopWithConfE HNStarting -> do
let headStartingDom conf = _ <- elAttr' "button" ("class" =: (btnStartStopCls "gray" <> " cursor-not-allowed")
if Map.null conf <> "disabled" =: "true")
then blank $ text "Starting head..."
else elClass "div" "text-white text-2xl m-4" $ text "Head starting..." pure never
void $ runWithReplace blank $ leftmost [ headStartingDom <$> startStopWithConfE HNRunning _ -> do
, blank <$ headStartedOrStoppedE fmap (stoppedDemo <$) $ buttonClass (pure $ btnStartStopCls "red") $ text "Stop head"
] pure demoStatusDyn
pure headStartedOrStoppedE
app :: app ::
@ -390,92 +408,95 @@ app ::
MonadHold t m, PerformEvent t m, TriggerEvent t m) => MonadHold t m, PerformEvent t m, TriggerEvent t m) =>
m () m ()
app = do app = do
hydraProcessHandlesRef :: IORef State <- liftIO (newIORef mempty)
elClass "div" "w-screen h-screen bg-gray-900 overflow-y-scroll overflow-x-hidden" $ do elClass "div" "w-screen h-screen bg-gray-900 overflow-y-scroll overflow-x-hidden" $ do
elClass "div" "p-4 m-4 text-white text-5xl font-bold" $ text "Hydra Proof Of Concept Demo" elClass "div" "p-4 m-4 text-white text-5xl font-bold" $ text "Hydra Proof Of Concept Demo"
mdo mdo
headStartedE <- startStopDemoControls hydraProcessHandlesRef headNetStatusDyn <- startStopDemoControls
void $ runWithReplace blank $ ffor headStartedE $ \actors -> mdo void $ dyn_ . ffor headNetStatusDyn $ \case
let actorNames = ffor (Map.toList actors) $ \(name, (_,_)) -> name HNNotRunning -> blank
headState <- holdDyn Idle newState HNStarting -> blank
let headStateDom = elClass "div" "text-lg" . text . ("Head State: " <>) HNRunning actors -> mdo
unless (null actors) $ elClass "div" "ml-4 mt-8 mr-4 mb-2 w-full font-black text-green-500" $ dyn_ $ ffor headState $ \case let actorNames = Map.keys actors
Idle -> do headState <- holdDyn Idle newState
headStateDom "Idle" let headStateDom = elClass "div" "text-lg" . text . ("Head State: " <>)
elClass "div" "text-green-700 text-sm" $ text "Waiting for participant to init..." unless (null actors) $ elClass "div" "ml-4 mt-8 mr-4 mb-2 w-full font-black text-green-500" $ dyn_ $ ffor headState $ \case
Initializing -> do Idle -> do
headStateDom "Initializing" headStateDom "Idle"
elClass "div" "text-green-700 text-sm" $ text $ "Waiting for commits from: " <> T.intercalate ", " actorNames elClass "div" "text-green-700 text-sm" $ text "Waiting for participant to init..."
Open -> headStateDom "Open" Initializing -> do
Closed _ -> headStateDom "Closed/Contestation period" headStateDom "Initializing"
StateReadyToFanout -> headStateDom "Ready to fanout" elClass "div" "text-green-700 text-sm" $ text $ "Waiting for commits from: " <> T.intercalate ", " actorNames
Open -> headStateDom "Open"
newState <- elClass "div" "ml-4 mr-4 overflow-hidden rounded-lg hover:drop-shadow-xl transition-all drop-shadow bg-gray-800" $ mdo Closed _ -> headStateDom "Closed/Contestation period"
rec StateReadyToFanout -> headStateDom "Ready to fanout"
currentTab <- holdDyn (head actorNames) changeTab
newState <- elClass "div" "ml-4 mr-4 overflow-hidden rounded-lg hover:drop-shadow-xl transition-all drop-shadow bg-gray-800" $ mdo
changeTab <- fmap leftmost $ elClass "div" "w-full flex flex-row justify-start" $ for actorNames $ \name -> do rec
let currentTab <- holdDyn (head actorNames) changeTab
isSelected = (== name) <$> currentTab
mkClasses selected = changeTab <- fmap leftmost $ elClass "div" "w-full flex flex-row justify-start" $ for actorNames $ \name -> do
T.intercalate " " [ "leading-none p-4 font-bold text-2xl text-gray-100 flex items-center justify-center" let
, bool "bg-gray-800 text-gray-300 pointer-cursor" "bg-gray-700 text-gray-100" selected isSelected = (== name) <$> currentTab
] mkClasses selected =
(buttonEl, _) <- elDynClass' "button" (mkClasses <$> isSelected) $ text name T.intercalate " " [ "leading-none p-4 font-bold text-2xl text-gray-100 flex items-center justify-center"
pure $ name <$ domEvent Click buttonEl , bool "bg-gray-800 text-gray-300 pointer-cursor" "bg-gray-700 text-gray-100" selected
fmap (fmap getFirst . snd) . runEventWriterT $ forM (Map.toList actors) $ \(name, (actorAddress, wsUrl)) -> mdo ]
let wsCfg = (WebSocketConfig @t @ClientInput) action never True [] (buttonEl, _) <- elDynClass' "button" (mkClasses <$> isSelected) $ text name
ws <- jsonWebSocket wsUrl wsCfg pure $ name <$ domEvent Click buttonEl
let isSelected = (== name) <$> currentTab fmap (fmap getFirst . snd) . runEventWriterT $ forM (Map.toList actors) $ \(name, RunningNode { _rnNodeInfo = nInfo, _rnAddress = actorAddress}) -> mdo
let mkClasses selected = let wsUrl = apiAddress nInfo
T.intercalate " " [ "p-2 bg-gray-700 text-white flex flex-col items-left" let wsCfg = (WebSocketConfig @t @ClientInput) action never True []
, bool "hidden" "" selected ws <- jsonWebSocket wsUrl wsCfg
] let isSelected = (== name) <$> currentTab
(_, action) <- elDynClass "div" (mkClasses <$> isSelected) $ runEventWriterT $ runWithReplace (elClass "div" "text-white" $ text "Connecting to node...") . ffor (_webSocket_open ws) $ \() -> do let mkClasses selected =
let T.intercalate " " [ "p-2 bg-gray-700 text-white flex flex-col items-left"
webSocketMessage :: Event t (ServerOutput Aeson.Value) = , bool "hidden" "" selected
fromMaybe (error "Parsing message from Hydra node failed") <$> _webSocket_recv ws ]
processLog = \case (_, action) <- elDynClass "div" (mkClasses <$> isSelected) $ runEventWriterT $ runWithReplace (elClass "div" "text-white" $ text "Connecting to node...") . ffor (_webSocket_open ws) $ \() -> do
ReadyToCommit {} -> Just Initializing let
HeadIsOpen {} -> Just Open webSocketMessage :: Event t (ServerOutput Aeson.Value) =
HeadIsClosed _ fanoutTime -> Just (Closed fanoutTime) fromMaybe (error "Parsing message from Hydra node failed") <$> _webSocket_recv ws
ReadyToFanout {} -> Just StateReadyToFanout processLog = \case
HeadIsAborted {} -> Just Idle ReadyToCommit {} -> Just Initializing
HeadIsFinalized {} -> Just Idle HeadIsOpen {} -> Just Open
_ -> Nothing HeadIsClosed _ fanoutTime -> Just (Closed fanoutTime)
let stateChange = fmapMaybe processLog webSocketMessage ReadyToFanout {} -> Just StateReadyToFanout
let HeadIsAborted {} -> Just Idle
HeadIsFinalized {} -> Just Idle
myVKeyB :: Behavior t (Maybe T.Text) <- _ -> Nothing
hold Nothing let stateChange = fmapMaybe processLog webSocketMessage
. fmap Just let
. mapMaybe (\case
Greetings (Party vkey') -> Just vkey' myVKeyB :: Behavior t (Maybe T.Text) <-
_ -> Nothing) hold Nothing
$ webSocketMessage . fmap Just
headStateE <- mdo . mapMaybe (\case
void $ dyn $ ffor headState $ \case Greetings (Party vkey') -> Just vkey'
Idle -> idleScreen name _ -> Nothing)
Initializing -> initializingScreen actorAddress myVKeyB webSocketMessage $ webSocketMessage
Open -> openScreen hydraProcessHandlesRef name actorNames actorAddress webSocketMessage headStateE <- mdo
Closed fanoutTime -> closedScreen fanoutTime void $ dyn $ ffor headState $ \case
StateReadyToFanout -> Idle -> idleScreen name
tellAction Initializing -> initializingScreen actorAddress myVKeyB webSocketMessage
. (Fanout <$) Open -> openScreen actors name webSocketMessage
<=< buttonClass "bg-green-400 hover:bg-green-400 active:bg-green-200 text-white font-bold text-xl my-2 px-4 py-2 rounded-md w-32" $ text "Do fanout" Closed fanoutTime -> closedScreen fanoutTime
elClass "div" "mt-4" $ do StateReadyToFanout ->
elClass "div" "mb-1 font-semibold text-sm" $ text "Hydra Node Log" tellAction
elClass "div" "p-2 bg-gray-800 rounded-md drop-shadow break-all" $ . (Fanout <$)
el "ul" $ do <=< buttonClass "bg-green-400 hover:bg-green-400 active:bg-green-200 text-white font-bold text-xl my-2 px-4 py-2 rounded-md w-32" $ text "Do fanout"
comms <- foldDyn (++) [] $ elClass "div" "mt-4" $ do
((:[]) . ("Rcv: " <>) . toStrict . toLazyText . encodeToTextBuilder . toJSON <$> webSocketMessage) elClass "div" "mb-1 font-semibold text-sm" $ text "Hydra Node Log"
<> elClass "div" "p-2 bg-gray-800 rounded-md drop-shadow break-all" $
fmap (fmap (("Snd: " <>) . toStrict . toLazyText . encodeToTextBuilder . toJSON)) action el "ul" $ do
dyn_ $ mapM (el "li" . text) <$> comms comms <- foldDyn (++) [] $
pure stateChange ((:[]) . ("Rcv: " <>) . toStrict . toLazyText . encodeToTextBuilder . toJSON <$> webSocketMessage)
lift $ tellEvent (First <$> headStateE) <>
pure () fmap (fmap (("Snd: " <>) . toStrict . toLazyText . encodeToTextBuilder . toJSON)) action
pure () dyn_ $ mapM (el "li" . text) <$> comms
pure stateChange
lift $ tellEvent (First <$> headStateE)
pure ()
pure ()
filterUtxos :: Address -> WholeUTXO -> WholeUTXO filterUtxos :: Address -> WholeUTXO -> WholeUTXO
filterUtxos addr = Map.filter ((== addr) . HT.address) filterUtxos addr = Map.filter ((== addr) . HT.address)
@ -501,13 +522,11 @@ initializingScreen ::
m () m ()
initializingScreen actorAddress myVKeyB webSocketMessage = do initializingScreen actorAddress myVKeyB webSocketMessage = do
elClass "div" "p-2 flex flex-col" $ do elClass "div" "p-2 flex flex-col" $ do
-- TODO: did not use performEvent here -- TODO: did not use performEvent here so this will block the UI until UTXOs are queried
newUTXOs <- liftIO $ queryAddressUTXOs actorAddress -- fmapMaybe eitherToMaybe <$> (undefined . (DemoApi_GetActorUTXO actorAddress <$) =<< getPostBuild) newUTXOs <- liftIO $ queryAddressUTXOs actorAddress
let commitSelection doCommit = do let commitSelection doCommit = do
(_, currentSet) <- (_, currentSet) <-
runDynamicWriterT $ (tellDyn <=< utxoPicker True) newUTXOs runDynamicWriterT $ (tellDyn <=< utxoPicker True) newUTXOs
-- runWithReplace (elClass "div" "p-4 bg-gray-800 rounded mb-2" $ text $ "Getting " <> name <> "'s UTXOs...") $
-- (tellDyn <=< utxoPicker True) <$> newUTXOs
tellAction $ fmap (Commit . fromMaybe mempty) $ current currentSet <@ doCommit tellAction $ fmap (Commit . fromMaybe mempty) $ current currentSet <@ doCommit
let hasCommitted = let hasCommitted =
attachWithMaybe attachWithMaybe
@ -536,6 +555,7 @@ initializingScreen actorAddress myVKeyB webSocketMessage = do
pure () pure ()
-- TODO: the names are passed in multiple times, the actorAddress can be found in twice as well
openScreen :: openScreen ::
( EventWriter t [ClientInput] m, ( EventWriter t [ClientInput] m,
DomBuilder t m, DomBuilder t m,
@ -543,13 +563,13 @@ openScreen ::
MonadHold t m, MonadHold t m,
PostBuild t m, PostBuild t m,
MonadIO (Performable m), PerformEvent t m) => MonadIO (Performable m), PerformEvent t m) =>
IORef State -> RunningNodes ->
Text -> Text ->
[Text] ->
Address ->
Event t (ServerOutput tx) -> Event t (ServerOutput tx) ->
m () m ()
openScreen hydraProcessHandlesRef name actorNames actorAddress webSocketMessage = do openScreen actors name webSocketMessage = do
let actorNames = Map.keys actors
let actorAddress = _rnAddress (actors ! name)
-- Get your UTxOs on page load and when we observe a transaction -- Get your UTxOs on page load and when we observe a transaction
tellAction . (GetUTxO <$) tellAction . (GetUTxO <$)
. ( ( void $ . ( ( void $
@ -602,7 +622,7 @@ openScreen hydraProcessHandlesRef name actorNames actorAddress webSocketMessage
elClass "div" "flex" $ do elClass "div" "flex" $ do
signedTxE <- signedTxE <-
performEvent . fmap liftIO $ performEvent . fmap liftIO $
makeTx hydraProcessHandlesRef name makeTx actors name
<$> current currentSet <$> current currentSet
-- NOTE/TODO(skylar): This is just to default to the minimum -- NOTE/TODO(skylar): This is just to default to the minimum
<*> current (fromMaybe 1000000 <$> lovelaceDyn) <*> current (fromMaybe 1000000 <$> lovelaceDyn)