11{-# LANGUAGE AllowAmbiguousTypes #-}
22
3- module BotPlutusInterface.Server (app , initState ) where
3+ module BotPlutusInterface.Server (
4+ app ,
5+ initState ,
6+ WebSocketEndpoint ,
7+ ActivateContractEndpoint ,
8+ RawTxEndpoint ,
9+ ) where
410
511import BotPlutusInterface.Contract (runContract )
612import BotPlutusInterface.Types (
713 AppState (AppState ),
814 ContractEnvironment (.. ),
915 ContractState (ContractState , csActivity , csObservableState ),
10- PABConfig ,
16+ PABConfig (.. ),
17+ RawTx ,
1118 SomeContractState (SomeContractState ),
1219 )
1320import Control.Concurrent (ThreadId , forkIO )
1421import Control.Concurrent.STM (TVar , atomically , modifyTVar , newTVarIO , readTVar , readTVarIO , retry )
1522import Control.Monad (forever , guard , unless , void )
23+ import Control.Monad.Error.Class (throwError )
1624import Control.Monad.IO.Class (liftIO )
1725import Data.Aeson (FromJSON , ToJSON (toJSON ))
1826import Data.Aeson qualified as JSON
27+ import Data.ByteString.Lazy qualified as LBS
1928import Data.Either.Combinators (leftToMaybe )
2029import Data.Kind (Type )
2130import Data.Map qualified as Map
2231import Data.Maybe (catMaybes )
2332import Data.Proxy (Proxy (Proxy ))
2433import Data.Row (Row )
34+ import Data.Text (Text , unpack )
2535import Data.UUID.V4 qualified as UUID
2636import Network.WebSockets (
2737 Connection ,
@@ -45,28 +55,43 @@ import Plutus.PAB.Webserver.Types (
4555 ContractActivationArgs (.. ),
4656 InstanceStatusToClient (ContractFinished , NewObservableState ),
4757 )
48- import Servant.API (JSON , Post , ReqBody , (:<|>) (.. ), (:>) )
58+ import Servant.API (Capture , Get , JSON , Post , ReqBody , (:<|>) (.. ), (:>) )
4959import Servant.API.WebSocket (WebSocketPending )
50- import Servant.Server (Application , Handler , Server , serve )
60+ import Servant.Server (Application , Handler , Server , err404 , serve )
61+ import System.Directory (canonicalizePath , doesFileExist , makeAbsolute )
62+ import System.FilePath (replaceExtension , takeDirectory , (</>) )
5163import Wallet.Types (ContractInstanceId (.. ))
5264import Prelude
5365
5466initState :: IO AppState
5567initState = AppState <$> newTVarIO Map. empty
5668
5769-- | Mock API Schema, stripped endpoints that we don't use in this project
58- type API a =
59- (" ws" :> WebSocketPending ) -- Combined websocket (subscription protocol)
60- :<|> ( " api"
61- :> " contract"
62- :> " activate"
63- :> ReqBody '[JSON ] (ContractActivationArgs a )
64- :> Post '[JSON ] ContractInstanceId -- Start a new instance.
65- )
70+ type API a = WebSocketEndpoint :<|> ActivateContractEndpoint a :<|> RawTxEndpoint
71+
72+ -- Endpoints are split up so it is easier to test them. In particular servant-client
73+ -- can not generate a client for the WebSocketEndpoint; this allows us to still
74+ -- use servant-client to test the other endpoints
75+
76+ type WebSocketEndpoint = " ws" :> WebSocketPending -- Combined websocket (subscription protocol)
77+
78+ type ActivateContractEndpoint a =
79+ " api"
80+ :> " contract"
81+ :> " activate"
82+ :> ReqBody '[JSON ] (ContractActivationArgs a )
83+ :> Post '[JSON ] ContractInstanceId -- Start a new instance.
84+
85+ type RawTxEndpoint =
86+ " rawTx"
87+ :> Capture " hash" Text
88+ :> Get '[JSON ] RawTx
6689
6790server :: HasDefinitions t => PABConfig -> AppState -> Server (API t )
6891server pabConfig state =
69- websocketHandler state :<|> activateContractHandler pabConfig state
92+ websocketHandler state
93+ :<|> activateContractHandler pabConfig state
94+ :<|> rawTxHandler pabConfig
7095
7196apiProxy :: forall (t :: Type ). Proxy (API t )
7297apiProxy = Proxy
@@ -206,3 +231,31 @@ handleContract pabConf state@(AppState st) contract = liftIO $ do
206231 let maybeError = toJSON <$> leftToMaybe result
207232 broadcastContractResult @ w state contractInstanceID maybeError
208233 pure contractInstanceID
234+
235+ -- | This handler will allow to retrieve raw transactions from the pcTxFileDir if pcEnableTxEndpoint is True
236+ rawTxHandler :: PABConfig -> Text -> Handler RawTx
237+ rawTxHandler config hash = do
238+ -- Check that endpoint is enabled
239+ assert config. pcEnableTxEndpoint
240+ -- Absolute path to pcTxFileDir that is specified in the config
241+ txFolderPath <- liftIO $ makeAbsolute (unpack config. pcTxFileDir)
242+
243+ -- Add/Set .raw extension on path
244+ let suppliedPath :: FilePath
245+ suppliedPath = replaceExtension (txFolderPath </> " tx-" <> unpack hash) " .raw"
246+ -- Resolve path indirections
247+ path <- liftIO $ canonicalizePath suppliedPath
248+ -- ensure it does not try to escape txFolderPath
249+ assert (takeDirectory path == txFolderPath)
250+ -- ensure file exists
251+ fileExists <- liftIO $ doesFileExist path
252+ assert fileExists
253+
254+ contents <- liftIO $ LBS. readFile path
255+ case JSON. decode contents of
256+ Just rawTx -> pure rawTx
257+ Nothing -> throwError err404
258+ where
259+ assert :: Bool -> Handler ()
260+ assert True = pure ()
261+ assert False = throwError err404
0 commit comments