@@ -5,17 +5,31 @@ module Hydra.API.HTTPServer where
55import Hydra.Prelude
66
77import Cardano.Ledger.Core (PParams )
8+ import Conduit (
9+ ConduitT ,
10+ MonadUnliftIO ,
11+ concatC ,
12+ linesUnboundedAsciiC ,
13+ mapMC ,
14+ runConduitRes ,
15+ sinkList ,
16+ sourceFileBS ,
17+ (.|) ,
18+ )
819import Control.Concurrent.STM (TChan , dupTChan , readTChan )
9- import Data.Aeson (KeyValue ((.=) ), object , withObject , (.:) , (.:?) )
20+ import Control.Lens ((^?) )
21+ import Data.Aeson (KeyValue ((.=) ), Value (String ), object , withObject , (.:) , (.:?) )
1022import Data.Aeson qualified as Aeson
23+ import Data.Aeson.Lens (key , _String )
1124import Data.Aeson.Types (Parser )
1225import Data.ByteString.Lazy qualified as LBS
1326import Data.ByteString.Short ()
27+ import Data.List qualified as List
1428import Data.Text (pack )
1529import Hydra.API.APIServerLog (APIServerLog (.. ), Method (.. ), PathInfo (.. ))
1630import Hydra.API.ClientInput (ClientInput (.. ))
1731import Hydra.API.ServerOutput (ClientMessage (.. ), CommitInfo (.. ), ServerOutput (.. ), TimedServerOutput (.. ), getConfirmedSnapshot , getSeenSnapshot , getSnapshotUtxo )
18- import Hydra.Cardano.Api (AddressInEra , LedgerEra , Tx )
32+ import Hydra.Cardano.Api (AddressInEra , LedgerEra , SlotNo , Tx )
1933import Hydra.Chain (Chain (.. ), PostTxError (.. ), draftCommitTx )
2034import Hydra.Chain.ChainState (IsChainState )
2135import Hydra.Chain.Direct.State ()
@@ -28,6 +42,7 @@ import Hydra.Node.State (NodeState (..))
2842import Hydra.Tx (CommitBlueprintTx (.. ), ConfirmedSnapshot , IsTx (.. ), Snapshot (.. ), UTxOType )
2943import Network.HTTP.Types (ResponseHeaders , hContentType , status200 , status202 , status400 , status404 , status500 )
3044import Network.Wai (Application , Request (pathInfo , requestMethod ), Response , consumeRequestBodyStrict , rawPathInfo , responseLBS )
45+ import System.Directory (doesFileExist )
3146
3247newtype DraftCommitTxResponse tx = DraftCommitTxResponse
3348 { commitTx :: tx
@@ -179,6 +194,13 @@ instance FromJSON SubmitL2TxResponse where
179194instance Arbitrary SubmitL2TxResponse where
180195 arbitrary = genericArbitrary
181196
197+ data HeadInitializationDetails
198+ = HeadInitializationDetails
199+ { time :: UTCTime
200+ , slot :: SlotNo
201+ }
202+ deriving (Eq , Show )
203+
182204jsonContent :: ResponseHeaders
183205jsonContent = [(hContentType, " application/json" )]
184206
@@ -189,6 +211,7 @@ httpApp ::
189211 Tracer IO APIServerLog ->
190212 Chain tx IO ->
191213 Environment ->
214+ FilePath ->
192215 PParams LedgerEra ->
193216 -- | Get latest 'NodeState'.
194217 IO (NodeState tx ) ->
@@ -203,7 +226,7 @@ httpApp ::
203226 -- | Channel to listen for events
204227 TChan (Either (TimedServerOutput tx ) (ClientMessage tx )) ->
205228 Application
206- httpApp tracer directChain env pparams getNodeState getCommitInfo getPendingDeposits putClientInput apiTransactionTimeout responseChannel request respond = do
229+ httpApp tracer directChain env stateFile pparams getNodeState getCommitInfo getPendingDeposits putClientInput apiTransactionTimeout responseChannel request respond = do
207230 traceWith tracer $
208231 APIHTTPRequestReceived
209232 { method = Method $ requestMethod request
@@ -225,6 +248,9 @@ httpApp tracer directChain env pparams getNodeState getCommitInfo getPendingDepo
225248 (" GET" , [" snapshot" , " last-seen" ]) -> do
226249 hs <- headState <$> getNodeState
227250 respond . okJSON $ getSeenSnapshot hs
251+ (" GET" , [" head-initialization" ]) ->
252+ handleHeadInitializationTime stateFile
253+ >>= respond
228254 (" POST" , [" snapshot" ]) ->
229255 consumeRequestBodyStrict request
230256 >>= handleSideLoadSnapshot putClientInput apiTransactionTimeout responseChannel
@@ -530,6 +556,33 @@ handleSubmitL2Tx putClientInput apiTransactionTimeout responseChannel body = do
530556 _ -> go
531557 Right _ -> go
532558
559+ handleHeadInitializationTime :: MonadUnliftIO m => FilePath -> m Response
560+ handleHeadInitializationTime stateFile =
561+ liftIO (doesFileExist stateFile) >>= \ case
562+ False -> pure $ responseLBS status400 jsonContent (Aeson. encode $ String $ " Could not read state file at path: " <> show stateFile)
563+ True -> do
564+ initializations <- runConduitRes $ sourceFileBS stateFile .| parseInitializingTime
565+ case initializations of
566+ [] -> pure $ responseLBS status400 jsonContent (Aeson. encode $ String " Unable to find Head initialization time in your state file." )
567+ as ->
568+ pure $ responseLBS status200 jsonContent (Aeson. encode $ List. last as)
569+
570+ parseInitializingTime :: MonadUnliftIO m => ConduitT ByteString Void m [Text ]
571+ parseInitializingTime =
572+ linesUnboundedAsciiC
573+ .| mapMC maybeDecode
574+ .| concatC
575+ .| sinkList
576+ where
577+ maybeDecode :: Monad m => ByteString -> m (Maybe Text )
578+ maybeDecode bs =
579+ case bs ^? key " stateChanged" . key " tag" . _String of
580+ Nothing -> pure Nothing
581+ Just tag ->
582+ if tag == " HeadInitialized"
583+ then pure $ bs ^? key " time" . _String
584+ else pure Nothing
585+
533586badRequest :: IsChainState tx => PostTxError tx -> Response
534587badRequest = responseLBS status400 jsonContent . Aeson. encode . toJSON
535588
0 commit comments