Skip to content

Instantly share code, notes, and snippets.

@amos-kibet
Created May 8, 2022 07:14
Show Gist options
  • Save amos-kibet/8c11d93a808203d89d92592e6382fee1 to your computer and use it in GitHub Desktop.
Save amos-kibet/8c11d93a808203d89d92592e6382fee1 to your computer and use it in GitHub Desktop.
Plutus Playground Smart Contract
-- A game with two players. Player 1 thinks of a secret word
-- and uses its hash, and the game validator script, to lock
-- some funds (the prize) in a pay-to-script transaction output.
-- Player 2 guesses the word by attempting to spend the transaction
-- output. If the guess is correct, the validator script releases the funds.
-- If it isn't, the funds stay locked.
import Control.Monad (void)
import Data.ByteString.Char8 qualified as C
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (catMaybes)
import Ledger (Address, Datum (Datum), ScriptContext, Validator, Value)
import Ledger qualified
import Ledger.Ada qualified as Ada
import Ledger.Constraints qualified as Constraints
import Ledger.Tx (ChainIndexTxOut (..))
import Ledger.Typed.Scripts qualified as Scripts
import Playground.Contract
import Plutus.Contract
import PlutusTx qualified
import PlutusTx.Prelude hiding (pure, (<$>))
import Prelude qualified as Haskell
------------------------------------------------------------
newtype HashedString = HashedString BuiltinByteString deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)
PlutusTx.makeLift ''HashedString
newtype ClearString = ClearString BuiltinByteString deriving newtype (PlutusTx.ToData, PlutusTx.FromData, PlutusTx.UnsafeFromData)
PlutusTx.makeLift ''ClearString
type GameSchema =
Endpoint "lock" LockParams
.\/ Endpoint "guess" GuessParams
data Game
instance Scripts.ValidatorTypes Game where
type instance RedeemerType Game = ClearString
type instance DatumType Game = HashedString
gameInstance :: Scripts.TypedValidator Game
gameInstance = Scripts.mkTypedValidator @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 :: Haskell.String -> HashedString
hashString = HashedString . sha2_256 . toBuiltin . C.pack
-- create a redeemer script for the guessing game by lifting the
-- string to its on-chain representation
clearString :: Haskell.String -> ClearString
clearString = ClearString . toBuiltin . C.pack
-- | The validation function (Datum -> Redeemer -> ScriptContext -> Bool)
validateGuess :: HashedString -> ClearString -> ScriptContext -> Bool
validateGuess hs cs _ = isGoodGuess hs cs
isGoodGuess :: HashedString -> ClearString -> Bool
isGoodGuess (HashedString actual) (ClearString guess') = actual == sha2_256 guess'
-- | The validator script of the game.
gameValidator :: Validator
gameValidator = Scripts.validatorScript gameInstance
-- | 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 :: Haskell.String
, amount :: Value
}
deriving stock (Haskell.Eq, Haskell.Show, Generic)
deriving anyclass (FromJSON, ToJSON, ToSchema, ToArgument)
-- | Parameters for the "guess" endpoint
newtype GuessParams = GuessParams
{ guessWord :: Haskell.String
}
deriving stock (Haskell.Eq, Haskell.Show, Generic)
deriving anyclass (FromJSON, ToJSON, ToSchema, ToArgument)
-- | The "lock" contract endpoint. See note [Contract endpoints]
lock :: AsContractError e => Promise () GameSchema e ()
lock = endpoint @"lock" @LockParams $ \(LockParams secret amt) -> do
logInfo @Haskell.String $ "Pay " <> Haskell.show amt <> " to the script"
let tx = Constraints.mustPayToTheScript (hashString secret) amt
void (submitTxConstraints gameInstance tx)
-- | The "guess" contract endpoint. See note [Contract endpoints]
guess :: AsContractError e => Promise () GameSchema e ()
guess = endpoint @"guess" @GuessParams $ \(GuessParams theGuess) -> do
-- Wait for script to have a UTxO of a least 1 lovelace
logInfo @Haskell.String "Waiting for script to have a UTxO of at least 1 lovelace"
utxos <- fundsAtAddressGeq gameAddress (Ada.lovelaceValueOf 1)
let redeemer = clearString theGuess
tx = collectFromScript utxos redeemer
-- Log a message saying if the secret word was correctly guessed
let hashedSecretWord = findSecretWordValue utxos
isCorrectSecretWord = fmap (`isGoodGuess` redeemer) hashedSecretWord == Just True
if isCorrectSecretWord
then logWarn @Haskell.String "Correct secret word! Submitting the transaction"
else logWarn @Haskell.String "Incorrect secret word, but still submiting the transaction"
-- This is only for test purposes to have a possible failing transaction.
-- In a real use-case, we would not submit the transaction if the guess is
-- wrong.
logInfo @Haskell.String "Submitting transaction to guess the secret word"
void (submitTxConstraintsSpending gameInstance utxos tx)
-- | Find the secret word in the Datum of the UTxOs
findSecretWordValue :: Map TxOutRef ChainIndexTxOut -> Maybe HashedString
findSecretWordValue =
listToMaybe . catMaybes . Map.elems . Map.map secretWordValue
-- | Extract the secret word in the Datum of a given transaction output is possible
secretWordValue :: ChainIndexTxOut -> Maybe HashedString
secretWordValue o = do
Datum d <- either (const Nothing) Just (_ciTxOutDatum o)
PlutusTx.fromBuiltinData d
game :: AsContractError e => Contract () GameSchema e ()
game = do
logInfo @Haskell.String "Waiting for guess or lock endpoint..."
selectList [lock, guess]
{- Note [Contract endpoints]
A contract endpoint is a function that uses the wallet API to interact with the
blockchain. We can look at contract endpoints from two different points of view.
1. Contract users
Contract endpoints are the visible interface of the contract. They provide a
UI (HTML form) for entering the parameters of the actions we may take as part
of the contract.
2. Contract authors
As contract authors we define endpoints as functions that return a value of
type 'MockWallet ()'. This type indicates that the function uses the wallet API
to produce and spend transaction outputs on the blockchain.
Endpoints can have any number of parameters: 'lock' has two
parameters, 'guess' has one and 'startGame' has none. For each endpoint we
include a call to 'mkFunction' at the end of the contract definition. This
causes the Haskell compiler to generate a schema for the endpoint. The Plutus
Playground then uses this schema to present an HTML form to the user where the
parameters can be entered.
-}
endpoints :: AsContractError e => Contract () GameSchema e ()
endpoints = game
mkSchemaDefinitions ''GameSchema
$(mkKnownCurrencies [])
[0,[{"simulationWallets":[{"simulatorWalletWallet":{"getWallet":1},"simulatorWalletBalance":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},100000000]]]]}},{"simulatorWalletWallet":{"getWallet":2},"simulatorWalletBalance":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},100000000]]]]}}],"simulationName":"Basic Game","simulationId":1,"simulationActions":[{"tag":"CallEndpoint","caller":{"getWallet":1},"argumentValues":{"endpointDescription":{"getEndpointDescription":"lock"},"argument":{"contents":[["secretWord",{"contents":"Plutus","tag":"FormStringF"}],["amount",{"contents":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},50000000]]]]},"tag":"FormValueF"}]],"tag":"FormObjectF"}}},{"tag":"AddBlocks","blocks":1},{"tag":"CallEndpoint","caller":{"getWallet":2},"argumentValues":{"endpointDescription":{"getEndpointDescription":"guess"},"argument":{"contents":[["guessWord",{"contents":"Plutus","tag":"FormStringF"}]],"tag":"FormObjectF"}}},{"tag":"AddBlocks","blocks":1}]},{"simulationWallets":[{"simulatorWalletWallet":{"getWallet":1},"simulatorWalletBalance":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},100000000]]]]}},{"simulatorWalletWallet":{"getWallet":2},"simulatorWalletBalance":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},100000000]]]]}},{"simulatorWalletWallet":{"getWallet":3},"simulatorWalletBalance":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},100000000]]]]}}],"simulationName":"One Bad Guess","simulationId":2,"simulationActions":[{"tag":"CallEndpoint","caller":{"getWallet":1},"argumentValues":{"endpointDescription":{"getEndpointDescription":"lock"},"argument":{"contents":[["secretWord",{"contents":"Plutus","tag":"FormStringF"}],["amount",{"contents":{"getValue":[[{"unCurrencySymbol":""},[[{"unTokenName":""},50000000]]]]},"tag":"FormValueF"}]],"tag":"FormObjectF"}}},{"tag":"AddBlocks","blocks":1},{"tag":"CallEndpoint","caller":{"getWallet":2},"argumentValues":{"endpointDescription":{"getEndpointDescription":"guess"},"argument":{"contents":[["guessWord",{"contents":"Marlowe","tag":"FormStringF"}]],"tag":"FormObjectF"}}},{"tag":"AddBlocks","blocks":1},{"tag":"CallEndpoint","caller":{"getWallet":3},"argumentValues":{"endpointDescription":{"getEndpointDescription":"guess"},"argument":{"contents":[["guessWord",{"contents":"Plutus","tag":"FormStringF"}]],"tag":"FormObjectF"}}},{"tag":"AddBlocks","blocks":1}]}]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment