-
Notifications
You must be signed in to change notification settings - Fork 10
Nazrhom/raw tx #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nazrhom/raw tx #49
Changes from all commits
6dd9fde
942b157
bc89ade
b5d2e80
1d3126d
294e250
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,37 @@ | ||
| {-# LANGUAGE AllowAmbiguousTypes #-} | ||
|
|
||
| module BotPlutusInterface.Server (app, initState) where | ||
| module BotPlutusInterface.Server ( | ||
| app, | ||
| initState, | ||
| WebSocketEndpoint, | ||
| ActivateContractEndpoint, | ||
| RawTxEndpoint, | ||
| ) where | ||
|
|
||
| import BotPlutusInterface.Contract (runContract) | ||
| import BotPlutusInterface.Types ( | ||
| AppState (AppState), | ||
| ContractEnvironment (..), | ||
| ContractState (ContractState, csActivity, csObservableState), | ||
| PABConfig, | ||
| PABConfig (..), | ||
| RawTx, | ||
| SomeContractState (SomeContractState), | ||
| ) | ||
| import Control.Concurrent (ThreadId, forkIO) | ||
| import Control.Concurrent.STM (TVar, atomically, modifyTVar, newTVarIO, readTVar, readTVarIO, retry) | ||
| import Control.Monad (forever, guard, unless, void) | ||
| import Control.Monad.Error.Class (throwError) | ||
| import Control.Monad.IO.Class (liftIO) | ||
| import Data.Aeson (FromJSON, ToJSON (toJSON)) | ||
| import Data.Aeson qualified as JSON | ||
| import Data.ByteString.Lazy qualified as LBS | ||
| import Data.Either.Combinators (leftToMaybe) | ||
| import Data.Kind (Type) | ||
| import Data.Map qualified as Map | ||
| import Data.Maybe (catMaybes) | ||
| import Data.Proxy (Proxy (Proxy)) | ||
| import Data.Row (Row) | ||
| import Data.Text (Text, unpack) | ||
| import Data.UUID.V4 qualified as UUID | ||
| import Network.WebSockets ( | ||
| Connection, | ||
|
|
@@ -45,28 +55,43 @@ import Plutus.PAB.Webserver.Types ( | |
| ContractActivationArgs (..), | ||
| InstanceStatusToClient (ContractFinished, NewObservableState), | ||
| ) | ||
| import Servant.API (JSON, Post, ReqBody, (:<|>) (..), (:>)) | ||
| import Servant.API (Capture, Get, JSON, Post, ReqBody, (:<|>) (..), (:>)) | ||
| import Servant.API.WebSocket (WebSocketPending) | ||
| import Servant.Server (Application, Handler, Server, serve) | ||
| import Servant.Server (Application, Handler, Server, err404, serve) | ||
| import System.Directory (canonicalizePath, doesFileExist, makeAbsolute) | ||
| import System.FilePath (replaceExtension, takeDirectory, (</>)) | ||
| import Wallet.Types (ContractInstanceId (..)) | ||
| import Prelude | ||
|
|
||
| initState :: IO AppState | ||
| initState = AppState <$> newTVarIO Map.empty | ||
|
|
||
| -- | Mock API Schema, stripped endpoints that we don't use in this project | ||
| type API a = | ||
| ("ws" :> WebSocketPending) -- Combined websocket (subscription protocol) | ||
| :<|> ( "api" | ||
| :> "contract" | ||
| :> "activate" | ||
| :> ReqBody '[JSON] (ContractActivationArgs a) | ||
| :> Post '[JSON] ContractInstanceId -- Start a new instance. | ||
| ) | ||
| type API a = WebSocketEndpoint :<|> ActivateContractEndpoint a :<|> RawTxEndpoint | ||
|
|
||
| -- Endpoints are split up so it is easier to test them. In particular servant-client | ||
| -- can not generate a client for the WebSocketEndpoint; this allows us to still | ||
| -- use servant-client to test the other endpoints | ||
|
|
||
| type WebSocketEndpoint = "ws" :> WebSocketPending -- Combined websocket (subscription protocol) | ||
|
|
||
| type ActivateContractEndpoint a = | ||
| "api" | ||
| :> "contract" | ||
| :> "activate" | ||
| :> ReqBody '[JSON] (ContractActivationArgs a) | ||
| :> Post '[JSON] ContractInstanceId -- Start a new instance. | ||
|
|
||
| type RawTxEndpoint = | ||
| "rawTx" | ||
| :> Capture "hash" Text | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately we can not use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like orphan captures become pretty common, but also a newtype wouldn't hurt, can just unpack it in the handler pattern match |
||
| :> Get '[JSON] RawTx | ||
|
|
||
| server :: HasDefinitions t => PABConfig -> AppState -> Server (API t) | ||
| server pabConfig state = | ||
| websocketHandler state :<|> activateContractHandler pabConfig state | ||
| websocketHandler state | ||
| :<|> activateContractHandler pabConfig state | ||
| :<|> rawTxHandler pabConfig | ||
|
|
||
| apiProxy :: forall (t :: Type). Proxy (API t) | ||
| apiProxy = Proxy | ||
|
|
@@ -206,3 +231,31 @@ handleContract pabConf state@(AppState st) contract = liftIO $ do | |
| let maybeError = toJSON <$> leftToMaybe result | ||
| broadcastContractResult @w state contractInstanceID maybeError | ||
| pure contractInstanceID | ||
|
|
||
| -- | This handler will allow to retrieve raw transactions from the pcTxFileDir if pcEnableTxEndpoint is True | ||
| rawTxHandler :: PABConfig -> Text -> Handler RawTx | ||
| rawTxHandler config hash = do | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
| -- Check that endpoint is enabled | ||
| assert config.pcEnableTxEndpoint | ||
| -- Absolute path to pcTxFileDir that is specified in the config | ||
| txFolderPath <- liftIO $ makeAbsolute (unpack config.pcTxFileDir) | ||
|
|
||
| -- Add/Set .raw extension on path | ||
| let suppliedPath :: FilePath | ||
| suppliedPath = replaceExtension (txFolderPath </> "tx-" <> unpack hash) ".raw" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be a good idea to put the filename conversion into |
||
| -- Resolve path indirections | ||
| path <- liftIO $ canonicalizePath suppliedPath | ||
| -- ensure it does not try to escape txFolderPath | ||
| assert (takeDirectory path == txFolderPath) | ||
| -- ensure file exists | ||
| fileExists <- liftIO $ doesFileExist path | ||
| assert fileExists | ||
|
|
||
| contents <- liftIO $ LBS.readFile path | ||
| case JSON.decode contents of | ||
| Just rawTx -> pure rawTx | ||
| Nothing -> throwError err404 | ||
| where | ||
| assert :: Bool -> Handler () | ||
| assert True = pure () | ||
| assert False = throwError err404 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| module Spec.BotPlutusInterface.Server (tests) where | ||
|
|
||
| import BotPlutusInterface.Server (RawTxEndpoint, app, initState) | ||
| import BotPlutusInterface.Types ( | ||
| HasDefinitions (..), | ||
| PABConfig (..), | ||
| RawTx (..), | ||
| SomeBuiltin (..), | ||
| ) | ||
|
|
||
| import Playground.Types (FunctionSchema) | ||
| import Schema (FormSchema) | ||
|
|
||
| import Test.Tasty (TestTree, testGroup) | ||
| import Test.Tasty.HUnit (testCase, (@?=)) | ||
|
|
||
| import Network.HTTP.Client (defaultManagerSettings, newManager) | ||
| import Network.HTTP.Types.Status (status404) | ||
| import Network.Wai.Handler.Warp (testWithApplication) | ||
| import Servant.Client (ClientEnv, ClientError (..), client, mkClientEnv, responseStatusCode, runClientM) | ||
| import Servant.Client.Core.BaseUrl (BaseUrl (..), parseBaseUrl) | ||
|
|
||
| import Data.Aeson (FromJSON, ToJSON, encode) | ||
| import Data.ByteString.Lazy qualified as LBS | ||
| import Data.Default (def) | ||
| import Data.Proxy (Proxy (..)) | ||
| import Data.Text (Text, pack, unpack) | ||
| import Data.Void (Void, absurd) | ||
| import System.FilePath ((</>)) | ||
| import System.IO.Temp (withSystemTempDirectory) | ||
| import Prelude | ||
|
|
||
| type RawTxEndpointResponse = Either ClientError RawTx | ||
| type RawTxTest a = (Text -> IO RawTxEndpointResponse) -> IO a | ||
|
|
||
| tests :: TestTree | ||
| tests = | ||
| testGroup | ||
| "BotPlutusInterface.Server" | ||
| [ rawTxTests | ||
| ] | ||
|
|
||
| rawTxTests :: TestTree | ||
| rawTxTests = | ||
| testGroup | ||
| "rawTx" | ||
| [ testCase "Can fetch valid tx file" fetchTx | ||
| , testCase "If an extension is supplied, it is replaced by .raw" fetchSignedTx | ||
| , testCase "Unable to fetch outside tx folder" fetchOutsideTxFolder | ||
| , testCase "Returns 404 for valid request when the endpoint is disabled" fetchWithDefaultConfig | ||
| ] | ||
| where | ||
| fetchTx :: IO () | ||
| fetchTx = do | ||
| initServerAndClient enableTxEndpointConfig $ \runRawTxClient -> do | ||
| result <- runRawTxClient txHash | ||
| result @?= Right rawTx | ||
|
|
||
| fetchSignedTx :: IO () | ||
| fetchSignedTx = do | ||
| initServerAndClient enableTxEndpointConfig $ \runRawTxClient -> do | ||
| result <- runRawTxClient $ txHash <> ".signed" | ||
| result @?= Right rawTx | ||
|
|
||
| fetchOutsideTxFolder :: IO () | ||
| fetchOutsideTxFolder = do | ||
| initServerAndClient enableTxEndpointConfig $ \runRawTxClient -> do | ||
| Left (FailureResponse _ res) <- runRawTxClient "../somefile" | ||
| responseStatusCode res @?= status404 | ||
|
|
||
| fetchWithDefaultConfig :: IO () | ||
| fetchWithDefaultConfig = do | ||
| initServerAndClient def $ \runRawTxClient -> do | ||
| Left (FailureResponse _ res) <- runRawTxClient txHash | ||
| responseStatusCode res @?= status404 | ||
|
|
||
| txProxy :: Proxy RawTxEndpoint | ||
| txProxy = Proxy | ||
|
|
||
| initServerAndClient :: PABConfig -> RawTxTest a -> IO a | ||
| initServerAndClient config test = do | ||
| withSystemTempDirectory "tx" $ \path -> do | ||
| let pabConfig :: PABConfig | ||
| pabConfig = config {pcTxFileDir = pack path} | ||
| state <- initState | ||
| LBS.writeFile (path </> txFileName) txFileContents | ||
| testWithApplication (pure $ app @EmptyContract pabConfig state) (initClientOnPort test) | ||
| where | ||
| initClientOnPort :: RawTxTest a -> Int -> IO a | ||
| initClientOnPort testToRun port = do | ||
| baseUrl <- parseBaseUrl "http://localhost" | ||
| manager <- newManager defaultManagerSettings | ||
|
|
||
| let clientEnv :: ClientEnv | ||
| clientEnv = mkClientEnv manager $ baseUrl {baseUrlPort = port} | ||
|
|
||
| runRawTxClient :: Text -> IO RawTxEndpointResponse | ||
| runRawTxClient hash = runClientM (client txProxy hash) clientEnv | ||
|
|
||
| testToRun runRawTxClient | ||
|
|
||
| txHash :: Text | ||
| txHash = "test" | ||
|
|
||
| txFileName :: FilePath | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once we have the filename conversion in Files, it could be imported from here |
||
| txFileName = "tx-" <> unpack txHash <> ".raw" | ||
|
|
||
| rawTx :: RawTx | ||
| rawTx = | ||
| RawTx | ||
| { _type = "TxBodyAlonzo" | ||
| , _description = "description" | ||
| , _cborHex = "hex" | ||
| } | ||
|
|
||
| txFileContents :: LBS.ByteString | ||
| txFileContents = encode rawTx | ||
|
|
||
| enableTxEndpointConfig :: PABConfig | ||
| enableTxEndpointConfig = def {pcEnableTxEndpoint = True} | ||
|
|
||
| -- Since we are not testing the contract endpoints we just use a newtype around Void as a Contract | ||
| newtype EmptyContract = EmptyContract {unEmptyContract :: Void} | ||
| deriving newtype (FromJSON, ToJSON) | ||
|
|
||
| instance HasDefinitions EmptyContract where | ||
| getDefinitions :: [EmptyContract] | ||
| getDefinitions = [] | ||
|
|
||
| getSchema :: EmptyContract -> [FunctionSchema FormSchema] | ||
| getSchema = absurd . unEmptyContract | ||
|
|
||
| getContract :: (EmptyContract -> SomeBuiltin) | ||
| getContract = absurd . unEmptyContract | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should avoid using camel case in the URL, how about using dash-case instead?