Skip to content

Commit cdd4c0a

Browse files
Merge pull request #57 from mlabs-haskell/sam/late-raw-tx-changes
Convert Text to TxId, update examples
2 parents c95b103 + 45df2cc commit cdd4c0a

File tree

10 files changed

+99
-45
lines changed

10 files changed

+99
-45
lines changed

.github/workflows/integrate.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ jobs:
6161
extra_nix_config: |
6262
trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= iohk.cachix.org-1:DpRUyj7h7V830dp/i6Nti+NEO2/nhblbov/8MW7Rqoo= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
6363
substituters = https://hydra.iohk.io https://iohk.cachix.org https://cache.nixos.org/
64+
extra-experimental-features = nix-command flakes
6465
- uses: cachix/cachix-action@v10
6566
with:
6667
name: mlabs
@@ -75,4 +76,4 @@ jobs:
7576
dist-newstyle
7677
key: ${{ runner.os }}-cabal
7778
- name: Build the full ci derivation
78-
run: nix build -L .#check.x86_64-linux --extra-experimental-features nix-command --extra-experimental-features flakes
79+
run: make nix_build

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# In most cases you should execute Make after entering nix-shell.
44

55
.PHONY: hoogle pab_servers_all pab_servers_all pab_db clean_db \
6-
build test accept_pirs watch ghci readme_contents \
6+
nix_build build test accept_pirs watch ghci readme_contents \
77
format lint requires_nix_shell
88

99
usage:
@@ -16,6 +16,7 @@ usage:
1616
@echo
1717
@echo "Available commands:"
1818
@echo " hoogle -- Start local hoogle"
19+
@echo " nix_build -- Run nix build -L on all targets"
1920
@echo " build -- Run cabal v2-build"
2021
@echo " watch -- Track files: bot-plutus-interface.cabal, src/* and run 'make build' on change"
2122
@echo " test -- Run cabal v2-test"
@@ -44,6 +45,9 @@ ifdef FLAGS
4445
GHC_FLAGS = --ghc-options "$(FLAGS)"
4546
endif
4647

48+
nix_build:
49+
nix build -L .#check.x86_64-linux .#plutus-transfer:exe:plutus-transfer-pab .#plutus-game:exe:plutus-game-pab .#plutus-nft:exe:plutus-nft-pab
50+
4751
build: requires_nix_shell
4852
cabal v2-build $(GHC_FLAGS)
4953

bot-plutus-interface.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ library
118118
, plutus-tx
119119
, plutus-tx-plugin
120120
, process
121+
, QuickCheck
121122
, row-types
122123
, serialise
123124
, servant
@@ -186,6 +187,7 @@ test-suite bot-plutus-interface-test
186187
, quickcheck-instances
187188
, row-types
188189
, serialise
190+
, servant
189191
, servant-client
190192
, servant-client-core
191193
, stm

cabal.project

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
index-state: 2021-10-20T00:00:00Z
33

44
packages: ./.
5+
./examples/plutus-game/plutus-game.cabal
6+
./examples/plutus-transfer/plutus-transfer.cabal
7+
./examples/plutus-nft/plutus-nft.cabal
58

69
-- You never, ever, want this.
710
write-ghc-environment-files: never
811

912
-- Always build tests and benchmarks.
1013
tests: true
1114
benchmarks: true
12-

examples/plutus-game/app/Main.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ main = do
6666
, pcDryRun = True
6767
, pcLogLevel = Debug
6868
, pcProtocolParamsFile = "./protocol.json"
69-
, pcEnableTxEndpoint = False
69+
, pcEnableTxEndpoint = True
7070
}
7171
BotPlutusInterface.runPAB @GameContracts pabConf

examples/plutus-nft/app/Main.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ main = do
6666
, pcDryRun = True
6767
, pcLogLevel = Debug
6868
, pcProtocolParamsFile = "./protocol.json"
69-
, pcEnableTxEndpoint = False
69+
, pcEnableTxEndpoint = True
7070
}
7171
BotPlutusInterface.runPAB @MintNFTContracts pabConf

examples/plutus-transfer/app/Main.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,6 @@ main = do
6565
, pcDryRun = True
6666
, pcLogLevel = Debug
6767
, pcProtocolParamsFile = "./protocol.json"
68+
, pcEnableTxEndpoint = True
6869
}
6970
BotPlutusInterface.runPAB @TransferContracts pabConf

