2022-10-20 20:45:15 +00:00
{- # LANGUAGE TemplateHaskell # -}
{- # LANGUAGE PartialTypeSignatures # -}
{- # OPTIONS_GHC - Wno - unused - do - bind # -}
{- # LANGUAGE TupleSections # -}
{- # LANGUAGE ConstraintKinds # -}
{- # OPTIONS_GHC - Wno - unrecognised - pragmas # -}
module Main
( main )
where
import Prelude hiding ( filter )
import Hydra.Devnet
import Control.Monad
import System.Directory
import Control.Monad.IO.Class ( MonadIO , liftIO )
import qualified Data.Map as Map
import Data.Map ( Map , ( ! ) )
import Data.Witherable
import Data.String.Interpolate ( i , iii , __i )
import qualified Data.Text as T
import Control.Concurrent
import System.Process
import Data.Aeson as Aeson
( decode , ( .: ) , withObject , Value )
import Data.Aeson.Text ( encodeToTextBuilder )
import Data.Text.Lazy ( toStrict )
import Data.Text.Lazy.Builder ( toLazyText )
import qualified Data.Map.Merge.Lazy as Map
import qualified Hydra.Types as HT
import Data.Maybe ( fromJust , fromMaybe )
import Data.Aeson.Types ( parseMaybe )
import System.IO ( IOMode ( WriteMode ) , openFile )
import Hydra.Types
import Data.Text ( Text )
import qualified Data.ByteString.Lazy.Char8 as ByteString.Char8
import Data.Time ( UTCTime , diffUTCTime )
import Reflex
import Reflex.Dom
import Control.Monad.Fix
import Hydra.ClientInput
import Hydra.ServerOutput
import Data.Bool ( bool )
import Text.Read ( readMaybe )
import Data.Traversable ( for )
import Data.Semigroup ( First ( getFirst , First ) )
import Data.Aeson
import Control.Monad.Trans ( lift )
import Language.Javascript.JSaddle.Types ( MonadJSM )
standupDemoHydraNetwork :: ( MonadIO m )
=> HydraScriptTxId
-> Map Text HydraKeyInfo
-> m ( Map Text ( ProcessHandle , HydraNodeInfo ) )
standupDemoHydraNetwork hstxid actors = do
liftIO $ createDirectoryIfMissing True " demo-logs "
liftIO $ sequence . flip Map . mapWithKey nodes $ \ name node'' -> do
logHndl <- openFile [ iii | demo - logs / hydra - node -# { name } . log | ] WriteMode
errHndl <- openFile [ iii | demo - logs / phydra - node -# { name } . error . log | ] WriteMode
let cp = ( mkHydraNodeCP sharedInfo node'' ( filter ( ( /= _nodeId node'' ) . _nodeId ) ( Map . elems nodes ) ) )
{ std_out = UseHandle logHndl
, std_err = UseHandle errHndl
}
( _ , _ , _ , handle ) <- createProcess cp
pure ( handle , node'' )
where
portNum p n = p * 1000 + n
node' ( n , ( name , keys ) ) =
( name
, HydraNodeInfo n ( portNum 5 n ) ( portNum 9 n ) ( portNum 6 n ) keys
)
nodes = Map . fromList . fmap node' $ zip [ 1 .. ] ( Map . toList actors )
sharedInfo = HydraSharedInfo
{ _hydraScriptsTxId = T . unpack hstxid
, _ledgerGenesis = " devnet/genesis-shelley.json "
, _ledgerProtocolParameters = " devnet/protocol-parameters.json "
, _networkId = show devnetMagic
, _nodeSocket = " devnet/node.socket "
}
-- | Takes the node participant and the list of peers
mkHydraNodeCP :: HydraSharedInfo -> HydraNodeInfo -> [ HydraNodeInfo ] -> CreateProcess
mkHydraNodeCP sharedInfo node peers =
( proc hydraNodePath $ sharedArgs sharedInfo <> nodeArgs node <> concatMap peerArgs peers )
{ std_out = Inherit
}
data HydraSharedInfo = HydraSharedInfo
{ _hydraScriptsTxId :: String
, _ledgerGenesis :: FilePath
, _ledgerProtocolParameters :: FilePath
, _networkId :: String
, _nodeSocket :: FilePath
}
data HydraNodeInfo = HydraNodeInfo
{ _nodeId :: Int
, _port :: Int
, _apiPort :: Int
, _monitoringPort :: Int
, _keys :: HydraKeyInfo
}
sharedArgs :: HydraSharedInfo -> [ String ]
sharedArgs ( HydraSharedInfo hydraScriptsTxId ledgerGenesis protocolParams networkId nodeSocket ) =
[ " --ledger-genesis "
, ledgerGenesis
, " --ledger-protocol-parameters "
, protocolParams
, " --network-id "
, networkId
, " --node-socket "
, nodeSocket
, " --hydra-scripts-tx-id "
, hydraScriptsTxId
]
nodeArgs :: HydraNodeInfo -> [ String ]
nodeArgs ( HydraNodeInfo nodeId port' apiPort monitoringPort
( HydraKeyInfo
( KeyPair cskPath _cvkPath )
( KeyPair hskPath _hvkPath ) ) ) =
[ " --node-id "
, show nodeId
, " --port "
, show port'
, " --api-port "
, show apiPort
, " --monitoring-port "
, show monitoringPort
, " --hydra-signing-key "
, hskPath
, " --cardano-signing-key "
, cskPath
]
peerArgs :: HydraNodeInfo -> [ String ]
peerArgs ni =
[ " --peer "
, [ i | 127.0 . 0.1 :# { _port ni } | ]
, " --hydra-verification-key "
, _verificationKey . _hydraKeys . _keys $ ni
, " --cardano-verification-key "
, _verificationKey . _cardanoKeys . _keys $ ni
]
cardanoNodeCreateProcess :: CreateProcess
cardanoNodeCreateProcess =
( proc cardanoNodePath
[ " run "
, " --config "
, " devnet/cardano-node.json "
, " --topology "
, " devnet/topology.json "
, " --database-path "
, " devnet/db "
, " --socket-path "
, " devnet/node.socket "
, " --shelley-operational-certificate "
, " devnet/opcert.cert "
, " --shelley-kes-key "
, " devnet/kes.skey "
, " --shelley-vrf-key "
, " devnet/vrf.skey "
] ) { std_out = CreatePipe
}
runHydraDemo :: ( MonadIO m )
=> HydraDemo
2022-10-26 16:35:33 +00:00
-> m RunningNodes
2022-10-20 20:45:15 +00:00
runHydraDemo nodes = do
keysAddresses <- forM nodes $ \ ( actorSeed , fuelSeed ) -> do
keys @ ( HydraKeyInfo ( KeyPair _ vk ) _ ) <- generateKeys
addr <- liftIO $ getCardanoAddress vk
void $ seedAddressFromFaucetAndWait addr actorSeed False
void $ seedAddressFromFaucetAndWait addr fuelSeed True
pure ( keys , addr )
liftIO . putStrLn $ " Publishing reference scripts "
hstxid <- publishReferenceScripts
handles <- standupDemoHydraNetwork hstxid ( fmap fst keysAddresses )
2022-10-25 12:52:40 +00:00
liftIO . putStrLn $ [ i | Hydra Network Running for nodes # { Map . keys nodes } | ]
2022-10-26 16:35:33 +00:00
pure $ Map . merge Map . dropMissing Map . dropMissing ( Map . zipWithMatched ( \ _ addr ( handle , nodeInfo ) -> RunningNode handle addr nodeInfo ) ) ( fmap snd keysAddresses ) handles
2022-10-20 20:45:15 +00:00
headElement :: forall t m . ( TriggerEvent t m , DomBuilder t m ) => m ()
headElement = do
el " title " $ text " Hydra Head Demo "
elAttr " script " ( " src " =: " https://cdn.tailwindcss.com " ) blank
main :: IO ()
main = liftIO $ do
prepareDevnet
withCreateProcess cardanoNodeCreateProcess $ \ _ _stdout _ _handle -> do
putStrLn " Devnet is running "
threadDelay $ seconds 3
mainWidgetWithHead headElement app
2022-10-26 16:35:33 +00:00
makeTx :: () => RunningNodes -> Text
2022-10-20 20:45:15 +00:00
-> Map TxIn TxInInfo -> Lovelace -> Text -> IO Text
2022-10-26 16:35:33 +00:00
makeTx actors fromName utxos lovelace toName = do
2022-10-20 20:45:15 +00:00
let lovelaceUtxos = mapMaybe ( Map . lookup " lovelace " . HT . value ) utxos
jsonStr <-
buildSignedHydraTx
2022-10-26 16:35:33 +00:00
( _signingKey . _cardanoKeys . _keys . _rnNodeInfo $ actors ! fromName )
( _rnAddress $ actors ! fromName )
( _rnAddress $ actors ! toName )
2022-10-20 20:45:15 +00:00
lovelaceUtxos
lovelace
let jsonTx :: Aeson . Value = fromMaybe ( error " Failed to parse TX " ) . Aeson . decode . ByteString . Char8 . pack $ jsonStr
pure . fromJust . parseMaybe ( withObject " signed tx " ( .: " cborHex " ) ) $ jsonTx
2022-10-26 16:35:33 +00:00
-- | Stopped demo desired state.
stoppedDemo :: HydraDemo
stoppedDemo = mempty
data HydraNetStatus = HNNotRunning | HNStarting | HNRunning { _hnRunningNodes :: RunningNodes }
-- | Start stop demo. When demo is 'mempty' the demo is stopped. If
-- status is 'HNStarting' nothing is done.
manageDemo ::
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 } | ]
2022-10-20 20:45:15 +00:00
-- | Friendly name for a Hydra node.
type DemoNodeName = Text
2022-10-26 16:35:33 +00:00
data RunningNode = RunningNode
{ _rnProcessHandle :: ProcessHandle
, _rnAddress :: Address
, _rnNodeInfo :: HydraNodeInfo
}
2022-10-20 20:45:15 +00:00
2022-10-26 16:35:33 +00:00
type RunningNodes = Map DemoNodeName RunningNode
2022-10-20 20:45:15 +00:00
type HydraDemo = Map
DemoNodeName
( Lovelace -- Seed for actor
, Lovelace -- Seed for fuel
)
seconds :: Int -> Int
seconds = ( * 1000000 )
alicebobcarolDemo :: HydraDemo
alicebobcarolDemo = Map . fromList [ ( " Alice " , ( 1000000000 , 100000000 ) ) , ( " Bob " , ( 500000000 , 100000000 ) ) , ( " Carol " , ( 250000000 , 100000000 ) ) ]
filterOutFuel :: WholeUTXO -> WholeUTXO
filterOutFuel = Map . filter ( not . isFuel )
isFuel :: TxInInfo -> Bool
isFuel txinfo = datumhash txinfo == Just fuelMarkerDatumHash
-- | Tracks the state of the head based on Hydra Node responses
data HeadState
= Idle
| Initializing
| Open
| Closed UTCTime
| StateReadyToFanout
deriving ( Eq , Show )
buttonClass :: ( PostBuild t m , DomBuilder t m ) => Dynamic t T . Text -> m b -> m ( Event t () )
buttonClass cls content = do
( buttonEl , _ ) <- elDynClass' " button " cls content
pure $ domEvent Click buttonEl
utxoPicker :: forall t m . ( DomBuilder t m , MonadFix m , MonadHold t m , PostBuild t m ) => Bool -> WholeUTXO -> m ( Dynamic t ( Maybe WholeUTXO ) )
utxoPicker pickable wholeUtxo = mdo
elClass " div " " font-semibold text-lg mb-2 " $ text " UTxOs "
currentUtxo <- holdDyn Nothing selectedUtxo
selectedUtxo <- fmap ( leftmost . Map . elems ) $ elClass " div " " flex flex-row flex-wrap gap-2 " $ flip Map . traverseWithKey wholeUtxo $ \ k v -> mdo
let amiSelected = maybe False ( ( k == ) . fst ) <$> currentUtxo
let cls = ( " text-white font-bold text-xl px-4 py-2 rounded-md flex flex-row cursor-pointer mr-2 " <> )
. bool
" bg-gray-500 hover:bg-gray-400 active:bg-gray-300 "
" bg-blue-500 hover:bg-blue-400 active:bg-blue-300 "
<$> amiSelected
( buttonEl , _ ) <- elDynClass' " button " cls $ do
elClass " div " " text-sm text-gray-300 font-semibold flex justify-between " $ do
elClass " div " " flex flex-col " $ do
elClass " div " " w-full flex flex-row justify-between " $ do
elClass " div " " text-gray-400 mr-4 " $ text " lovelace "
when ( isFuel v ) $ elClass " div " " px-2 py-0 flex items-center justify-center leading-node bg-green-500 text-xs text-white font-semibold text-sm rounded-full flex " $
el " div " $ text " FUEL "
elClass " div " " text-lg text-left font-semibold " $ text $ maybe " " ( T . pack . show ) ( Map . lookup " lovelace " $ HT . value v )
pure $ bool Nothing ( Just ( k , v ) ) . ( pickable && ) . not <$> current amiSelected <@ domEvent Click buttonEl
pure $ fmap ( uncurry Map . singleton ) <$> currentUtxo
demoSettings :: ( DomBuilder t m , PostBuild t m , MonadHold t m , MonadFix m ) => HydraDemo -> m ( Dynamic t HydraDemo )
demoSettings setngs = elClass " div " " flex flex-col pl-4 pr-4 " $ do
let initSize = Map . size setngs
let initialList = Map . fromList ( zip [ 1 .. ] ( Map . toList setngs ) )
rec
nextIdentityNumber <- fmap ( 1 + initSize + ) <$> count newNode
let updates =
( ( \ n -> Map . singleton n ( Just ( [ i | Node # { n } | ] , ( 100000000 , 100000000 ) ) ) ) <$> current nextIdentityNumber <@ newNode )
<> deleteEs
( ( () , deleteEs ) , demoDyn ) <- runDynamicWriterT $ runEventWriterT $ void $ elClass " div " " flex-col space-y-2 " $ do
elClass " p " " text-white text-2xl my-4 " $ text " Configure a Hydra Head by specifying the node names and their initial funds in Lovelace. "
listHoldWithKey initialList updates $ \ k ( name , ( actorSeed , _hydraSeed ) ) -> elClass " div " " flex flex-col " $ do
name' <- elClass " div " " flex flex-row space-x-2 " $ do
name' <- fmap _inputElement_value . inputElement $
def & inputElementConfig_initialValue .~ name
& initialAttributes .~ ( " class " =: " text-white bg-gray-800 text-2xl font-bold focus:outline-none p-2 " <> " type " =: " text " )
amount' <- fmap _inputElement_value . inputElement $
def & inputElementConfig_initialValue .~ ( T . pack . show $ actorSeed )
& initialAttributes .~ ( " class " =: " text-white bg-gray-800 text-2xl font-bold focus:outline-none p-2 " <> " type " =: " number " )
deleteE <- buttonClass " bg-gray-400 hover:bg-gray-300 active:bg-gray-200 text-white font-bold text-xl px-4 py-2 rounded-md " $
text " × "
let actorSeed' = ( \ n a -> ( n , ) <$> readMaybe ( T . unpack a ) ) <$> name' <*> amount'
tellDyn ( maybe mempty ( Map . singleton k . ( \ ( actor , sd ) -> ( actor , ( sd , 100000000 ) ) ) ) <$> actorSeed' )
tellEvent ( Map . singleton k Nothing <$ deleteE )
pure name'
let hasDuplicateName = ( \ n ns -> ( > 1 ) . Map . size . Map . filter ( \ ( n' , _ ) -> n == n' ) $ ns ) <$> name' <*> demoDyn
let duplicateNameMsg = elClass " div " " text-red-400 m-2 " $ text " Duplicate name "
dyn_ ( bool blank duplicateNameMsg <$> fromUniqDynamic ( uniqDynamic hasDuplicateName ) )
pure ()
newNode <- buttonClass " bg-gray-400 hover:bg-gray-300 active:bg-gray-200 text-white font-bold text-xl my-4 px-4 py-2 rounded-md w-32 " $
text " Add node "
let demoDyn' = Map . fromList . Map . elems <$> demoDyn
pure demoDyn'
startStopDemoControls ::
( DomBuilder t m ,
MonadFix m ,
PostBuild t m ,
2022-10-26 16:35:33 +00:00
MonadHold t m ,
MonadIO ( Performable m ) ,
PerformEvent t m ,
TriggerEvent t m
) =>
m ( Dynamic t HydraNetStatus )
startStopDemoControls = mdo
demoStatusDyn <- manageDemo desiredDemoStateE
( () , demoConfigDyn ) <- runDynamicWriterT $ dyn_ . ffor demoStatusDyn $ \ case
HNNotRunning -> tellDyn =<< demoSettings alicebobcarolDemo
_ -> blank
let btnBaseCls = " text-white font-bold text-xl m-4 px-4 py-2 rounded-md " :: Text
let btnStartStopCls ( color :: Text ) =
[ __i | bg -# { color } - 500 hover : bg -# { color } - 400 active : bg -# { color } - 300 # { btnBaseCls } | ]
desiredDemoStateE <- switchHold never <=< dyn . ffor demoStatusDyn $ \ case
HNNotRunning ->
fmap ( pushAlways ( const ( sample ( current demoConfigDyn ) ) ) ) $
buttonClass ( pure $ btnStartStopCls " green " ) $ text " Start head "
HNStarting -> do
_ <- elAttr' " button " ( " class " =: ( btnStartStopCls " gray " <> " cursor-not-allowed " )
<> " disabled " =: " true " )
$ text " Starting head... "
pure never
HNRunning _ -> do
fmap ( stoppedDemo <$ ) $ buttonClass ( pure $ btnStartStopCls " red " ) $ text " Stop head "
pure demoStatusDyn
2022-10-20 20:45:15 +00:00
app ::
forall t m .
( PostBuild t m ,
DomBuilder t m ,
MonadFix m ,
MonadJSM m , MonadJSM ( Performable m ) ,
MonadHold t m , PerformEvent t m , TriggerEvent t m ) =>
m ()
app = 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 "
mdo
2022-10-26 16:35:33 +00:00
headNetStatusDyn <- startStopDemoControls
void $ dyn_ . ffor headNetStatusDyn $ \ case
HNNotRunning -> blank
HNStarting -> blank
HNRunning actors -> mdo
let actorNames = Map . keys actors
headState <- holdDyn Idle newState
let headStateDom = elClass " div " " text-lg " . text . ( " Head State: " <> )
unless ( null actors ) $ elClass " div " " ml-4 mt-8 mr-4 mb-2 w-full font-black text-green-500 " $ dyn_ $ ffor headState $ \ case
Idle -> do
headStateDom " Idle "
elClass " div " " text-green-700 text-sm " $ text " Waiting for participant to init... "
Initializing -> do
headStateDom " Initializing "
elClass " div " " text-green-700 text-sm " $ text $ " Waiting for commits from: " <> T . intercalate " , " actorNames
Open -> headStateDom " Open "
Closed _ -> headStateDom " Closed/Contestation period "
StateReadyToFanout -> headStateDom " Ready to fanout "
newState <- elClass " div " " ml-4 mr-4 overflow-hidden rounded-lg hover:drop-shadow-xl transition-all drop-shadow bg-gray-800 " $ mdo
rec
currentTab <- holdDyn ( head actorNames ) changeTab
changeTab <- fmap leftmost $ elClass " div " " w-full flex flex-row justify-start " $ for actorNames $ \ name -> do
let
isSelected = ( == name ) <$> currentTab
mkClasses selected =
T . intercalate " " [ " leading-none p-4 font-bold text-2xl text-gray-100 flex items-center justify-center "
, bool " bg-gray-800 text-gray-300 pointer-cursor " " bg-gray-700 text-gray-100 " selected
]
( buttonEl , _ ) <- elDynClass' " button " ( mkClasses <$> isSelected ) $ text name
pure $ name <$ domEvent Click buttonEl
fmap ( fmap getFirst . snd ) . runEventWriterT $ forM ( Map . toList actors ) $ \ ( name , RunningNode { _rnNodeInfo = nInfo , _rnAddress = actorAddress } ) -> mdo
let wsUrl = apiAddress nInfo
let wsCfg = ( WebSocketConfig @ t @ ClientInput ) action never True []
ws <- jsonWebSocket wsUrl wsCfg
let isSelected = ( == name ) <$> currentTab
let mkClasses selected =
T . intercalate " " [ " p-2 bg-gray-700 text-white flex flex-col items-left "
, bool " hidden " " " selected
]
( _ , action ) <- elDynClass " div " ( mkClasses <$> isSelected ) $ runEventWriterT $ runWithReplace ( elClass " div " " text-white " $ text " Connecting to node... " ) . ffor ( _webSocket_open ws ) $ \ () -> do
let
webSocketMessage :: Event t ( ServerOutput Aeson . Value ) =
fromMaybe ( error " Parsing message from Hydra node failed " ) <$> _webSocket_recv ws
processLog = \ case
ReadyToCommit { } -> Just Initializing
HeadIsOpen { } -> Just Open
HeadIsClosed _ fanoutTime -> Just ( Closed fanoutTime )
ReadyToFanout { } -> Just StateReadyToFanout
HeadIsAborted { } -> Just Idle
HeadIsFinalized { } -> Just Idle
_ -> Nothing
let stateChange = fmapMaybe processLog webSocketMessage
let
myVKeyB :: Behavior t ( Maybe T . Text ) <-
hold Nothing
. fmap Just
. mapMaybe ( \ case
Greetings ( Party vkey' ) -> Just vkey'
_ -> Nothing )
$ webSocketMessage
headStateE <- mdo
void $ dyn $ ffor headState $ \ case
Idle -> idleScreen name
Initializing -> initializingScreen actorAddress myVKeyB webSocketMessage
Open -> openScreen actors name webSocketMessage
Closed fanoutTime -> closedScreen fanoutTime
StateReadyToFanout ->
tellAction
. ( Fanout <$ )
<=< 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 "
elClass " div " " mt-4 " $ do
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 " $
el " ul " $ do
comms <- foldDyn ( ++ ) [] $
( ( : [] ) . ( " Rcv: " <> ) . toStrict . toLazyText . encodeToTextBuilder . toJSON <$> webSocketMessage )
<>
fmap ( fmap ( ( " Snd: " <> ) . toStrict . toLazyText . encodeToTextBuilder . toJSON ) ) action
dyn_ $ mapM ( el " li " . text ) <$> comms
pure stateChange
lift $ tellEvent ( First <$> headStateE )
pure ()
pure ()
2022-10-20 20:45:15 +00:00
filterUtxos :: Address -> WholeUTXO -> WholeUTXO
filterUtxos addr = Map . filter ( ( == addr ) . HT . address )
tellAction :: ( EventWriter t [ a ] m , Reflex t ) => Event t a -> m ()
tellAction = tellEvent . fmap ( : [] )
idleScreen :: ( EventWriter t [ ClientInput ] m , DomBuilder t m ) => Text -> m ()
idleScreen name =
elClass " div " " p-2 flex flex-row " $ do
( buttonEl , _ ) <- elClass' " button " " bg-blue-500 hover:bg-blue-400 active:bg-blue-300 text-white font-bold text-xl px-4 py-2 rounded-md " $ text $ " Initialize head as " <> name
tellAction $ Init 10 <$ domEvent Click buttonEl
initializingScreen ::
( EventWriter t [ ClientInput ] m ,
DomBuilder t m ,
MonadFix m ,
MonadHold t m ,
PostBuild t m , MonadIO m ) =>
Address ->
Behavior t ( Maybe Text ) ->
Event t ( ServerOutput tx ) ->
m ()
initializingScreen actorAddress myVKeyB webSocketMessage = do
elClass " div " " p-2 flex flex-col " $ do
2022-10-26 16:35:33 +00:00
-- TODO: did not use performEvent here so this will block the UI until UTXOs are queried
newUTXOs <- liftIO $ queryAddressUTXOs actorAddress
2022-10-20 20:45:15 +00:00
let commitSelection doCommit = do
( _ , currentSet ) <-
runDynamicWriterT $ ( tellDyn <=< utxoPicker True ) newUTXOs
tellAction $ fmap ( Commit . fromMaybe mempty ) $ current currentSet <@ doCommit
let hasCommitted =
attachWithMaybe
( \ mvkey -> \ case
Committed ( Party vk ) _ -> guard ( Just vk == mvkey )
_ -> Nothing
)
myVKeyB
webSocketMessage
mdo
void . runWithReplace ( commitSelection doCommit ) . ffor hasCommitted $ \ () ->
elClass " div " " text-xl py-4 " $ text " Committed, waiting for the others. "
doCommit <- elClass " div " " flex flex-row mt-4 " $ do
-- Until the head is committed starting the head can be aborted:
tellAction
. ( Hydra . ClientInput . Abort <$ )
<=< buttonClass " bg-gray-400 hover:bg-gray-300 active:bg-gray-200 text-white font-bold text-xl px-4 py-2 rounded-md mr-2 "
$ text " Abort "
isDisabled <- holdDyn False ( True <$ hasCommitted )
let cls =
( bool " bg-blue-500 hover:bg-blue-400 active:bg-blue-300 " " bg-gray-500 hover:bg-gray-500 active:bg-gray-500 cursor-not-allowed " <$> isDisabled )
<> " text-white font-bold text-xl px-4 py-2 rounded-md "
buttonClass cls $ text " Commit "
pure ()
pure ()
2022-10-26 16:35:33 +00:00
-- TODO: the names are passed in multiple times, the actorAddress can be found in twice as well
2022-10-20 20:45:15 +00:00
openScreen ::
( EventWriter t [ ClientInput ] m ,
DomBuilder t m ,
MonadFix m ,
MonadHold t m ,
PostBuild t m ,
MonadIO ( Performable m ) , PerformEvent t m ) =>
2022-10-26 16:35:33 +00:00
RunningNodes ->
2022-10-20 20:45:15 +00:00
Text ->
Event t ( ServerOutput tx ) ->
m ()
2022-10-26 16:35:33 +00:00
openScreen actors name webSocketMessage = do
let actorNames = Map . keys actors
let actorAddress = _rnAddress ( actors ! name )
2022-10-20 20:45:15 +00:00
-- Get your UTxOs on page load and when we observe a transaction
tellAction . ( GetUTxO <$ )
. ( ( void $
filter
( \ case
TxSeen { } -> True
_ -> False
)
webSocketMessage
)
<>
)
=<< getPostBuild
let updatedUTXOs =
fmap ( filterUtxos actorAddress )
. mapMaybe
( \ case
GetUTxOResponse utxoz -> Just utxoz
_ -> Nothing
)
$ webSocketMessage
currentUTXOs <- holdDyn mempty updatedUTXOs
let ifUTXOs yes no = dyn_ ( bool yes no <$> fmap Map . null currentUTXOs )
ifUTXOsDyn yes no = dyn ( bool yes no <$> fmap Map . null currentUTXOs )
mdo
( _ , currentSet ) <-
runDynamicWriterT
. runWithReplace ( elClass " div " " text-white text-2xl " $ text " Getting your UTxOs " )
$ fmap ( tellDyn <=< ( pure . pure . filterOutFuel ) ) updatedUTXOs
_ <- elClass " div " " mb-4 ml-2 " $ dyn_ $ utxoPicker False <$> currentSet
elClass " div " " text-xl mb-8 ml-2 " $ ifUTXOs ( text " Send Ada to a participant: " ) ( text " No UTXOs for this participant " )
flip ifUTXOs blank $ do
( recipientDyn , lovelaceDyn ) <- elClass " div " " flex ml-2 mb-2 " $
elClass " div " " w-auto flex flex-row rounded bg-gray-800 mb-2 overflow-hidden " $ do
ie <- elClass " div " " flex flex-col p-2 " $ do
elClass " div " " text-gray-600 text-sm font-semibold " $ text " LOVELACES "
inputElement $
def
& initialAttributes .~ ( " class " =: " bg-gray-800 text-2xl font-bold focus:outline-none p-2 " <> " type " =: " number " )
& inputElementConfig_initialValue .~ " 1000000 "
recipient <- fmap Reflex . Dom . value $
elClass " div " " flex flex-col p-2 " $ do
elClass " div " " text-gray-600 text-sm font-semibold uppercase " $ text " To "
-- FIXME: unsafe head, will crash with <= 1 actors
dropdown
( head $ filter ( /= name ) actorNames )
( pure ( Map . filter ( /= name ) $ Map . fromList ( fmap ( \ n -> ( n , n ) ) actorNames ) ) )
$ def & dropdownConfig_attributes .~ pure ( " class " =: " bg-gray-800 hover:bg-gray-700 active:bg-gray-900 text-gray-100 font-semibold text-xl px-4 py-2 rounded-md m-2 " )
pure ( recipient , readMaybe . T . unpack <$> _inputElement_value ie )
elClass " div " " flex " $ do
signedTxE <-
performEvent . fmap liftIO $
2022-10-26 16:35:33 +00:00
makeTx actors name
2022-10-20 20:45:15 +00:00
<$> current currentSet
-- NOTE/TODO(skylar): This is just to default to the minimum
<*> current ( fromMaybe 1000000 <$> lovelaceDyn )
<*> current recipientDyn
<@ doSend
tellAction
. fmap NewTx
$ signedTxE
doSend <- elClass " div " " flex flex-row ml-2 " $ do
sendButtonClick <- flip ifUTXOsDyn ( pure never ) $ do
buttonClass " bg-green-500 hover:bg-green-400 active:bg-green-200 text-white font-bold text-xl mr-2 px-4 py-2 rounded-md " $ text " Send "
tellAction
. ( Close <$ )
<=< buttonClass " bg-red-500 hover:bg-red-400 active:bg-red-200 text-white font-bold text-xl px-4 py-2 rounded-md "
$ text " Close Head "
switchHold never sendButtonClick
pure ()
pure ()
closedScreen ::
( MonadFix m ,
MonadIO m ,
MonadIO ( Performable m ) ,
DomBuilder t m ,
PostBuild t m ,
TriggerEvent t m ,
PerformEvent t m ,
MonadHold t m
) =>
UTCTime ->
m ()
closedScreen fanoutTime = do
countDownDyn <- clockLossy 1 fanoutTime
elClass " div " " text-white text-2xl my-4 ml-2 " $ do
text " Fanout time left: "
dyn_
( text . T . pack . show @ Integer
. ceiling
. diffUTCTime fanoutTime
. _tickInfo_lastUTC
<$> countDownDyn
)
text " seconds "