-
Notifications
You must be signed in to change notification settings - Fork 0
/
raffle.hs
272 lines (226 loc) · 11 KB
/
raffle.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
import Control.Monad hiding (fmap)
import Data.Aeson (ToJSON, FromJSON)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map as Map
import Data.Text (pack, Text)
import GHC.Generics (Generic)
import Plutus.Contract
import qualified PlutusTx as PlutusTx
import PlutusTx.Prelude hiding (Semigroup(..), unless)
import qualified PlutusTx.Prelude as Plutus
import Ledger hiding (singleton)
import Ledger.Constraints as Constraints
import qualified Ledger.Scripts as Scripts
import qualified Ledger.Typed.Scripts as Scripts hiding (validatorHash)
import Ledger.Value as Value
import Ledger.Ada as Ada
import Playground.Contract (ensureKnownCurrencies, printSchemas, stage, printJson)
import Playground.TH (mkKnownCurrencies, mkSchemaDefinitions)
import Playground.Types (KnownCurrency (..))
import Prelude (IO, Semigroup (..), Show (..), String)
import Schema (ToSchema)
import Text.Printf (printf)
import System.Random as R
{-# OPTIONS_GHC -fno-warn-unused-imports #-}
data Raffle = Raffle {
lGameHost :: !PubKeyHash
, lDeadline :: !POSIXTime
, lTicketPrice :: !Integer
, lCurrency :: !CurrencySymbol
, lToken :: !TokenName
} deriving (Show, Generic, ToJSON, FromJSON, ToSchema)
PlutusTx.unstableMakeIsData ''Raffle
PlutusTx.makeLift ''Raffle
data Participant = Participant
{ pParticipant :: !PubKeyHash
, pAmount :: !Integer
} deriving (Show)
PlutusTx.unstableMakeIsData ''Participant
PlutusTx.makeLift ''Participant
data RaffleAction = BuyTicket Participant | Close {clWinner :: !(Maybe PubKeyHash)}
deriving (Show)
PlutusTx.unstableMakeIsData ''RaffleAction
PlutusTx.makeLift ''RaffleAction
data RaffleDatum = RaffleDatum
{ adRaffle :: !Raffle
, adParticipants :: ![Participant]
} deriving Show
PlutusTx.unstableMakeIsData ''RaffleDatum
PlutusTx.makeLift ''RaffleDatum
{-# INLINABLE mkValidator #-}
mkValidator :: RaffleDatum -> RaffleAction -> ScriptContext -> Bool
mkValidator ad redeemer ctx =
case redeemer of
BuyTicket p@Participant{..} ->
traceIfFalse "The amount is insuficient" $ sufficientAmount p &&
traceIfFalse "Too Late" correctBuySlotRange
Close winner ->
-- We don't care how gets to close the raffle since it won't change the outcome, so we don't check
traceIfFalse "Too soon to close the raffle" correctCloseSlotRange &&
case winner of
--If there is no winner that would mean that there were no participants
Nothing ->
traceIfFalse "Expected game host to recieve token" (getsValue (lGameHost raffleInfo) tokenValue)
Just winnerPkh ->
traceIfFalse "Expected winner to be in the list of participants" (winnerInList winnerPkh) &&
traceIfFalse "Expected winner to recieve token" (getsValue winnerPkh tokenValue) &&
traceIfFalse "Expected game host to get the total amount of money betted" hostRecievedTotalAmount
where
info :: TxInfo
info = scriptContextTxInfo ctx
raffleInfo :: Raffle
raffleInfo = adRaffle ad
tokenValue :: Value
tokenValue = Value.singleton (lCurrency raffleInfo) (lToken raffleInfo) 1
sufficientAmount :: Participant -> Bool
sufficientAmount p = (pAmount p) >= (lTicketPrice raffleInfo)
correctBuySlotRange :: Bool
correctBuySlotRange = to (lDeadline raffleInfo) `contains` txInfoValidRange info
correctCloseSlotRange :: Bool
correctCloseSlotRange = from (lDeadline raffleInfo) `contains` txInfoValidRange info
winnerInList :: PubKeyHash -> Bool
winnerInList w = w `elem` (PlutusTx.Prelude.map pParticipant $ adParticipants ad)
getsValue :: PubKeyHash -> Value -> Bool
getsValue h v =
let
[o] = [o' | o' <- txInfoOutputs info, txOutValue o' == v]
in
txOutAddress o == pubKeyHashAddress h
outputTokens :: [Value]
outputTokens = PlutusTx.Prelude.map txOutValue $ txInfoOutputs info
hostRecievedTotalAmount :: Bool
hostRecievedTotalAmount = getsValue (lGameHost raffleInfo) totalValue
where
total :: Integer
total = (length (adParticipants ad)) * (lTicketPrice raffleInfo)
totalValue :: Value
totalValue = Value.singleton Ada.adaSymbol Ada.adaToken total
data Typed
instance Scripts.ValidatorTypes Typed where
type instance DatumType Typed = RaffleDatum
type instance RedeemerType Typed = RaffleAction
typedValidator :: Scripts.TypedValidator Typed
typedValidator = Scripts.mkTypedValidator @Typed
$$(PlutusTx.compile [|| mkValidator ||])
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @RaffleDatum @RaffleAction
validator :: Validator
validator = Scripts.validatorScript typedValidator
data StartParams = StartParams
{ spDeadline :: !POSIXTime
, spTicketPrice :: !Integer
, spCurrency :: !CurrencySymbol
, spToken :: !TokenName
} deriving (Generic, ToJSON, FromJSON, ToSchema)
data BuyParams = BuyParams
{ bpCurrency :: !CurrencySymbol
, bpToken :: !TokenName
} deriving (Generic, ToJSON, FromJSON, ToSchema)
data CloseParams = CloseParams
{ cpCurrency :: !CurrencySymbol
, cpToken :: !TokenName
} deriving (Generic, ToJSON, FromJSON, ToSchema)
type RaffleSchema =
Endpoint "start" StartParams .\/
Endpoint "buyTicket" BuyParams .\/
Endpoint "close" CloseParams
start :: StartParams -> Contract w s Text ()
start StartParams{..} = do
pkh <- pubKeyHash <$> ownPubKey
let a = Raffle
{ lGameHost = pkh
, lDeadline = spDeadline
, lTicketPrice = spTicketPrice
, lCurrency = spCurrency
, lToken = spToken
}
d = RaffleDatum
{ adRaffle = a
, adParticipants = []
}
v = Value.singleton spCurrency spToken 1
tx = mustPayToTheScript d v
ledgerTx <- submitTxConstraints typedValidator tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ printf "started raffle %s for token %s" (show a) (show v)
buyTicket :: BuyParams -> Contract w s Text ()
buyTicket BuyParams{..} = do
(oref, o, d@RaffleDatum{..}) <- findAuction bpCurrency bpToken
logInfo @String $ printf "found auction utxo with datum %s" (show d)
pkh <- pubKeyHash <$> ownPubKey
let ticketPrice = lTicketPrice adRaffle
p = Participant {pParticipant = pkh, pAmount = ticketPrice}
newParticipantsList = adParticipants ++ [p]
d' = d { adParticipants = newParticipantsList }
v = Ada.lovelaceValueOf ticketPrice <> (txOutValue $ txOutTxOut o)
r = Redeemer $ PlutusTx.toData $ BuyTicket p
lookups = Constraints.typedValidatorLookups typedValidator <>
Constraints.otherScript validator <>
Constraints.unspentOutputs (Map.singleton oref o)
tx = mustPayToTheScript d' v <>
mustValidateIn (to $ lDeadline adRaffle) <>
mustSpendScriptOutput oref r
ledgerTx <- submitTxConstraintsWith lookups tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ printf "bouth ticket for token (%s, %s)" (show bpCurrency) (show bpToken)
close :: CloseParams -> Contract w s Text ()
close CloseParams{..} = do
(oref, o, d@RaffleDatum{..}) <- findAuction cpCurrency cpToken
logInfo @String $ printf "found auction utxo with datum %s" (show d)
let v = Value.singleton cpCurrency cpToken 1
-- Choose a winner randomly
winner = case adParticipants of
[] -> Nothing
ps -> Just $ pParticipant (ps !! (giveRandomInteger $ length ps))
r = Redeemer $ PlutusTx.toData $ Close {clWinner = winner}
lookups = Constraints.typedValidatorLookups typedValidator <>
Constraints.otherScript validator <>
Constraints.unspentOutputs (Map.singleton oref o)
tx = case winner of
Nothing -> do
mustPayToPubKey (lGameHost adRaffle) v <> mustSpendScriptOutput oref r
Just w -> do
mustPayToPubKey w v <> mustValidateIn (from $ lDeadline adRaffle) <> mustPayToPubKey hostPubKey (Ada.lovelaceValueOf totalFunds) <> mustSpendScriptOutput oref r
where
hostPubKey = lGameHost $ adRaffle
totalFunds = (length adParticipants) * (lTicketPrice adRaffle)
ledgerTx <- submitTxConstraintsWith lookups tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String "Game Over!"
findAuction :: CurrencySymbol -> TokenName -> Contract w s Text (TxOutRef, TxOutTx, RaffleDatum)
findAuction cs tn = do
utxos <- utxoAt $ scriptAddress validator
let xs = [(oref, o) | (oref, o) <- Map.toList utxos, Value.valueOf (txOutValue $ txOutTxOut o) cs tn == 1]
case xs of
[(oref, o)] -> case txOutDatumHash $ txOutTxOut o of
Nothing -> throwError "unexpected output type"
Just h -> case Map.lookup h $ txData $ txOutTxTx o of
Nothing -> throwError "datum not found"
Just (Datum e) -> case PlutusTx.fromData e of
Nothing -> throwError "datum was from type"
Just d@RaffleDatum{..}
| lCurrency adRaffle == cs && lToken adRaffle == tn -> return (oref, o, d)
| otherwise -> throwError "action token missmatch"
_ -> throwError "auction utxo not found"
giveRandomInteger :: Integer -> Integer
giveRandomInteger l = let (i, g) = R.randomR (0, l-1) $ mkStdGen 42 :: (Integer, StdGen) in i
endpoints :: Contract () RaffleSchema Text ()
endpoints = (start' `select` buyTicket' `select` close') >> endpoints
where
start' = endpoint @"start" >>= start
buyTicket' = endpoint @"buyTicket" >>= buyTicket
close' = endpoint @"close" >>= close
mkSchemaDefinitions ''RaffleSchema
myToken :: KnownCurrency
myToken = KnownCurrency (ValidatorHash "f") "Token" (TokenName "T" :| [])
mkKnownCurrencies ['myToken]