src/BotPlutusInterface/Files.hs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module BotPlutusInterface.Files (
99
signingKeyFilePath,
1010
txFilePath,
1111
txFileName,
12+
txIdToText,
1213
writeAll,
1314
writePolicyScriptFile,
1415
redeemerJsonFilePath,
@@ -84,7 +85,7 @@ import Plutus.V1.Ledger.Api (
8485
)
8586
import PlutusTx (ToData, toData)
8687
import PlutusTx.Builtins (fromBuiltin)
87-
import System.FilePath (replaceExtension, takeExtension, (</>))
88+
import System.FilePath (takeExtension, (</>))
8889
import Prelude
8990

9091
-- | Filename of a minting policy script
@@ -115,12 +116,13 @@ signingKeyFilePath pabConf (PubKeyHash pubKeyHash) =
115116
in pabConf.pcSigningKeyFileDir <> "/signing-key-" <> h <> ".skey"
116117

117118
txFilePath :: PABConfig -> Text -> Tx.Tx -> Text
118-
txFilePath pabConf ext tx =
119-
let txId = encodeByteString $ fromBuiltin $ TxId.getTxId $ Tx.txId tx
120-
in pabConf.pcTxFileDir <> "/" <> txFileName txId ext
119+
txFilePath pabConf ext tx = pabConf.pcTxFileDir <> "/" <> txFileName (Tx.txId tx) ext
121120

122-
txFileName :: Text -> Text -> Text
123-
txFileName name ext = Text.pack $ replaceExtension ("tx-" <> Text.unpack name) (Text.unpack ext)
121+
txFileName :: TxId.TxId -> Text -> Text
122+
txFileName txId ext = "tx-" <> txIdToText txId <> "." <> ext
123+
124+
txIdToText :: TxId.TxId -> Text
125+
txIdToText = encodeByteString . fromBuiltin . TxId.getTxId
124126

125127
-- | Compiles and writes a script file under the given folder
126128
writePolicyScriptFile ::

src/BotPlutusInterface/Server.hs

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ module BotPlutusInterface.Server (
66
WebSocketEndpoint,
77
ActivateContractEndpoint,
88
RawTxEndpoint,
9+
TxIdCapture (TxIdCapture),
910
) where
1011

1112
import BotPlutusInterface.Contract (runContract)
12-
import BotPlutusInterface.Files (txFileName)
13+
import BotPlutusInterface.Files (txFileName, txIdToText)
1314
import BotPlutusInterface.Types (
1415
AppState (AppState),
1516
ContractEnvironment (..),
@@ -25,15 +26,19 @@ import Control.Monad.Error.Class (throwError)
2526
import Control.Monad.IO.Class (liftIO)
2627
import Data.Aeson (FromJSON, ToJSON (toJSON))
2728
import Data.Aeson qualified as JSON
29+
import Data.Bifunctor (bimap)
2830
import Data.ByteString.Lazy qualified as LBS
2931
import Data.Either.Combinators (leftToMaybe)
3032
import Data.Kind (Type)
3133
import Data.Map qualified as Map
3234
import Data.Maybe (catMaybes)
3335
import Data.Proxy (Proxy (Proxy))
3436
import Data.Row (Row)
35-
import Data.Text (Text, unpack)
37+
import Data.String (fromString)
38+
import Data.Text (Text, pack, unpack)
39+
import Data.Text.Encoding (encodeUtf8)
3640
import Data.UUID.V4 qualified as UUID
41+
import Ledger.TxId (TxId (TxId))
3742
import Network.WebSockets (
3843
Connection,
3944
PendingConnection,
@@ -56,11 +61,24 @@ import Plutus.PAB.Webserver.Types (
5661
ContractActivationArgs (..),
5762
InstanceStatusToClient (ContractFinished, NewObservableState),
5863
)
59-
import Servant.API (Capture, Get, JSON, Post, ReqBody, (:<|>) (..), (:>))
64+
import Plutus.V1.Ledger.Bytes (LedgerBytes (LedgerBytes), fromHex)
65+
import PlutusTx.Prelude (lengthOfByteString)
66+
import Servant.API (
67+
Capture,
68+
FromHttpApiData (parseUrlPiece),
69+
Get,
70+
JSON,
71+
Post,
72+
ReqBody,
73+
ToHttpApiData (toUrlPiece),
74+
(:<|>) ((:<|>)),
75+
(:>),
76+
)
6077
import Servant.API.WebSocket (WebSocketPending)
6178
import Servant.Server (Application, Handler, Server, err404, serve)
62-
import System.Directory (canonicalizePath, doesFileExist, makeAbsolute)
63-
import System.FilePath (takeDirectory, (</>))
79+
import System.Directory (doesFileExist, makeAbsolute)
80+
import System.FilePath ((</>))
81+
import Test.QuickCheck (Arbitrary (arbitrary), elements, vectorOf)
6482
import Wallet.Types (ContractInstanceId (..))
6583
import Prelude
6684

@@ -85,9 +103,30 @@ type ActivateContractEndpoint a =
85103

86104
type RawTxEndpoint =
87105
"raw-tx"
88-
:> Capture "txId" Text
106+
:> Capture "tx-id" TxIdCapture
89107
:> Get '[JSON] RawTx
90108

109+
newtype TxIdCapture = TxIdCapture TxId
110+
deriving newtype (Eq, Show)
111+
112+
instance FromHttpApiData TxIdCapture where
113+
parseUrlPiece :: Text -> Either Text TxIdCapture
114+
parseUrlPiece t = bimap pack bytesToTxIdCapture $ checkLength =<< fromHex (encodeUtf8 t)
115+
where
116+
checkLength :: LedgerBytes -> Either String LedgerBytes
117+
checkLength b@(LedgerBytes bs) =
118+
if lengthOfByteString bs == 32
119+
then Right b
120+
else Left "Invalid length"
121+
bytesToTxIdCapture :: LedgerBytes -> TxIdCapture
122+
bytesToTxIdCapture (LedgerBytes b) = TxIdCapture $ TxId b
123+
124+
instance ToHttpApiData TxIdCapture where
125+
toUrlPiece (TxIdCapture txId) = txIdToText txId
126+
127+
instance Arbitrary TxIdCapture where
128+
arbitrary = TxIdCapture . fromString <$> vectorOf 64 (elements "0123456789abcdefABCDEF")
129+
91130
server :: HasDefinitions t => PABConfig -> AppState -> Server (API t)
92131
server pabConfig state =
93132
websocketHandler state
@@ -234,20 +273,17 @@ handleContract pabConf state@(AppState st) contract = liftIO $ do
234273
pure contractInstanceID
235274

236275
-- | This handler will allow to retrieve raw transactions from the pcTxFileDir if pcEnableTxEndpoint is True
237-
rawTxHandler :: PABConfig -> Text -> Handler RawTx
238-
rawTxHandler config txId = do
276+
rawTxHandler :: PABConfig -> TxIdCapture -> Handler RawTx
277+
rawTxHandler config (TxIdCapture txId) = do
239278
-- Check that endpoint is enabled
240279
assert config.pcEnableTxEndpoint
241280
-- Absolute path to pcTxFileDir that is specified in the config
242281
txFolderPath <- liftIO $ makeAbsolute (unpack config.pcTxFileDir)
243282

244-
-- Add/Set .raw extension on path
245-
let suppliedPath :: FilePath
246-
suppliedPath = txFolderPath </> unpack (txFileName txId ".raw")
247-
-- Resolve path indirections
248-
path <- liftIO $ canonicalizePath suppliedPath
249-
-- ensure it does not try to escape txFolderPath
250-
assert (takeDirectory path == txFolderPath)
283+
-- Create full path
284+
let path :: FilePath
285+
path = txFolderPath </> unpack (txFileName txId "raw")
286+
251287
-- ensure file exists
252288
fileExists <- liftIO $ doesFileExist path
253289
assert fileExists

test/Spec/BotPlutusInterface/Server.hs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,44 @@
11
module Spec.BotPlutusInterface.Server (tests) where
22

33
import BotPlutusInterface.Files (txFileName)
4-
import BotPlutusInterface.Server (RawTxEndpoint, app, initState)
4+
import BotPlutusInterface.Server (RawTxEndpoint, TxIdCapture (TxIdCapture), app, initState)
55
import BotPlutusInterface.Types (
66
HasDefinitions (..),
77
PABConfig (..),
88
RawTx (..),
99
SomeBuiltin (..),
1010
)
1111

12+
import Ledger.TxId (TxId)
1213
import Playground.Types (FunctionSchema)
1314
import Schema (FormSchema)
1415

1516
import Test.Tasty (TestTree, testGroup)
1617
import Test.Tasty.HUnit (testCase, (@?=))
18+
import Test.Tasty.QuickCheck (Property, testProperty, (===))
1719

1820
import Network.HTTP.Client (defaultManagerSettings, newManager)
1921
import Network.HTTP.Types.Status (status404)
2022
import Network.Wai.Handler.Warp (testWithApplication)
23+
import Servant.API (
24+
FromHttpApiData (parseUrlPiece),
25+
ToHttpApiData (toUrlPiece),
26+
)
2127
import Servant.Client (ClientEnv, ClientError (..), client, mkClientEnv, responseStatusCode, runClientM)
2228
import Servant.Client.Core.BaseUrl (BaseUrl (..), parseBaseUrl)
2329

2430
import Data.Aeson (FromJSON, ToJSON, encode)
2531
import Data.ByteString.Lazy qualified as LBS
2632
import Data.Default (def)
2733
import Data.Proxy (Proxy (..))
28-
import Data.Text (Text, pack, unpack)
34+
import Data.Text (pack, unpack)
2935
import Data.Void (Void, absurd)
3036
import System.FilePath ((</>))
3137
import System.IO.Temp (withSystemTempDirectory)
3238
import Prelude
3339

3440
type RawTxEndpointResponse = Either ClientError RawTx
35-
type RawTxTest a = (Text -> IO RawTxEndpointResponse) -> IO a
41+
type RawTxTest a = (TxId -> IO RawTxEndpointResponse) -> IO a
3642

3743
tests :: TestTree
3844
tests =
@@ -46,9 +52,9 @@ rawTxTests =
4652
testGroup
4753
"rawTx"
4854
[ testCase "Can fetch valid tx file" fetchTx
49-
, testCase "If an extension is supplied, it is replaced by .raw" fetchSignedTx
50-
, testCase "Unable to fetch outside tx folder" fetchOutsideTxFolder
55+
, testCase "Returns 404 for missing txs" fetchMissingTx
5156
, testCase "Returns 404 for valid request when the endpoint is disabled" fetchWithDefaultConfig
57+
, testProperty "TxId URL encoding reversible" txIdReversible
5258
]
5359
where
5460
fetchTx :: IO ()
@@ -57,16 +63,10 @@ rawTxTests =
5763
result <- runRawTxClient txHash
5864
result @?= Right rawTx
5965

60-
fetchSignedTx :: IO ()
61-
fetchSignedTx = do
62-
initServerAndClient enableTxEndpointConfig $ \runRawTxClient -> do
63-
result <- runRawTxClient $ txHash <> ".signed"
64-
result @?= Right rawTx
65-
66-
fetchOutsideTxFolder :: IO ()
67-
fetchOutsideTxFolder = do
66+
fetchMissingTx :: IO ()
67+
fetchMissingTx = do
6868
initServerAndClient enableTxEndpointConfig $ \runRawTxClient -> do
69-
Left (FailureResponse _ res) <- runRawTxClient "../somefile"
69+
Left (FailureResponse _ res) <- runRawTxClient txHash2
7070
responseStatusCode res @?= status404
7171

7272
fetchWithDefaultConfig :: IO ()
@@ -75,6 +75,9 @@ rawTxTests =
7575
Left (FailureResponse _ res) <- runRawTxClient txHash
7676
responseStatusCode res @?= status404
7777

78+
txIdReversible :: TxIdCapture -> Property
79+
txIdReversible txId = parseUrlPiece (toUrlPiece txId) === Right txId
80+
7881
txProxy :: Proxy RawTxEndpoint
7982
txProxy = Proxy
8083

@@ -95,16 +98,19 @@ initServerAndClient config test = do
9598
let clientEnv :: ClientEnv
9699
clientEnv = mkClientEnv manager $ baseUrl {baseUrlPort = port}
97100

98-
runRawTxClient :: Text -> IO RawTxEndpointResponse
99-
runRawTxClient hash = runClientM (client txProxy hash) clientEnv
101+
runRawTxClient :: TxId -> IO RawTxEndpointResponse
102+
runRawTxClient txId = runClientM (client txProxy (TxIdCapture txId)) clientEnv
100103

101104
testToRun runRawTxClient
102105

103-
txHash :: Text
104-
txHash = "test"
106+
txHash :: TxId
107+
txHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
108+
109+
txHash2 :: TxId
110+
txHash2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
105111

106112
testTxFileName :: FilePath
107-
testTxFileName = unpack $ txFileName txHash ".raw"
113+
testTxFileName = unpack $ txFileName txHash "raw"
108114

109115
rawTx :: RawTx
110116
rawTx =

0 commit comments

Comments
 (0)