Skip to content

Commit

Permalink
Add Game contract and test
Browse files Browse the repository at this point in the history
  • Loading branch information
silky committed Apr 6, 2021
1 parent 63bd0c3 commit eb8733f
Show file tree
Hide file tree
Showing 6 changed files with 498 additions and 6 deletions.
11 changes: 7 additions & 4 deletions cabal.project
Expand Up @@ -14,14 +14,15 @@ source-repository-package
location: https://github.com/input-output-hk/plutus.git
subdir:
freer-extras
playground-common
plutus-core
plutus-contract
plutus-ledger
plutus-ledger-api
plutus-tx
plutus-tx-plugin
prettyprinter-configurable
tag: 026b65532514bb3bf59d35631637146811f6c245
prettyprinter-configurable
tag: 41c04ab2d705979b5a69c9c5b1d67552a23d81c4

-- The following sections are copied from the 'plutus' repository cabal.project at the revision
-- given above.
Expand All @@ -32,7 +33,7 @@ source-repository-package
package eventful-sql-common
ghc-options: -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances

allow-newer:
allow-newer:
-- Has a commit to allow newer aeson, not on Hackage yet
monoidal-containers:aeson
-- Pins to an old version of Template Haskell, unclear if/when it will be updated
Expand All @@ -46,6 +47,8 @@ constraints:
-- aws-lambda-haskell-runtime-wai doesn't compile with newer versions
aws-lambda-haskell-runtime <= 3.0.3

extra-packages: ieee, filemanip

-- Needs some patches, but upstream seems to be fairly dead (no activity in > 1 year)
source-repository-package
type: git
Expand Down Expand Up @@ -84,7 +87,7 @@ source-repository-package
location: https://github.com/input-output-hk/cardano-prelude
tag: ee4e7b547a991876e6b05ba542f4e62909f4a571
subdir:
cardano-prelude
cardano-prelude
cardano-prelude-test

source-repository-package
Expand Down
162 changes: 162 additions & 0 deletions examples/src/Plutus/Contracts/Game.hs
@@ -0,0 +1,162 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-ignore-interface-pragmas #-}
-- | A guessing game
module Plutus.Contracts.Game
( lock
, guess
, game
, GameSchema
, GuessParams(..)
, LockParams(..)
-- * Scripts
, gameValidator
, hashString
, clearString
-- * Address
, gameAddress
, validateGuess
-- * Traces
, guessTrace
, guessWrongTrace
, lockTrace
) where

import Control.Monad (void)
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
import Ledger (Address, Validator, ValidatorCtx, Value)
import qualified Ledger.Constraints as Constraints
import qualified Ledger.Typed.Scripts as Scripts
import Plutus.Contract
import Plutus.Contract.Schema ()
import Plutus.Trace.Emulator (EmulatorTrace)
import qualified Plutus.Trace.Emulator as Trace
import qualified PlutusTx
import PlutusTx.Prelude
import Schema (ToArgument, ToSchema)
import Wallet.Emulator (Wallet (..))

import qualified Ledger
import qualified Ledger.Ada as Ada

import qualified Data.ByteString.Char8 as C
import qualified Prelude

newtype HashedString = HashedString ByteString deriving newtype PlutusTx.IsData

PlutusTx.makeLift ''HashedString

newtype ClearString = ClearString ByteString deriving newtype PlutusTx.IsData

PlutusTx.makeLift ''ClearString

type GameSchema =
BlockchainActions
.\/ Endpoint "lock" LockParams
.\/ Endpoint "guess" GuessParams

-- | The validation function (DataValue -> RedeemerValue -> ValidatorCtx -> Bool)
validateGuess :: HashedString -> ClearString -> ValidatorCtx -> Bool
validateGuess (HashedString actual) (ClearString guess') _ = actual == sha2_256 guess'

-- | The validator script of the game.
gameValidator :: Validator
gameValidator = Scripts.validatorScript gameInstance

data Game
instance Scripts.ScriptType Game where
type instance RedeemerType Game = ClearString
type instance DatumType Game = HashedString

gameInstance :: Scripts.ScriptInstance Game
gameInstance = Scripts.validator @Game
$$(PlutusTx.compile [|| validateGuess ||])
$$(PlutusTx.compile [|| wrap ||]) where
wrap = Scripts.wrapValidator @HashedString @ClearString

-- create a data script for the guessing game by hashing the string
-- and lifting the hash to its on-chain representation
hashString :: String -> HashedString
hashString = HashedString . sha2_256 . C.pack

-- create a redeemer script for the guessing game by lifting the
-- string to its on-chain representation
clearString :: String -> ClearString
clearString = ClearString . C.pack

-- | The address of the game (the hash of its validator script)
gameAddress :: Address
gameAddress = Ledger.scriptAddress gameValidator

-- | Parameters for the "lock" endpoint
data LockParams = LockParams
{ secretWord :: String
, amount :: Value
}
deriving stock (Prelude.Eq, Prelude.Show, Generic)
deriving anyclass (FromJSON, ToJSON, ToSchema, ToArgument)

-- | Parameters for the "guess" endpoint
newtype GuessParams = GuessParams
{ guessWord :: String
}
deriving stock (Prelude.Eq, Prelude.Show, Generic)
deriving anyclass (FromJSON, ToJSON, ToSchema, ToArgument)

lock :: AsContractError e => Contract () GameSchema e ()
lock = do
LockParams secret amt <- endpoint @"lock" @LockParams
let tx = Constraints.mustPayToTheScript (hashString secret) amt
void (submitTxConstraints gameInstance tx)

guess :: AsContractError e => Contract () GameSchema e ()
guess = do
GuessParams theGuess <- endpoint @"guess" @GuessParams
unspentOutputs <- utxoAt gameAddress
let redeemer = clearString theGuess
tx = collectFromScript unspentOutputs redeemer
void (submitTxConstraintsSpending gameInstance unspentOutputs tx)

game :: AsContractError e => Contract () GameSchema e ()
game = lock `select` guess

lockTrace :: EmulatorTrace ()
lockTrace = do
let w1 = Wallet 1
hdl <- Trace.activateContractWallet w1 (game @ContractError)
Trace.callEndpoint @"lock" hdl (LockParams "secret" (Ada.lovelaceValueOf 10))
void $ Trace.waitNSlots 1

guessTrace :: EmulatorTrace ()
guessTrace = do
lockTrace
let w2 = Wallet 2
hdl <- Trace.activateContractWallet w2 (game @ContractError)
Trace.callEndpoint @"guess" hdl (GuessParams "secret")

guessWrongTrace :: EmulatorTrace ()
guessWrongTrace = do
lockTrace
let w2 = Wallet 2
hdl <- Trace.activateContractWallet w2 (game @ContractError)
Trace.callEndpoint @"guess" hdl (GuessParams "SECRET")
21 changes: 21 additions & 0 deletions examples/test/Spec.hs
@@ -0,0 +1,21 @@
{-# LANGUAGE OverloadedStrings #-}
module Main(main) where

import qualified Spec.Game
import Test.Tasty
import Test.Tasty.Hedgehog (HedgehogTestLimit (..))

main :: IO ()
main = defaultMain tests

-- | Number of successful tests for each hedgehog property.
-- The default is 100 but we use a smaller number here in order to speed up
-- the test suite.
--
limit :: HedgehogTestLimit
limit = HedgehogTestLimit (Just 5)

tests :: TestTree
tests = localOption limit $ testGroup "use cases" [
Spec.Game.tests
]
60 changes: 60 additions & 0 deletions examples/test/Spec/Game.hs
@@ -0,0 +1,60 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

module Spec.Game
( tests
) where

import Control.Monad (void)
import Ledger.Ada (adaValueOf)
import Plutus.Contract (Contract, ContractError)
import Plutus.Contract.Test
import Plutus.Contracts.Game
import Plutus.Trace.Emulator (ContractInstanceTag)
import qualified Plutus.Trace.Emulator as Trace
import qualified PlutusTx
import qualified PlutusTx.Prelude as PlutusTx
import Test.Tasty
import qualified Test.Tasty.HUnit as HUnit

w1, w2 :: Wallet
w1 = Wallet 1
w2 = Wallet 2

t1, t2 :: ContractInstanceTag
t1 = Trace.walletInstanceTag w1
t2 = Trace.walletInstanceTag w2

theContract :: Contract () GameSchema ContractError ()
theContract = game

tests :: TestTree
tests = testGroup "game"
[ checkPredicate "Expose 'lock' and 'guess' endpoints"
(endpointAvailable @"lock" theContract (Trace.walletInstanceTag w1)
.&&. endpointAvailable @"guess" theContract (Trace.walletInstanceTag w1))
$ void (Trace.activateContractWallet w1 theContract)

, checkPredicate "'lock' endpoint submits a transaction"
(anyTx theContract (Trace.walletInstanceTag w1))
$ do
hdl <- Trace.activateContractWallet w1 theContract
Trace.callEndpoint @"lock" hdl (LockParams "secret" (adaValueOf 10))

, checkPredicate "'guess' endpoint is available after locking funds"
(endpointAvailable @"guess" theContract (Trace.walletInstanceTag w2))
lockTrace

, checkPredicate "guess right (unlock funds)"
(walletFundsChange w2 (1 `timesFeeAdjust` 10)
.&&. walletFundsChange w1 (1 `timesFeeAdjust` (-10)))
guessTrace

, checkPredicate "guess wrong"
(walletFundsChange w2 PlutusTx.zero
.&&. walletFundsChange w1 (1 `timesFeeAdjust` (-10)))
guessWrongTrace
, goldenPir "examples/test/Spec/game.pir" $$(PlutusTx.compile [|| validateGuess ||])
, HUnit.testCase "script size is reasonable" (reasonable gameValidator 20000)
]

0 comments on commit eb8733f

Please sign in to comment.