diff --git a/Marlowe_Plutus_Pioneers_June_2021.pdf b/Marlowe_Plutus_Pioneers_June_2021.pdf new file mode 100644 index 0000000..7dc63b4 Binary files /dev/null and b/Marlowe_Plutus_Pioneers_June_2021.pdf differ diff --git a/README.md b/README.md index 3683006..bbf3e15 100644 --- a/README.md +++ b/README.md @@ -43,15 +43,40 @@ - Commit schemes. - State machines. +- [Lecture #8](https://youtu.be/JMRwkMgaBOg) + + - Another state machine example: token sale. + - Automatic testing using emulator traces. + - Interlude: optics. + - Property based testing with `QuickCheck`. + - Testing Plutus contracts with property based testing. + +- [Lecture #9](https://youtu.be/-RpCqHuxfQQ) + + - Marlowe overview ([slides](Marlowe_Plutus_Pioneers_June_2021.pdf)). + - Marlowe in Plutus. + - Marlowe Playground demo. + +- [Lecture #10](https://youtu.be/Dg36h9YPMz4) + + - Uniswap overview. + - Uniswap implementation in Plutus. + - Deploying Uniswap with the PAB. + - Demo. + - Using `curl` to interact with the PAB. + ## Code Examples -- Lecture #1: [English Auction](code/week01) -- Lecture #2: [Simple Validation](code/week02) -- Lecture #3: [Validation Context & Parameterized Contracts](code/week03) -- Lecture #4: [Monads, `EmulatorTrace` & `Contract`](code/week04) -- Lecture #5: [Minting Policies](code/week05) -- Lecture #6: [Oracles](code/week06) -- Lecture #7: [State Machines](code/week07) +- Lecture #1: [English Auction](code/week01) +- Lecture #2: [Simple Validation](code/week02) +- Lecture #3: [Validation Context & Parameterized Contracts](code/week03) +- Lecture #4: [Monads, `EmulatorTrace` & `Contract`](code/week04) +- Lecture #5: [Minting Policies](code/week05) +- Lecture #6: [Oracles](code/week06) +- Lecture #7: [State Machines](code/week07) +- Lecture #8: [Testing](code/week08) +- Lecture #9: [Marlowe](code/week09) +- Lecture #10: [Uniswap](code/week10) ## Exercises @@ -97,6 +122,21 @@ - Implement the game of "Rock, Paper, Scissors" using state machines. +- Week #8 + + - Add a new operation `close` to the `TokenSale`-contract that allows the seller to close the contract and + retrieve all remaining funds (including the NFT). + - Modify the tests accordingly. + +- Week #9 + + - Modify the example Marlowe contract, so that Charlie must put down twice the deposit in the very beginning, + which gets split between Alice and Bob if Charlie refuses to make his choice. + +- Week #10 + + - Get the Uniswap demo running and extend it in some way. + ## Solutions - Week #2 @@ -118,9 +158,28 @@ - [`Homework1`](code/week05/src/Week05/Solution1.hs) - [`Homework2`](code/week05/src/Week05/Solution2.hs) +- Week #7 + + - [`RockPaperScissors`](code/week07/src/Week07/RockPaperScissors.hs) + - [`TestRockPaperScissors`](code/week07/src/Week07/TestRockPaperScissors.hs) + +- Week #8 + + - [`TokenSaleWithClose`](code/week08/src/Week08/TokenSaleWithClose.hs) + - [`ModelWithClose`](code/week08/test/Spec/ModelWithClose.hs) + - [`TraceWithClose`](code/week08/test/Spec/TraceWithClose.hs) + +- Week #9 + + - [`solution`](code/week09/app/solution.hs) + ## Some Plutus Modules +- [`Language.Marlowe.Semantics`](https://github.com/input-output-hk/plutus/blob/master/marlowe/src/Language/Marlowe/Semantics.hs), contains Marlowe types and semantics. - [`Plutus.Contract.StateMachine`](https://github.com/input-output-hk/plutus/blob/master/plutus-contract/src/Plutus/Contract/StateMachine.hs), contains types and functions for using state machines. +- [`Plutus.Contract.Test`](https://github.com/input-output-hk/plutus/blob/master/plutus-contract/src/Plutus/Contract/Test.hs), provides various ways to write tests for Plutus contracts. +- [`Plutus.Contract.Test.ContractModel`](https://github.com/input-output-hk/plutus/blob/master/plutus-contract/src/Plutus/Contract/Test/ContractModel.hs), support for property based testing of Plutus contracts. +- [`Plutus.Contracts.Uniswap`](https://github.com/input-output-hk/plutus/blob/master/plutus-use-cases/src/Plutus/Contracts/Uniswap.hs), an implementation of Uniswap in Plutus. - [`Plutus.PAB.Webserver.API`](https://github.com/input-output-hk/plutus/blob/master/plutus-pab/src/Plutus/PAB/Webserver/API.hs), contains the HTTP-interface provided by the PAB. - [`Plutus.Trace.Emulator`](https://github.com/input-output-hk/plutus/blob/master/plutus-contract/src/Plutus/Trace/Emulator.hs), contains types and functions related to traces. - [`Plutus.V1.Ledger.Ada`](https://github.com/input-output-hk/plutus/blob/master/plutus-ledger-api/src/Plutus/V1/Ledger/Ada.hs), contains support for the Ada currency. diff --git a/code/week07/plutus-pioneer-program-week07.cabal b/code/week07/plutus-pioneer-program-week07.cabal index 3f9d784..c42c2be 100644 --- a/code/week07/plutus-pioneer-program-week07.cabal +++ b/code/week07/plutus-pioneer-program-week07.cabal @@ -11,8 +11,10 @@ License-files: LICENSE library hs-source-dirs: src exposed-modules: Week07.EvenOdd + , Week07.RockPaperScissors , Week07.StateMachine , Week07.Test + , Week07.TestRockPaperScissors , Week07.TestStateMachine build-depends: aeson , base ^>=4.14.1.0 diff --git a/code/week07/src/Week07/RockPaperScissors.hs b/code/week07/src/Week07/RockPaperScissors.hs new file mode 100644 index 0000000..1b1db18 --- /dev/null +++ b/code/week07/src/Week07/RockPaperScissors.hs @@ -0,0 +1,302 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Week07.RockPaperScissors + ( Game (..) + , GameChoice (..) + , FirstParams (..) + , SecondParams (..) + , GameSchema + , endpoints + ) where + +import Control.Monad hiding (fmap) +import Data.Aeson (FromJSON, ToJSON) +import Data.Text (Text, pack) +import GHC.Generics (Generic) +import Plutus.Contract as Contract hiding (when) +import Plutus.Contract.StateMachine +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup(..), check, unless) +import Ledger hiding (singleton) +import Ledger.Ada as Ada +import Ledger.Constraints as Constraints +import Ledger.Typed.Tx +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Value +import Playground.Contract (ToSchema) +import Prelude (Semigroup (..)) +import qualified Prelude + +data Game = Game + { gFirst :: !PubKeyHash + , gSecond :: !PubKeyHash + , gStake :: !Integer + , gPlayDeadline :: !Slot + , gRevealDeadline :: !Slot + , gToken :: !AssetClass + } deriving (Show, Generic, FromJSON, ToJSON, Prelude.Eq, Prelude.Ord) + +PlutusTx.makeLift ''Game + +data GameChoice = Rock | Paper | Scissors + deriving (Show, Generic, FromJSON, ToJSON, ToSchema, Prelude.Eq, Prelude.Ord) + +instance Eq GameChoice where + {-# INLINABLE (==) #-} + Rock == Rock = True + Paper == Paper = True + Scissors == Scissors = True + _ == _ = False + +PlutusTx.unstableMakeIsData ''GameChoice + +{-# INLINABLE beats #-} +beats :: GameChoice -> GameChoice -> Bool +beats Rock Scissors = True +beats Paper Rock = True +beats Scissors Paper = True +beats _ _ = False + +data GameDatum = GameDatum ByteString (Maybe GameChoice) | Finished + deriving Show + +instance Eq GameDatum where + {-# INLINABLE (==) #-} + GameDatum bs mc == GameDatum bs' mc' = (bs == bs') && (mc == mc') + Finished == Finished = True + _ == _ = False + +PlutusTx.unstableMakeIsData ''GameDatum + +data GameRedeemer = Play GameChoice | Reveal ByteString GameChoice | ClaimFirst | ClaimSecond + deriving Show + +PlutusTx.unstableMakeIsData ''GameRedeemer + +{-# INLINABLE lovelaces #-} +lovelaces :: Value -> Integer +lovelaces = Ada.getLovelace . Ada.fromValue + +{-# INLINABLE gameDatum #-} +gameDatum :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe GameDatum +gameDatum o f = do + dh <- txOutDatum o + Datum d <- f dh + PlutusTx.fromData d + +{-# INLINABLE transition #-} +transition :: Game -> State GameDatum -> GameRedeemer -> Maybe (TxConstraints Void Void, State GameDatum) +transition game s r = case (stateValue s, stateData s, r) of + (v, GameDatum bs Nothing, Play c) + | lovelaces v == gStake game -> Just ( Constraints.mustBeSignedBy (gSecond game) <> + Constraints.mustValidateIn (to $ gPlayDeadline game) + , State (GameDatum bs $ Just c) (lovelaceValueOf $ 2 * gStake game) + ) + (v, GameDatum _ (Just c), Reveal _ c') + | (lovelaces v == (2 * gStake game)) && + (c' `beats` c) -> Just ( Constraints.mustBeSignedBy (gFirst game) <> + Constraints.mustValidateIn (to $ gRevealDeadline game) <> + Constraints.mustPayToPubKey (gFirst game) token + , State Finished mempty + ) + + | (lovelaces v == (2 * gStake game)) && + (c' == c) -> Just ( Constraints.mustBeSignedBy (gFirst game) <> + Constraints.mustValidateIn (to $ gRevealDeadline game) <> + Constraints.mustPayToPubKey (gFirst game) token <> + Constraints.mustPayToPubKey (gSecond game) + (lovelaceValueOf $ gStake game) + , State Finished mempty + ) + (v, GameDatum _ Nothing, ClaimFirst) + | lovelaces v == gStake game -> Just ( Constraints.mustBeSignedBy (gFirst game) <> + Constraints.mustValidateIn (from $ 1 + gPlayDeadline game) <> + Constraints.mustPayToPubKey (gFirst game) token + , State Finished mempty + ) + (v, GameDatum _ (Just _), ClaimSecond) + | lovelaces v == (2 * gStake game) -> Just ( Constraints.mustBeSignedBy (gSecond game) <> + Constraints.mustValidateIn (from $ 1 + gRevealDeadline game) <> + Constraints.mustPayToPubKey (gFirst game) token + , State Finished mempty + ) + _ -> Nothing + where + token :: Value + token = assetClassValue (gToken game) 1 + +{-# INLINABLE final #-} +final :: GameDatum -> Bool +final Finished = True +final _ = False + +{-# INLINABLE check #-} +check :: ByteString -> ByteString -> ByteString -> GameDatum -> GameRedeemer -> ScriptContext -> Bool +check bsRock' bsPaper' bsScissors' (GameDatum bs (Just _)) (Reveal nonce c) _ = + sha2_256 (nonce `concatenate` toBS c) == bs + where + toBS :: GameChoice -> ByteString + toBS Rock = bsRock' + toBS Paper = bsPaper' + toBS Scissors = bsScissors' +check _ _ _ _ _ _ = True + +{-# INLINABLE gameStateMachine #-} +gameStateMachine :: Game -> ByteString -> ByteString -> ByteString -> StateMachine GameDatum GameRedeemer +gameStateMachine game bsRock' bsPaper' bsScissors' = StateMachine + { smTransition = transition game + , smFinal = final + , smCheck = check bsRock' bsPaper' bsScissors' + , smThreadToken = Just $ gToken game + } + +{-# INLINABLE mkGameValidator #-} +mkGameValidator :: Game -> ByteString -> ByteString -> ByteString -> GameDatum -> GameRedeemer -> ScriptContext -> Bool +mkGameValidator game bsRock' bsPaper' bsScissors' = mkValidator $ gameStateMachine game bsRock' bsPaper' bsScissors' + +type Gaming = StateMachine GameDatum GameRedeemer + +bsRock, bsPaper, bsScissors :: ByteString +bsRock = "R" +bsPaper = "P" +bsScissors = "S" + +gameStateMachine' :: Game -> StateMachine GameDatum GameRedeemer +gameStateMachine' game = gameStateMachine game bsRock bsPaper bsScissors + +gameInst :: Game -> Scripts.ScriptInstance Gaming +gameInst game = Scripts.validator @Gaming + ($$(PlutusTx.compile [|| mkGameValidator ||]) + `PlutusTx.applyCode` PlutusTx.liftCode game + `PlutusTx.applyCode` PlutusTx.liftCode bsRock + `PlutusTx.applyCode` PlutusTx.liftCode bsPaper + `PlutusTx.applyCode` PlutusTx.liftCode bsScissors) + $$(PlutusTx.compile [|| wrap ||]) + where + wrap = Scripts.wrapValidator @GameDatum @GameRedeemer + +gameValidator :: Game -> Validator +gameValidator = Scripts.validatorScript . gameInst + +gameAddress :: Game -> Ledger.Address +gameAddress = scriptAddress . gameValidator + +gameClient :: Game -> StateMachineClient GameDatum GameRedeemer +gameClient game = mkStateMachineClient $ StateMachineInstance (gameStateMachine' game) (gameInst game) + +data FirstParams = FirstParams + { fpSecond :: !PubKeyHash + , fpStake :: !Integer + , fpPlayDeadline :: !Slot + , fpRevealDeadline :: !Slot + , fpNonce :: !ByteString + , fpCurrency :: !CurrencySymbol + , fpTokenName :: !TokenName + , fpChoice :: !GameChoice + } deriving (Show, Generic, FromJSON, ToJSON, ToSchema) + +mapError' :: Contract w s SMContractError a -> Contract w s Text a +mapError' = mapError $ pack . show + +firstGame :: forall w s. HasBlockchainActions s => FirstParams -> Contract w s Text () +firstGame fp = do + pkh <- pubKeyHash <$> Contract.ownPubKey + let game = Game + { gFirst = pkh + , gSecond = fpSecond fp + , gStake = fpStake fp + , gPlayDeadline = fpPlayDeadline fp + , gRevealDeadline = fpRevealDeadline fp + , gToken = AssetClass (fpCurrency fp, fpTokenName fp) + } + client = gameClient game + v = lovelaceValueOf (fpStake fp) + c = fpChoice fp + x = case c of + Rock -> bsRock + Paper -> bsPaper + Scissors -> bsScissors + bs = sha2_256 $ fpNonce fp `concatenate` x + void $ mapError' $ runInitialise client (GameDatum bs Nothing) v + logInfo @String $ "made first move: " ++ show (fpChoice fp) + + void $ awaitSlot $ 1 + fpPlayDeadline fp + + m <- mapError' $ getOnChainState client + case m of + Nothing -> throwError "game output not found" + Just ((o, _), _) -> case tyTxOutData o of + + GameDatum _ Nothing -> do + logInfo @String "second player did not play" + void $ mapError' $ runStep client ClaimFirst + logInfo @String "first player reclaimed stake" + + GameDatum _ (Just c') | c `beats` c' || c' == c -> do + logInfo @String "second player played and lost or drew" + void $ mapError' $ runStep client $ Reveal (fpNonce fp) c + logInfo @String "first player revealed and won or drew" + + _ -> logInfo @String "second player played and won" + +data SecondParams = SecondParams + { spFirst :: !PubKeyHash + , spStake :: !Integer + , spPlayDeadline :: !Slot + , spRevealDeadline :: !Slot + , spCurrency :: !CurrencySymbol + , spTokenName :: !TokenName + , spChoice :: !GameChoice + } deriving (Show, Generic, FromJSON, ToJSON, ToSchema) + +secondGame :: forall w s. HasBlockchainActions s => SecondParams -> Contract w s Text () +secondGame sp = do + pkh <- pubKeyHash <$> Contract.ownPubKey + let game = Game + { gFirst = spFirst sp + , gSecond = pkh + , gStake = spStake sp + , gPlayDeadline = spPlayDeadline sp + , gRevealDeadline = spRevealDeadline sp + , gToken = AssetClass (spCurrency sp, spTokenName sp) + } + client = gameClient game + m <- mapError' $ getOnChainState client + case m of + Nothing -> logInfo @String "no running game found" + Just ((o, _), _) -> case tyTxOutData o of + GameDatum _ Nothing -> do + logInfo @String "running game found" + void $ mapError' $ runStep client $ Play $ spChoice sp + logInfo @String $ "made second move: " ++ show (spChoice sp) + + void $ awaitSlot $ 1 + spRevealDeadline sp + + m' <- mapError' $ getOnChainState client + case m' of + Nothing -> logInfo @String "first player won or drew" + Just _ -> do + logInfo @String "first player didn't reveal" + void $ mapError' $ runStep client ClaimSecond + logInfo @String "second player won" + + _ -> throwError "unexpected datum" + +type GameSchema = BlockchainActions .\/ Endpoint "first" FirstParams .\/ Endpoint "second" SecondParams + +endpoints :: Contract () GameSchema Text () +endpoints = (first `select` second) >> endpoints + where + first = endpoint @"first" >>= firstGame + second = endpoint @"second" >>= secondGame diff --git a/code/week07/src/Week07/TestRockPaperScissors.hs b/code/week07/src/Week07/TestRockPaperScissors.hs new file mode 100644 index 0000000..150e755 --- /dev/null +++ b/code/week07/src/Week07/TestRockPaperScissors.hs @@ -0,0 +1,96 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Week07.TestRockPaperScissors where + +import Control.Monad hiding (fmap) +import Control.Monad.Freer.Extras as Extras +import Data.Default (Default (..)) +import qualified Data.Map as Map +import Ledger +import Ledger.Value +import Ledger.Ada as Ada +import Plutus.Trace.Emulator as Emulator +import PlutusTx.Prelude +import Wallet.Emulator.Wallet + +import Week07.RockPaperScissors + +test :: IO () +test = do + test' Rock Rock + test' Rock Paper + test' Rock Scissors + test' Paper Rock + test' Paper Paper + test' Paper Scissors + test' Scissors Rock + test' Scissors Paper + test' Scissors Scissors + +test' :: GameChoice -> GameChoice -> IO () +test' c1 c2 = runEmulatorTraceIO' def emCfg $ myTrace c1 c2 + where + emCfg :: EmulatorConfig + emCfg = EmulatorConfig $ Left $ Map.fromList + [ (Wallet 1, v <> assetClassValue (AssetClass (gameTokenCurrency, gameTokenName)) 1) + , (Wallet 2, v) + ] + + v :: Value + v = Ada.lovelaceValueOf 1000_000_000 + +gameTokenCurrency :: CurrencySymbol +gameTokenCurrency = "ff" + +gameTokenName :: TokenName +gameTokenName = "STATE TOKEN" + +myTrace :: GameChoice -> GameChoice -> EmulatorTrace () +myTrace c1 c2 = do + Extras.logInfo $ "first move: " ++ show c1 ++ ", second move: " ++ show c2 + + h1 <- activateContractWallet (Wallet 1) endpoints + h2 <- activateContractWallet (Wallet 2) endpoints + + let pkh1 = pubKeyHash $ walletPubKey $ Wallet 1 + pkh2 = pubKeyHash $ walletPubKey $ Wallet 2 + + fp = FirstParams + { fpSecond = pkh2 + , fpStake = 5000000 + , fpPlayDeadline = 5 + , fpRevealDeadline = 10 + , fpNonce = "SECRETNONCE" + , fpCurrency = gameTokenCurrency + , fpTokenName = gameTokenName + , fpChoice = c1 + } + sp = SecondParams + { spFirst = pkh1 + , spStake = 5000000 + , spPlayDeadline = 5 + , spRevealDeadline = 10 + , spCurrency = gameTokenCurrency + , spTokenName = gameTokenName + , spChoice = c2 + } + + callEndpoint @"first" h1 fp + + void $ Emulator.waitNSlots 3 + + callEndpoint @"second" h2 sp + + void $ Emulator.waitNSlots 10 diff --git a/code/week08/.devcontainer/devcontainer.json b/code/week08/.devcontainer/devcontainer.json new file mode 100644 index 0000000..51f7dce --- /dev/null +++ b/code/week08/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "Plutus Starter Project", + "image": "plutus-devcontainer:latest", + + "remoteUser": "plutus", + + "mounts": [ + // This shares cabal's remote repository state with the host. We don't mount the whole of '.cabal', because + // 1. '.cabal/config' contains absolute paths that will only make sense on the host, and + // 2. '.cabal/store' is not necessarily portable to different version of cabal etc. + "source=${localEnv:HOME}/.cabal/packages,target=/home/plutus/.cabal/packages,type=bind,consistency=cached", + ], + + "settings": { + // Note: don't change from bash so it runs .bashrc + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // IDs of extensions inside container + "extensions": [ + "haskell.haskell" + ], +} diff --git a/code/week08/.gitignore b/code/week08/.gitignore new file mode 100644 index 0000000..2bbd0a6 --- /dev/null +++ b/code/week08/.gitignore @@ -0,0 +1,6 @@ +dist-newstyle/ +oracle.cid +W2.cid +W3.cid +W4.cid +W5.cid diff --git a/code/week08/LICENSE b/code/week08/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/code/week08/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/code/week08/cabal.project b/code/week08/cabal.project new file mode 100644 index 0000000..711f268 --- /dev/null +++ b/code/week08/cabal.project @@ -0,0 +1,145 @@ +index-state: 2021-04-13T00:00:00Z + +packages: ./. + +-- You never, ever, want this. +write-ghc-environment-files: never + +-- Always build tests and benchmarks. +tests: true +benchmarks: true + +source-repository-package + type: git + location: https://github.com/input-output-hk/plutus.git + subdir: + freer-extras + playground-common + plutus-core + plutus-contract + plutus-ledger + plutus-ledger-api + plutus-pab + plutus-tx + plutus-tx-plugin + plutus-use-cases + prettyprinter-configurable + quickcheck-dynamic + tag: ae35c4b8fe66dd626679bd2951bd72190e09a123 + +-- The following sections are copied from the 'plutus' repository cabal.project at the revision +-- given above. +-- This is necessary because the 'plutus' libraries depend on a number of other libraries which are +-- not on Hackage, and so need to be pulled in as `source-repository-package`s themselves. Make sure to +-- re-update this section from the template when you do an upgrade. + +-- This is also needed so evenful-sql-common will build with a +-- newer version of persistent. See stack.yaml for the mirrored +-- configuration. +package eventful-sql-common + ghc-options: -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances + +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 + , size-based:template-haskell + + -- The following two dependencies are needed by plutus. + , eventful-sql-common:persistent + , eventful-sql-common:persistent-template + +constraints: + -- aws-lambda-haskell-runtime-wai doesn't compile with newer versions + aws-lambda-haskell-runtime <= 3.0.3 + -- big breaking change here, inline-r doens't have an upper bound + , singletons < 3.0 + -- breaks eventful even more than it already was + , persistent-template < 2.12 + +-- See the note on nix/pkgs/default.nix:agdaPackages for why this is here. +-- (NOTE this will change to ieee754 in newer versions of nixpkgs). +extra-packages: ieee, filemanip + + +-- Needs some patches, but upstream seems to be fairly dead (no activity in > 1 year) +source-repository-package + type: git + location: https://github.com/shmish111/purescript-bridge.git + tag: 6a92d7853ea514be8b70bab5e72077bf5a510596 + +source-repository-package + type: git + location: https://github.com/shmish111/servant-purescript.git + tag: a76104490499aa72d40c2790d10e9383e0dbde63 + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-crypto.git + tag: f73079303f663e028288f9f4a9e08bcca39a923e + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-base + tag: 4251c0bb6e4f443f00231d28f5f70d42876da055 + subdir: + binary + binary/test + slotting + cardano-crypto-class + cardano-crypto-praos + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-prelude + tag: ee4e7b547a991876e6b05ba542f4e62909f4a571 + subdir: + cardano-prelude + cardano-prelude-test + +source-repository-package + type: git + location: https://github.com/input-output-hk/ouroboros-network + tag: 6cb9052bde39472a0555d19ade8a42da63d3e904 + subdir: + typed-protocols + typed-protocols-examples + ouroboros-network + ouroboros-network-testing + ouroboros-network-framework + io-sim + io-sim-classes + network-mux + Win32-network + +source-repository-package + type: git + location: https://github.com/input-output-hk/iohk-monitoring-framework + tag: a89c38ed5825ba17ca79fddb85651007753d699d + subdir: + iohk-monitoring + tracer-transformers + contra-tracer + plugins/backend-ekg + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-ledger-specs + tag: 097890495cbb0e8b62106bcd090a5721c3f4b36f + subdir: + byron/chain/executable-spec + byron/crypto + byron/crypto/test + byron/ledger/executable-spec + byron/ledger/impl + byron/ledger/impl/test + semantics/executable-spec + semantics/small-steps-test + shelley/chain-and-ledger/dependencies/non-integer + shelley/chain-and-ledger/executable-spec + shelley-ma/impl + +source-repository-package + type: git + location: https://github.com/input-output-hk/goblins + tag: cde90a2b27f79187ca8310b6549331e59595e7ba diff --git a/code/week08/hie.yaml b/code/week08/hie.yaml new file mode 100644 index 0000000..7dc90d6 --- /dev/null +++ b/code/week08/hie.yaml @@ -0,0 +1,6 @@ +cradle: + cabal: + - path: "./src" + component: "lib:plutus-pioneer-program-week08" + - path: "./test" + component: "test:plutus-pioneer-program-week08-tests" diff --git a/code/week08/plutus-pioneer-program-week08.cabal b/code/week08/plutus-pioneer-program-week08.cabal new file mode 100644 index 0000000..94171f0 --- /dev/null +++ b/code/week08/plutus-pioneer-program-week08.cabal @@ -0,0 +1,58 @@ +Cabal-Version: 2.4 +Name: plutus-pioneer-program-week08 +Version: 0.1.0.0 +Author: Lars Bruenjes +Maintainer: brunjlar@gmail.com +Build-Type: Simple +Copyright: © 2021 Lars Bruenjes +License: Apache-2.0 +License-files: LICENSE + +library + hs-source-dirs: src + exposed-modules: Week08.Lens + , Week08.QuickCheck + , Week08.TokenSale + , Week08.TokenSaleWithClose + build-depends: aeson + , base ^>=4.14.1.0 + , containers + , lens + , playground-common + , plutus-contract + , plutus-ledger + , plutus-ledger-api + , plutus-tx-plugin + , plutus-tx + , plutus-use-cases + , prettyprinter + , QuickCheck + , text + default-language: Haskell2010 + ghc-options: -Wall -fobject-code -fno-ignore-interface-pragmas -fno-omit-interface-pragmas -fno-strictness -fno-spec-constr -fno-specialise + +test-suite plutus-pioneer-program-week08-tests + type: exitcode-stdio-1.0 + main-is: Spec.hs + hs-source-dirs: test + other-modules: Spec.Model + , Spec.ModelWithClose + , Spec.Trace + , Spec.TraceWithClose + default-language: Haskell2010 + ghc-options: -Wall -fobject-code -fno-ignore-interface-pragmas -fno-omit-interface-pragmas + build-depends: base ^>=4.14.1.0 + , containers + , data-default + , freer-extras + , lens + , plutus-contract + , plutus-ledger + , plutus-pioneer-program-week08 + , plutus-tx + , QuickCheck + , tasty + , tasty-quickcheck + , text + if !(impl(ghcjs) || os(ghcjs)) + build-depends: plutus-tx-plugin -any diff --git a/code/week08/src/Week08/Lens.hs b/code/week08/src/Week08/Lens.hs new file mode 100644 index 0000000..ff6e85d --- /dev/null +++ b/code/week08/src/Week08/Lens.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Week08.Lens where + +import Control.Lens + +newtype Company = Company {_staff :: [Person]} deriving Show + + +data Person = Person + { _name :: String + , _address :: Address + } deriving Show + +newtype Address = Address {_city :: String} deriving Show + +alejandro, lars :: Person +alejandro = Person + { _name = "Alejandro" + , _address = Address {_city = "Zacateca"} + } +lars = Person + { _name = "Lars" + , _address = Address {_city = "Regensburg"} + } + +iohk :: Company +iohk = Company { _staff = [alejandro, lars] } + +goTo :: String -> Company -> Company +goTo there c = c {_staff = map movePerson (_staff c)} + where + movePerson p = p {_address = (_address p) {_city = there}} + +makeLenses ''Company +makeLenses ''Person +makeLenses ''Address + +goTo' :: String -> Company -> Company +goTo' there c = c & staff . each . address . city .~ there diff --git a/code/week08/src/Week08/QuickCheck.hs b/code/week08/src/Week08/QuickCheck.hs new file mode 100644 index 0000000..5cd11d2 --- /dev/null +++ b/code/week08/src/Week08/QuickCheck.hs @@ -0,0 +1,37 @@ +module Week08.QuickCheck where + +prop_simple :: Bool +prop_simple = 2 + 2 == (4 :: Int) + +-- Insertion sort code: + +-- | Sort a list of integers in ascending order. +-- +-- >>> sort [5,1,9] +-- [1,5,9] +-- +sort :: [Int] -> [Int] -- not correct +sort [] = [] +sort (x:xs) = insert x $ sort xs + +-- | Insert an integer at the right position into an /ascendingly sorted/ +-- list of integers. +-- +-- >>> insert 5 [1,9] +-- [1,5,9] +-- +insert :: Int -> [Int] -> [Int] -- not correct +insert x [] = [x] +insert x (y:ys) | x <= y = x : y : ys + | otherwise = y : insert x ys + +isSorted :: [Int] -> Bool +isSorted [] = True +isSorted [_] = True +isSorted (x : y : ys) = x <= y && isSorted (y : ys) + +prop_sort_sorts :: [Int] -> Bool +prop_sort_sorts xs = isSorted $ sort xs + +prop_sort_preserves_length :: [Int] -> Bool +prop_sort_preserves_length xs = length (sort xs) == length xs diff --git a/code/week08/src/Week08/TokenSale.hs b/code/week08/src/Week08/TokenSale.hs new file mode 100644 index 0000000..9c22b25 --- /dev/null +++ b/code/week08/src/Week08/TokenSale.hs @@ -0,0 +1,188 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Week08.TokenSale + ( TokenSale (..) + , TSRedeemer (..) + , nftName + , TSStartSchema + , TSStartSchema' + , TSUseSchema + , startEndpoint + , startEndpoint' + , useEndpoints + ) where + +import Control.Monad hiding (fmap) +import Data.Aeson (FromJSON, ToJSON) +import Data.Monoid (Last (..)) +import Data.Text (Text, pack) +import GHC.Generics (Generic) +import Plutus.Contract as Contract hiding (when) +import Plutus.Contract.StateMachine +import qualified Plutus.Contracts.Currency as C +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup(..), check, unless) +import Ledger hiding (singleton) +import Ledger.Ada as Ada +import Ledger.Constraints as Constraints +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Value +import Prelude (Semigroup (..), Show (..), uncurry) +import qualified Prelude + +data TokenSale = TokenSale + { tsSeller :: !PubKeyHash + , tsToken :: !AssetClass + , tsNFT :: !AssetClass + } deriving (Show, Generic, FromJSON, ToJSON, Prelude.Eq, Prelude.Ord) + +PlutusTx.makeLift ''TokenSale + +data TSRedeemer = + SetPrice Integer + | AddTokens Integer + | BuyTokens Integer + | Withdraw Integer Integer + deriving (Show, Prelude.Eq) + +PlutusTx.unstableMakeIsData ''TSRedeemer + +{-# INLINABLE lovelaces #-} +lovelaces :: Value -> Integer +lovelaces = Ada.getLovelace . Ada.fromValue + +{-# INLINABLE transition #-} +transition :: TokenSale -> State Integer -> TSRedeemer -> Maybe (TxConstraints Void Void, State Integer) +transition ts s r = case (stateValue s, stateData s, r) of + (v, _, SetPrice p) | p >= 0 -> Just ( Constraints.mustBeSignedBy (tsSeller ts) + , State p $ + v <> + nft (negate 1) + ) + (v, p, AddTokens n) | n > 0 -> Just ( mempty + , State p $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) n + ) + (v, p, BuyTokens n) | n > 0 -> Just ( mempty + , State p $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) (negate n) <> + lovelaceValueOf (n * p) + ) + (v, p, Withdraw n l) | n >= 0 && l >= 0 -> Just ( Constraints.mustBeSignedBy (tsSeller ts) + , State p $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) (negate n) <> + lovelaceValueOf (negate l) + ) + _ -> Nothing + where + nft :: Integer -> Value + nft = assetClassValue (tsNFT ts) + +{-# INLINABLE tsStateMachine #-} +tsStateMachine :: TokenSale -> StateMachine Integer TSRedeemer +tsStateMachine ts = mkStateMachine (Just $ tsNFT ts) (transition ts) (const False) + +{-# INLINABLE mkTSValidator #-} +mkTSValidator :: TokenSale -> Integer -> TSRedeemer -> ScriptContext -> Bool +mkTSValidator = mkValidator . tsStateMachine + +type TS = StateMachine Integer TSRedeemer + +tsInst :: TokenSale -> Scripts.ScriptInstance TS +tsInst ts = Scripts.validator @TS + ($$(PlutusTx.compile [|| mkTSValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode ts) + $$(PlutusTx.compile [|| wrap ||]) + where + wrap = Scripts.wrapValidator @Integer @TSRedeemer + +tsValidator :: TokenSale -> Validator +tsValidator = Scripts.validatorScript . tsInst + +tsAddress :: TokenSale -> Ledger.Address +tsAddress = scriptAddress . tsValidator + +tsClient :: TokenSale -> StateMachineClient Integer TSRedeemer +tsClient ts = mkStateMachineClient $ StateMachineInstance (tsStateMachine ts) (tsInst ts) + +mapErrorC :: Contract w s C.CurrencyError a -> Contract w s Text a +mapErrorC = mapError $ pack . show + +mapErrorSM :: Contract w s SMContractError a -> Contract w s Text a +mapErrorSM = mapError $ pack . show + +nftName :: TokenName +nftName = "NFT" + +startTS :: HasBlockchainActions s => Maybe CurrencySymbol -> AssetClass -> Contract (Last TokenSale) s Text TokenSale +startTS mcs token = do + pkh <- pubKeyHash <$> Contract.ownPubKey + cs <- case mcs of + Nothing -> C.currencySymbol <$> mapErrorC (C.forgeContract pkh [(nftName, 1)]) + Just cs' -> return cs' + let ts = TokenSale + { tsSeller = pkh + , tsToken = token + , tsNFT = AssetClass (cs, nftName) + } + client = tsClient ts + void $ mapErrorSM $ runInitialise client 0 mempty + tell $ Last $ Just ts + logInfo $ "started token sale " ++ show ts + return ts + +setPrice :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +setPrice ts p = void $ mapErrorSM $ runStep (tsClient ts) $ SetPrice p + +addTokens :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +addTokens ts n = void (mapErrorSM $ runStep (tsClient ts) $ AddTokens n) + +buyTokens :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +buyTokens ts n = void $ mapErrorSM $ runStep (tsClient ts) $ BuyTokens n + +withdraw :: HasBlockchainActions s => TokenSale -> Integer -> Integer -> Contract w s Text () +withdraw ts n l = void $ mapErrorSM $ runStep (tsClient ts) $ Withdraw n l + +type TSStartSchema = BlockchainActions + .\/ Endpoint "start" (CurrencySymbol, TokenName) +type TSStartSchema' = BlockchainActions + .\/ Endpoint "start" (CurrencySymbol, CurrencySymbol, TokenName) +type TSUseSchema = BlockchainActions + .\/ Endpoint "set price" Integer + .\/ Endpoint "add tokens" Integer + .\/ Endpoint "buy tokens" Integer + .\/ Endpoint "withdraw" (Integer, Integer) + +startEndpoint :: Contract (Last TokenSale) TSStartSchema Text () +startEndpoint = startTS' >> startEndpoint + where + startTS' = handleError logError $ endpoint @"start" >>= void . startTS Nothing . AssetClass + +startEndpoint' :: Contract (Last TokenSale) TSStartSchema' Text () +startEndpoint' = startTS' >> startEndpoint' + where + startTS' = handleError logError $ endpoint @"start" >>= \(cs1, cs2, tn) -> void $ startTS (Just cs1) $ AssetClass (cs2, tn) + +useEndpoints :: TokenSale -> Contract () TSUseSchema Text () +useEndpoints ts = (setPrice' `select` addTokens' `select` buyTokens' `select` withdraw') >> useEndpoints ts + where + setPrice' = handleError logError $ endpoint @"set price" >>= setPrice ts + addTokens' = handleError logError $ endpoint @"add tokens" >>= addTokens ts + buyTokens' = handleError logError $ endpoint @"buy tokens" >>= buyTokens ts + withdraw' = handleError logError $ endpoint @"withdraw" >>= uncurry (withdraw ts) diff --git a/code/week08/src/Week08/TokenSaleWithClose.hs b/code/week08/src/Week08/TokenSaleWithClose.hs new file mode 100644 index 0000000..c118da2 --- /dev/null +++ b/code/week08/src/Week08/TokenSaleWithClose.hs @@ -0,0 +1,198 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Week08.TokenSaleWithClose + ( TokenSale (..) + , TSRedeemer (..) + , nftName + , TSStartSchema + , TSStartSchema' + , TSUseSchema + , startEndpoint + , startEndpoint' + , useEndpoints + ) where + +import Control.Monad hiding (fmap) +import Data.Aeson (FromJSON, ToJSON) +import Data.Monoid (Last (..)) +import Data.Text (Text, pack) +import GHC.Generics (Generic) +import Plutus.Contract as Contract hiding (when) +import Plutus.Contract.StateMachine +import qualified Plutus.Contracts.Currency as C +import qualified PlutusTx +import PlutusTx.Prelude hiding (Semigroup(..), check, unless) +import Ledger hiding (singleton) +import Ledger.Ada as Ada +import Ledger.Constraints as Constraints +import qualified Ledger.Typed.Scripts as Scripts +import Ledger.Value +import Prelude (Semigroup (..), Show (..), uncurry) +import qualified Prelude + +data TokenSale = TokenSale + { tsSeller :: !PubKeyHash + , tsToken :: !AssetClass + , tsNFT :: !AssetClass + } deriving (Show, Generic, FromJSON, ToJSON, Prelude.Eq, Prelude.Ord) + +PlutusTx.makeLift ''TokenSale + +data TSRedeemer = + SetPrice Integer + | AddTokens Integer + | BuyTokens Integer + | Withdraw Integer Integer + | Close + deriving (Show, Prelude.Eq) + +PlutusTx.unstableMakeIsData ''TSRedeemer + +{-# INLINABLE lovelaces #-} +lovelaces :: Value -> Integer +lovelaces = Ada.getLovelace . Ada.fromValue + +{-# INLINABLE transition #-} +transition :: TokenSale -> State (Maybe Integer) -> TSRedeemer -> Maybe (TxConstraints Void Void, State (Maybe Integer)) +transition ts s r = case (stateValue s, stateData s, r) of + (v, Just _, SetPrice p) | p >= 0 -> Just ( Constraints.mustBeSignedBy (tsSeller ts) + , State (Just p) $ + v <> + nft (negate 1) + ) + (v, Just p, AddTokens n) | n > 0 -> Just ( mempty + , State (Just p) $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) n + ) + (v, Just p, BuyTokens n) | n > 0 -> Just ( mempty + , State (Just p) $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) (negate n) <> + lovelaceValueOf (n * p) + ) + (v, Just p, Withdraw n l) | n >= 0 && l >= 0 -> Just ( Constraints.mustBeSignedBy (tsSeller ts) + , State (Just p) $ + v <> + nft (negate 1) <> + assetClassValue (tsToken ts) (negate n) <> + lovelaceValueOf (negate l) + ) + (_, Just _, Close) -> Just ( Constraints.mustBeSignedBy (tsSeller ts) + , State Nothing $ + mempty + ) + _ -> Nothing + where + nft :: Integer -> Value + nft = assetClassValue (tsNFT ts) + +{-# INLINABLE tsStateMachine #-} +tsStateMachine :: TokenSale -> StateMachine (Maybe Integer) TSRedeemer +tsStateMachine ts = mkStateMachine (Just $ tsNFT ts) (transition ts) isNothing + +{-# INLINABLE mkTSValidator #-} +mkTSValidator :: TokenSale -> Maybe Integer -> TSRedeemer -> ScriptContext -> Bool +mkTSValidator = mkValidator . tsStateMachine + +type TS = StateMachine (Maybe Integer) TSRedeemer + +tsInst :: TokenSale -> Scripts.ScriptInstance TS +tsInst ts = Scripts.validator @TS + ($$(PlutusTx.compile [|| mkTSValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode ts) + $$(PlutusTx.compile [|| wrap ||]) + where + wrap = Scripts.wrapValidator @(Maybe Integer) @TSRedeemer + +tsValidator :: TokenSale -> Validator +tsValidator = Scripts.validatorScript . tsInst + +tsAddress :: TokenSale -> Ledger.Address +tsAddress = scriptAddress . tsValidator + +tsClient :: TokenSale -> StateMachineClient (Maybe Integer) TSRedeemer +tsClient ts = mkStateMachineClient $ StateMachineInstance (tsStateMachine ts) (tsInst ts) + +mapErrorC :: Contract w s C.CurrencyError a -> Contract w s Text a +mapErrorC = mapError $ pack . show + +mapErrorSM :: Contract w s SMContractError a -> Contract w s Text a +mapErrorSM = mapError $ pack . show + +nftName :: TokenName +nftName = "NFT" + +startTS :: HasBlockchainActions s => Maybe CurrencySymbol -> AssetClass -> Contract (Last TokenSale) s Text TokenSale +startTS mcs token = do + pkh <- pubKeyHash <$> Contract.ownPubKey + cs <- case mcs of + Nothing -> C.currencySymbol <$> mapErrorC (C.forgeContract pkh [(nftName, 1)]) + Just cs' -> return cs' + let ts = TokenSale + { tsSeller = pkh + , tsToken = token + , tsNFT = AssetClass (cs, nftName) + } + client = tsClient ts + void $ mapErrorSM $ runInitialise client (Just 0) mempty + tell $ Last $ Just ts + logInfo $ "started token sale " ++ show ts + return ts + +setPrice :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +setPrice ts p = void $ mapErrorSM $ runStep (tsClient ts) $ SetPrice p + +addTokens :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +addTokens ts n = void (mapErrorSM $ runStep (tsClient ts) $ AddTokens n) + +buyTokens :: HasBlockchainActions s => TokenSale -> Integer -> Contract w s Text () +buyTokens ts n = void $ mapErrorSM $ runStep (tsClient ts) $ BuyTokens n + +withdraw :: HasBlockchainActions s => TokenSale -> Integer -> Integer -> Contract w s Text () +withdraw ts n l = void $ mapErrorSM $ runStep (tsClient ts) $ Withdraw n l + +close :: HasBlockchainActions s => TokenSale -> Contract w s Text () +close ts = void $ mapErrorSM $ runStep (tsClient ts) Close + +type TSStartSchema = BlockchainActions + .\/ Endpoint "start" (CurrencySymbol, TokenName) +type TSStartSchema' = BlockchainActions + .\/ Endpoint "start" (CurrencySymbol, CurrencySymbol, TokenName) +type TSUseSchema = BlockchainActions + .\/ Endpoint "set price" Integer + .\/ Endpoint "add tokens" Integer + .\/ Endpoint "buy tokens" Integer + .\/ Endpoint "withdraw" (Integer, Integer) + .\/ Endpoint "close" () + +startEndpoint :: Contract (Last TokenSale) TSStartSchema Text () +startEndpoint = startTS' >> startEndpoint + where + startTS' = handleError logError $ endpoint @"start" >>= void . startTS Nothing . AssetClass + +startEndpoint' :: Contract (Last TokenSale) TSStartSchema' Text () +startEndpoint' = startTS' >> startEndpoint' + where + startTS' = handleError logError $ endpoint @"start" >>= \(cs1, cs2, tn) -> void $ startTS (Just cs1) $ AssetClass (cs2, tn) + +useEndpoints :: TokenSale -> Contract () TSUseSchema Text () +useEndpoints ts = (setPrice' `select` addTokens' `select` buyTokens' `select` withdraw' `select` close') >> useEndpoints ts + where + setPrice' = handleError logError $ endpoint @"set price" >>= setPrice ts + addTokens' = handleError logError $ endpoint @"add tokens" >>= addTokens ts + buyTokens' = handleError logError $ endpoint @"buy tokens" >>= buyTokens ts + withdraw' = handleError logError $ endpoint @"withdraw" >>= uncurry (withdraw ts) + close' = handleError logError $ endpoint @"close" >> close ts diff --git a/code/week08/test/Spec.hs b/code/week08/test/Spec.hs new file mode 100644 index 0000000..c45518f --- /dev/null +++ b/code/week08/test/Spec.hs @@ -0,0 +1,20 @@ +module Main + ( main + ) where + +import qualified Spec.Model +import qualified Spec.ModelWithClose +import qualified Spec.Trace +import qualified Spec.TraceWithClose +import Test.Tasty + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = testGroup "token sale" + [ Spec.Trace.tests + , Spec.TraceWithClose.tests + , Spec.Model.tests + , Spec.ModelWithClose.tests + ] diff --git a/code/week08/test/Spec/Model.hs b/code/week08/test/Spec/Model.hs new file mode 100644 index 0000000..d7b55af --- /dev/null +++ b/code/week08/test/Spec/Model.hs @@ -0,0 +1,222 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Spec.Model + ( tests + , test + , TSModel (..) + ) where + +import Control.Lens hiding (elements) +import Control.Monad (void, when) +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Maybe (isJust, isNothing) +import Data.Monoid (Last (..)) +import Data.String (IsString (..)) +import Data.Text (Text) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Trace.Emulator +import Ledger hiding (singleton) +import Ledger.Ada as Ada +import Ledger.Value +import Test.QuickCheck +import Test.Tasty +import Test.Tasty.QuickCheck + +import Week08.TokenSale (TokenSale (..), TSStartSchema', TSUseSchema, startEndpoint', useEndpoints, nftName) + +data TSState = TSState + { _tssPrice :: !Integer + , _tssLovelace :: !Integer + , _tssToken :: !Integer + } deriving Show + +makeLenses ''TSState + +newtype TSModel = TSModel {_tsModel :: Map Wallet TSState} + deriving Show + +makeLenses ''TSModel + +tests :: TestTree +tests = testProperty "token sale model" prop_TS + +instance ContractModel TSModel where + + data Action TSModel = + Start Wallet + | SetPrice Wallet Wallet Integer + | AddTokens Wallet Wallet Integer + | Withdraw Wallet Wallet Integer Integer + | BuyTokens Wallet Wallet Integer + deriving (Show, Eq) + + data ContractInstanceKey TSModel w s e where + StartKey :: Wallet -> ContractInstanceKey TSModel (Last TokenSale) TSStartSchema' Text + UseKey :: Wallet -> Wallet -> ContractInstanceKey TSModel () TSUseSchema Text + + instanceTag key _ = fromString $ "instance tag for: " ++ show key + + arbitraryAction _ = oneof $ + (Start <$> genWallet) : + [ SetPrice <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ AddTokens <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ BuyTokens <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ Withdraw <$> genWallet <*> genWallet <*> genNonNeg <*> genNonNeg ] + + initialState = TSModel Map.empty + + nextState (Start w) = do + withdraw w $ nfts Map.! w + (tsModel . at w) $= Just (TSState 0 0 0) + wait 1 + + nextState (SetPrice v w p) = do + when (v == w) $ + (tsModel . ix v . tssPrice) $= p + wait 1 + + nextState (AddTokens v w n) = do + started <- hasStarted v -- has the token sale started? + when (n > 0 && started) $ do + bc <- askModelState $ view $ balanceChange w + let token = tokens Map.! v + when (tokenAmt + assetClassValueOf bc token >= n) $ do -- does the wallet have the tokens to give? + withdraw w $ assetClassValue token n + (tsModel . ix v . tssToken) $~ (+ n) + wait 1 + + nextState (BuyTokens v w n) = do + when (n > 0) $ do + m <- getTSState v + case m of + Just t + | t ^. tssToken >= n -> do + let p = t ^. tssPrice + l = p * n + withdraw w $ lovelaceValueOf l + deposit w $ assetClassValue (tokens Map.! v) n + (tsModel . ix v . tssLovelace) $~ (+ l) + (tsModel . ix v . tssToken) $~ (+ (- n)) + _ -> return () + wait 1 + + nextState (Withdraw v w n l) = do + when (v == w) $ do + m <- getTSState v + case m of + Just t + | t ^. tssToken >= n && t ^. tssLovelace >= l -> do + deposit w $ lovelaceValueOf l <> assetClassValue (tokens Map.! w) n + (tsModel . ix v . tssLovelace) $~ (+ (- l)) + (tsModel . ix v . tssToken) $~ (+ (- n)) + _ -> return () + wait 1 + + perform h _ cmd = case cmd of + (Start w) -> callEndpoint @"start" (h $ StartKey w) (nftCurrencies Map.! w, tokenCurrencies Map.! w, tokenNames Map.! w) >> delay 1 + (SetPrice v w p) -> callEndpoint @"set price" (h $ UseKey v w) p >> delay 1 + (AddTokens v w n) -> callEndpoint @"add tokens" (h $ UseKey v w) n >> delay 1 + (BuyTokens v w n) -> callEndpoint @"buy tokens" (h $ UseKey v w) n >> delay 1 + (Withdraw v w n l) -> callEndpoint @"withdraw" (h $ UseKey v w) (n, l) >> delay 1 + + precondition s (Start w) = isNothing $ getTSState' s w + precondition s (SetPrice v _ _) = isJust $ getTSState' s v + precondition s (AddTokens v _ _) = isJust $ getTSState' s v + precondition s (BuyTokens v _ _) = isJust $ getTSState' s v + precondition s (Withdraw v _ _ _) = isJust $ getTSState' s v + +deriving instance Eq (ContractInstanceKey TSModel w s e) +deriving instance Show (ContractInstanceKey TSModel w s e) + +getTSState' :: ModelState TSModel -> Wallet -> Maybe TSState +getTSState' s v = s ^. contractState . tsModel . at v + +getTSState :: Wallet -> Spec TSModel (Maybe TSState) +getTSState v = do + s <- getModelState + return $ getTSState' s v + +hasStarted :: Wallet -> Spec TSModel Bool +hasStarted v = isJust <$> getTSState v + +w1, w2 :: Wallet +w1 = Wallet 1 +w2 = Wallet 2 + +wallets :: [Wallet] +wallets = [w1, w2] + +tokenCurrencies, nftCurrencies :: Map Wallet CurrencySymbol +tokenCurrencies = Map.fromList $ zip wallets ["aa", "bb"] +nftCurrencies = Map.fromList $ zip wallets ["01", "02"] + +tokenNames :: Map Wallet TokenName +tokenNames = Map.fromList $ zip wallets ["A", "B"] + +tokens :: Map Wallet AssetClass +tokens = Map.fromList [(w, AssetClass (tokenCurrencies Map.! w, tokenNames Map.! w)) | w <- wallets] + +nftAssets :: Map Wallet AssetClass +nftAssets = Map.fromList [(w, AssetClass (nftCurrencies Map.! w, nftName)) | w <- wallets] + +nfts :: Map Wallet Value +nfts = Map.fromList [(w, assetClassValue (nftAssets Map.! w) 1) | w <- wallets] + +tss :: Map Wallet TokenSale +tss = Map.fromList + [ (w, TokenSale { tsSeller = pubKeyHash $ walletPubKey w + , tsToken = tokens Map.! w + , tsNFT = nftAssets Map.! w + }) + | w <- wallets + ] + +delay :: Int -> EmulatorTrace () +delay = void . waitNSlots . fromIntegral + +instanceSpec :: [ContractInstanceSpec TSModel] +instanceSpec = + [ContractInstanceSpec (StartKey w) w startEndpoint' | w <- wallets] ++ + [ContractInstanceSpec (UseKey v w) w $ useEndpoints $ tss Map.! v | v <- wallets, w <- wallets] + +genWallet :: Gen Wallet +genWallet = elements wallets + +genNonNeg :: Gen Integer +genNonNeg = getNonNegative <$> arbitrary + +tokenAmt :: Integer +tokenAmt = 1_000 + +prop_TS :: Actions TSModel -> Property +prop_TS = withMaxSuccess 100 . propRunActionsWithOptions + (defaultCheckOptions & emulatorConfig .~ EmulatorConfig (Left d)) + instanceSpec + (const $ pure True) + where + d :: InitialDistribution + d = Map.fromList $ [ ( w + , lovelaceValueOf 1000_000_000 <> + (nfts Map.! w) <> + mconcat [assetClassValue t tokenAmt | t <- Map.elems tokens]) + | w <- wallets + ] + +test :: IO () +test = quickCheck prop_TS diff --git a/code/week08/test/Spec/ModelWithClose.hs b/code/week08/test/Spec/ModelWithClose.hs new file mode 100644 index 0000000..4dcea71 --- /dev/null +++ b/code/week08/test/Spec/ModelWithClose.hs @@ -0,0 +1,238 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Spec.ModelWithClose + ( tests + , test + , TSModel (..) + ) where + +import Control.Lens hiding (elements) +import Control.Monad (void, when) +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Maybe (isJust, isNothing) +import Data.Monoid (Last (..)) +import Data.String (IsString (..)) +import Data.Text (Text) +import Plutus.Contract.Test +import Plutus.Contract.Test.ContractModel +import Plutus.Trace.Emulator +import Ledger hiding (singleton) +import Ledger.Ada as Ada +import Ledger.Value +import Test.QuickCheck +import Test.Tasty +import Test.Tasty.QuickCheck + +import Week08.TokenSaleWithClose (TokenSale (..), TSStartSchema', TSUseSchema, startEndpoint', useEndpoints, nftName) + +data TSState = TSState + { _tssPrice :: !Integer + , _tssLovelace :: !Integer + , _tssToken :: !Integer + } deriving Show + +makeLenses ''TSState + +newtype TSModel = TSModel {_tsModel :: Map Wallet TSState} + deriving Show + +makeLenses ''TSModel + +tests :: TestTree +tests = testProperty "token sale model" prop_TS + +instance ContractModel TSModel where + + data Action TSModel = + Start Wallet + | SetPrice Wallet Wallet Integer + | AddTokens Wallet Wallet Integer + | Withdraw Wallet Wallet Integer Integer + | BuyTokens Wallet Wallet Integer + | Close Wallet Wallet + deriving (Show, Eq) + + data ContractInstanceKey TSModel w s e where + StartKey :: Wallet -> ContractInstanceKey TSModel (Last TokenSale) TSStartSchema' Text + UseKey :: Wallet -> Wallet -> ContractInstanceKey TSModel () TSUseSchema Text + + instanceTag key _ = fromString $ "instance tag for: " ++ show key + + arbitraryAction _ = oneof $ + (Start <$> genWallet) : + [ SetPrice <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ AddTokens <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ BuyTokens <$> genWallet <*> genWallet <*> genNonNeg ] ++ + [ Withdraw <$> genWallet <*> genWallet <*> genNonNeg <*> genNonNeg ] ++ + [ Close <$> genWallet <*> genWallet ] + + initialState = TSModel Map.empty + + nextState (Start w) = do + withdraw w $ nfts Map.! w + (tsModel . at w) $= Just (TSState 0 0 0) + wait 1 + + nextState (SetPrice v w p) = do + when (v == w) $ + (tsModel . ix v . tssPrice) $= p + wait 1 + + nextState (AddTokens v w n) = do + started <- hasStarted v -- has the token sale started? + when (n > 0 && started) $ do + bc <- askModelState $ view $ balanceChange w + let token = tokens Map.! v + when (tokenAmt + assetClassValueOf bc token >= n) $ do -- does the wallet have the tokens to give? + withdraw w $ assetClassValue token n + (tsModel . ix v . tssToken) $~ (+ n) + wait 1 + + nextState (BuyTokens v w n) = do + when (n > 0) $ do + m <- getTSState v + case m of + Just t + | t ^. tssToken >= n -> do + let p = t ^. tssPrice + l = p * n + withdraw w $ lovelaceValueOf l + deposit w $ assetClassValue (tokens Map.! v) n + (tsModel . ix v . tssLovelace) $~ (+ l) + (tsModel . ix v . tssToken) $~ (+ (- n)) + _ -> return () + wait 1 + + nextState (Withdraw v w n l) = do + when (v == w) $ do + m <- getTSState v + case m of + Just t + | t ^. tssToken >= n && t ^. tssLovelace >= l -> do + deposit w $ lovelaceValueOf l <> assetClassValue (tokens Map.! w) n + (tsModel . ix v . tssLovelace) $~ (+ (- l)) + (tsModel . ix v . tssToken) $~ (+ (- n)) + _ -> return () + wait 1 + + nextState (Close v w) = do + when (v == w) $ do + m <- getTSState v + case m of + Just t -> do + deposit w $ lovelaceValueOf (t ^. tssLovelace) <> + assetClassValue (tokens Map.! w) (t ^. tssToken) <> + (nfts Map.! w) + (tsModel . at v) $= Nothing + _ -> return () + wait 1 + + perform h _ cmd = case cmd of + (Start w) -> callEndpoint @"start" (h $ StartKey w) (nftCurrencies Map.! w, tokenCurrencies Map.! w, tokenNames Map.! w) >> delay 1 + (SetPrice v w p) -> callEndpoint @"set price" (h $ UseKey v w) p >> delay 1 + (AddTokens v w n) -> callEndpoint @"add tokens" (h $ UseKey v w) n >> delay 1 + (BuyTokens v w n) -> callEndpoint @"buy tokens" (h $ UseKey v w) n >> delay 1 + (Withdraw v w n l) -> callEndpoint @"withdraw" (h $ UseKey v w) (n, l) >> delay 1 + (Close v w) -> callEndpoint @"close" (h $ UseKey v w) () >> delay 1 + + precondition s (Start w) = isNothing $ getTSState' s w + precondition s (SetPrice v _ _) = isJust $ getTSState' s v + precondition s (AddTokens v _ _) = isJust $ getTSState' s v + precondition s (BuyTokens v _ _) = isJust $ getTSState' s v + precondition s (Withdraw v _ _ _) = isJust $ getTSState' s v + precondition s (Close v _) = isJust $ getTSState' s v + +deriving instance Eq (ContractInstanceKey TSModel w s e) +deriving instance Show (ContractInstanceKey TSModel w s e) + +getTSState' :: ModelState TSModel -> Wallet -> Maybe TSState +getTSState' s v = s ^. contractState . tsModel . at v + +getTSState :: Wallet -> Spec TSModel (Maybe TSState) +getTSState v = do + s <- getModelState + return $ getTSState' s v + +hasStarted :: Wallet -> Spec TSModel Bool +hasStarted v = isJust <$> getTSState v + +w1, w2 :: Wallet +w1 = Wallet 1 +w2 = Wallet 2 + +wallets :: [Wallet] +wallets = [w1, w2] + +tokenCurrencies, nftCurrencies :: Map Wallet CurrencySymbol +tokenCurrencies = Map.fromList $ zip wallets ["aa", "bb"] +nftCurrencies = Map.fromList $ zip wallets ["01", "02"] + +tokenNames :: Map Wallet TokenName +tokenNames = Map.fromList $ zip wallets ["A", "B"] + +tokens :: Map Wallet AssetClass +tokens = Map.fromList [(w, AssetClass (tokenCurrencies Map.! w, tokenNames Map.! w)) | w <- wallets] + +nftAssets :: Map Wallet AssetClass +nftAssets = Map.fromList [(w, AssetClass (nftCurrencies Map.! w, nftName)) | w <- wallets] + +nfts :: Map Wallet Value +nfts = Map.fromList [(w, assetClassValue (nftAssets Map.! w) 1) | w <- wallets] + +tss :: Map Wallet TokenSale +tss = Map.fromList + [ (w, TokenSale { tsSeller = pubKeyHash $ walletPubKey w + , tsToken = tokens Map.! w + , tsNFT = nftAssets Map.! w + }) + | w <- wallets + ] + +delay :: Int -> EmulatorTrace () +delay = void . waitNSlots . fromIntegral + +instanceSpec :: [ContractInstanceSpec TSModel] +instanceSpec = + [ContractInstanceSpec (StartKey w) w startEndpoint' | w <- wallets] ++ + [ContractInstanceSpec (UseKey v w) w $ useEndpoints $ tss Map.! v | v <- wallets, w <- wallets] + +genWallet :: Gen Wallet +genWallet = elements wallets + +genNonNeg :: Gen Integer +genNonNeg = getNonNegative <$> arbitrary + +tokenAmt :: Integer +tokenAmt = 1_000 + +prop_TS :: Actions TSModel -> Property +prop_TS = withMaxSuccess 100 . propRunActionsWithOptions + (defaultCheckOptions & emulatorConfig .~ EmulatorConfig (Left d)) + instanceSpec + (const $ pure True) + where + d :: InitialDistribution + d = Map.fromList $ [ ( w + , lovelaceValueOf 1000_000_000 <> + (nfts Map.! w) <> + mconcat [assetClassValue t tokenAmt | t <- Map.elems tokens]) + | w <- wallets + ] + +test :: IO () +test = quickCheck prop_TS diff --git a/code/week08/test/Spec/Trace.hs b/code/week08/test/Spec/Trace.hs new file mode 100644 index 0000000..9796228 --- /dev/null +++ b/code/week08/test/Spec/Trace.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Spec.Trace + ( tests + , runMyTrace + ) where + +import Control.Lens +import Control.Monad hiding (fmap) +import Control.Monad.Freer.Extras as Extras +import Data.Default (Default (..)) +import qualified Data.Map as Map +import Data.Monoid (Last (..)) +import Ledger +import Ledger.Value +import Ledger.Ada as Ada +import Plutus.Contract.Test +import Plutus.Trace.Emulator as Emulator +import PlutusTx.Prelude +import Prelude (IO, String, Show (..)) +import Test.Tasty + +import Week08.TokenSale + +tests :: TestTree +tests = checkPredicateOptions + (defaultCheckOptions & emulatorConfig .~ emCfg) + "token sale trace" + ( walletFundsChange (Wallet 1) (Ada.lovelaceValueOf 10_000_000 <> assetClassValue token (-60)) + .&&. walletFundsChange (Wallet 2) (Ada.lovelaceValueOf (-20_000_000) <> assetClassValue token 20) + .&&. walletFundsChange (Wallet 3) (Ada.lovelaceValueOf (- 5_000_000) <> assetClassValue token 5) + ) + myTrace + +runMyTrace :: IO () +runMyTrace = runEmulatorTraceIO' def emCfg myTrace + +emCfg :: EmulatorConfig +emCfg = EmulatorConfig $ Left $ Map.fromList [(Wallet w, v) | w <- [1 .. 3]] + where + v :: Value + v = Ada.lovelaceValueOf 1000_000_000 <> assetClassValue token 1000 + +currency :: CurrencySymbol +currency = "aa" + +name :: TokenName +name = "A" + +token :: AssetClass +token = AssetClass (currency, name) + +myTrace :: EmulatorTrace () +myTrace = do + h <- activateContractWallet (Wallet 1) startEndpoint + callEndpoint @"start" h (currency, name) + void $ Emulator.waitNSlots 5 + Last m <- observableState h + case m of + Nothing -> Extras.logError @String "error starting token sale" + Just ts -> do + Extras.logInfo $ "started token sale " ++ show ts + + h1 <- activateContractWallet (Wallet 1) $ useEndpoints ts + h2 <- activateContractWallet (Wallet 2) $ useEndpoints ts + h3 <- activateContractWallet (Wallet 3) $ useEndpoints ts + + callEndpoint @"set price" h1 1_000_000 + void $ Emulator.waitNSlots 5 + + callEndpoint @"add tokens" h1 100 + void $ Emulator.waitNSlots 5 + + callEndpoint @"buy tokens" h2 20 + void $ Emulator.waitNSlots 5 + + callEndpoint @"buy tokens" h3 5 + void $ Emulator.waitNSlots 5 + + callEndpoint @"withdraw" h1 (40, 10_000_000) + void $ Emulator.waitNSlots 5 diff --git a/code/week08/test/Spec/TraceWithClose.hs b/code/week08/test/Spec/TraceWithClose.hs new file mode 100644 index 0000000..263554a --- /dev/null +++ b/code/week08/test/Spec/TraceWithClose.hs @@ -0,0 +1,101 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Spec.TraceWithClose + ( tests + , runMyTrace + ) where + +import Control.Lens +import Control.Monad hiding (fmap) +import Control.Monad.Freer.Extras as Extras +import Data.Default (Default (..)) +import qualified Data.Map as Map +import Data.Monoid (Last (..)) +import Ledger +import Ledger.Value +import Ledger.Ada as Ada +import Plutus.Contract.Test +import Plutus.Trace.Emulator as Emulator +import PlutusTx.Prelude +import Prelude (IO, String, Show (..)) +import Test.Tasty + +import Week08.TokenSaleWithClose + +tests :: TestTree +tests = checkPredicateOptions + (defaultCheckOptions & emulatorConfig .~ emCfg) + "token sale trace" + ( walletFundsChange (Wallet 1) (Ada.lovelaceValueOf 25_000_000 <> assetClassValue token (-25)) + .&&. walletFundsChange (Wallet 2) (Ada.lovelaceValueOf (-20_000_000) <> assetClassValue token 20) + .&&. walletFundsChange (Wallet 3) (Ada.lovelaceValueOf (- 5_000_000) <> assetClassValue token 5) + ) + myTrace + +runMyTrace :: IO () +runMyTrace = runEmulatorTraceIO' def emCfg myTrace + +emCfg :: EmulatorConfig +emCfg = EmulatorConfig $ Left $ Map.fromList [(Wallet w, v' w) | w <- [1 .. 3]] + where + v :: Value + v = Ada.lovelaceValueOf 1000_000_000 <> assetClassValue token 1000 + + v' :: Integer -> Value + v' w + | w == 1 = v <> assetClassValue nft 1 + | otherwise = v + +tokenCurrency, nftCurrency :: CurrencySymbol +tokenCurrency = "aa" +nftCurrency = "01" + +tokenName' :: TokenName +tokenName' = "A" + +token, nft :: AssetClass +token = AssetClass (tokenCurrency, tokenName') +nft = AssetClass (nftCurrency, nftName) + +myTrace :: EmulatorTrace () +myTrace = do + h <- activateContractWallet (Wallet 1) startEndpoint' + callEndpoint @"start" h (nftCurrency, tokenCurrency, tokenName') + void $ Emulator.waitNSlots 5 + Last m <- observableState h + case m of + Nothing -> Extras.logError @String "error starting token sale" + Just ts -> do + Extras.logInfo $ "started token sale " ++ show ts + + h1 <- activateContractWallet (Wallet 1) $ useEndpoints ts + h2 <- activateContractWallet (Wallet 2) $ useEndpoints ts + h3 <- activateContractWallet (Wallet 3) $ useEndpoints ts + + callEndpoint @"set price" h1 1_000_000 + void $ Emulator.waitNSlots 5 + + callEndpoint @"add tokens" h1 100 + void $ Emulator.waitNSlots 5 + + callEndpoint @"buy tokens" h2 20 + void $ Emulator.waitNSlots 5 + + callEndpoint @"buy tokens" h3 5 + void $ Emulator.waitNSlots 5 + + callEndpoint @"close" h1 () +-- callEndpoint @"withdraw" h1 (40, 10_000_000) + void $ Emulator.waitNSlots 5 diff --git a/code/week09/.devcontainer/devcontainer.json b/code/week09/.devcontainer/devcontainer.json new file mode 100644 index 0000000..51f7dce --- /dev/null +++ b/code/week09/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "Plutus Starter Project", + "image": "plutus-devcontainer:latest", + + "remoteUser": "plutus", + + "mounts": [ + // This shares cabal's remote repository state with the host. We don't mount the whole of '.cabal', because + // 1. '.cabal/config' contains absolute paths that will only make sense on the host, and + // 2. '.cabal/store' is not necessarily portable to different version of cabal etc. + "source=${localEnv:HOME}/.cabal/packages,target=/home/plutus/.cabal/packages,type=bind,consistency=cached", + ], + + "settings": { + // Note: don't change from bash so it runs .bashrc + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // IDs of extensions inside container + "extensions": [ + "haskell.haskell" + ], +} diff --git a/code/week09/.gitignore b/code/week09/.gitignore new file mode 100644 index 0000000..2bbd0a6 --- /dev/null +++ b/code/week09/.gitignore @@ -0,0 +1,6 @@ +dist-newstyle/ +oracle.cid +W2.cid +W3.cid +W4.cid +W5.cid diff --git a/code/week09/LICENSE b/code/week09/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/code/week09/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/code/week09/app/marlowe.hs b/code/week09/app/marlowe.hs new file mode 100644 index 0000000..72c5a12 --- /dev/null +++ b/code/week09/app/marlowe.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE OverloadedStrings #-} +import Language.Marlowe.Extended + +main :: IO () +main = print . pretty $ contract "Charles" "Simon" "Alex" $ Constant 100 + +choiceId :: Party -> ChoiceId +choiceId p = ChoiceId "Winner" p + +contract :: Party -> Party -> Party -> Value -> Contract +contract alice bob charlie deposit = + When + [ f alice bob + , f bob alice + ] + 10 Close + where + f :: Party -> Party -> Case + f x y = + Case + (Deposit + x + x + ada + deposit + ) + (When + [Case + (Deposit + y + y + ada + deposit + ) + (When + [Case + (Choice + (choiceId charlie) + [Bound 1 2] + ) + (If + (ValueEQ + (ChoiceValue $ choiceId charlie) + (Constant 1) + ) + (Pay + bob + (Account alice) + ada + deposit + Close + ) + (Pay + alice + (Account bob) + ada + deposit + Close + ) + )] + 30 Close + )] + 20 Close + ) diff --git a/code/week09/app/solution.hs b/code/week09/app/solution.hs new file mode 100644 index 0000000..0cfe0df --- /dev/null +++ b/code/week09/app/solution.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE OverloadedStrings #-} +import Language.Marlowe.Extended + +main :: IO () +main = print . pretty $ contract "Alice" "Bob" "Charlie" $ Constant 10 + +choiceId :: Party -> ChoiceId +choiceId = ChoiceId "Winner" + +contract :: Party -> Party -> Party -> Value -> Contract +contract alice bob charlie deposit = + When + [Case (Deposit charlie charlie ada $ AddValue deposit deposit) $ + When + [ f alice bob + , f bob alice + ] + 20 Close + ] + 10 Close + where + f :: Party -> Party -> Case + f x y = + Case + (Deposit x x ada deposit + ) + (When + [Case + (Deposit y y ada deposit + ) + (When + [Case + (Choice (choiceId charlie) [Bound 1 2] + ) + (If + (ValueEQ (ChoiceValue $ choiceId charlie) (Constant 1) + ) + (Pay bob (Account alice) ada deposit Close) + (Pay alice (Account bob) ada deposit Close) + )] + 40 + (Pay charlie (Account alice) ada deposit $ + Pay charlie (Account bob) ada deposit + Close) + )] + 30 Close + ) diff --git a/code/week09/cabal.project b/code/week09/cabal.project new file mode 100644 index 0000000..782e3d4 --- /dev/null +++ b/code/week09/cabal.project @@ -0,0 +1,146 @@ +index-state: 2021-04-13T00:00:00Z + +packages: ./. + +-- You never, ever, want this. +write-ghc-environment-files: never + +-- Always build tests and benchmarks. +tests: true +benchmarks: true + +source-repository-package + type: git + location: https://github.com/input-output-hk/plutus.git + subdir: + freer-extras + marlowe + playground-common + plutus-core + plutus-contract + plutus-ledger + plutus-ledger-api + plutus-pab + plutus-tx + plutus-tx-plugin + plutus-use-cases + prettyprinter-configurable + quickcheck-dynamic + tag: 13da6d416b2b47cdb6f287ff078b9e759bb90b7f + +-- The following sections are copied from the 'plutus' repository cabal.project at the revision +-- given above. +-- This is necessary because the 'plutus' libraries depend on a number of other libraries which are +-- not on Hackage, and so need to be pulled in as `source-repository-package`s themselves. Make sure to +-- re-update this section from the template when you do an upgrade. + +-- This is also needed so evenful-sql-common will build with a +-- newer version of persistent. See stack.yaml for the mirrored +-- configuration. +package eventful-sql-common + ghc-options: -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances + +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 + , size-based:template-haskell + + -- The following two dependencies are needed by plutus. + , eventful-sql-common:persistent + , eventful-sql-common:persistent-template + +constraints: + -- aws-lambda-haskell-runtime-wai doesn't compile with newer versions + aws-lambda-haskell-runtime <= 3.0.3 + -- big breaking change here, inline-r doens't have an upper bound + , singletons < 3.0 + -- breaks eventful even more than it already was + , persistent-template < 2.12 + +-- See the note on nix/pkgs/default.nix:agdaPackages for why this is here. +-- (NOTE this will change to ieee754 in newer versions of nixpkgs). +extra-packages: ieee, filemanip + + +-- Needs some patches, but upstream seems to be fairly dead (no activity in > 1 year) +source-repository-package + type: git + location: https://github.com/shmish111/purescript-bridge.git + tag: 6a92d7853ea514be8b70bab5e72077bf5a510596 + +source-repository-package + type: git + location: https://github.com/shmish111/servant-purescript.git + tag: a76104490499aa72d40c2790d10e9383e0dbde63 + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-crypto.git + tag: f73079303f663e028288f9f4a9e08bcca39a923e + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-base + tag: 4251c0bb6e4f443f00231d28f5f70d42876da055 + subdir: + binary + binary/test + slotting + cardano-crypto-class + cardano-crypto-praos + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-prelude + tag: ee4e7b547a991876e6b05ba542f4e62909f4a571 + subdir: + cardano-prelude + cardano-prelude-test + +source-repository-package + type: git + location: https://github.com/input-output-hk/ouroboros-network + tag: 6cb9052bde39472a0555d19ade8a42da63d3e904 + subdir: + typed-protocols + typed-protocols-examples + ouroboros-network + ouroboros-network-testing + ouroboros-network-framework + io-sim + io-sim-classes + network-mux + Win32-network + +source-repository-package + type: git + location: https://github.com/input-output-hk/iohk-monitoring-framework + tag: a89c38ed5825ba17ca79fddb85651007753d699d + subdir: + iohk-monitoring + tracer-transformers + contra-tracer + plugins/backend-ekg + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-ledger-specs + tag: 097890495cbb0e8b62106bcd090a5721c3f4b36f + subdir: + byron/chain/executable-spec + byron/crypto + byron/crypto/test + byron/ledger/executable-spec + byron/ledger/impl + byron/ledger/impl/test + semantics/executable-spec + semantics/small-steps-test + shelley/chain-and-ledger/dependencies/non-integer + shelley/chain-and-ledger/executable-spec + shelley-ma/impl + +source-repository-package + type: git + location: https://github.com/input-output-hk/goblins + tag: cde90a2b27f79187ca8310b6549331e59595e7ba diff --git a/code/week09/hie.yaml b/code/week09/hie.yaml new file mode 100644 index 0000000..a9ce8cc --- /dev/null +++ b/code/week09/hie.yaml @@ -0,0 +1,6 @@ +cradle: + cabal: + - path: "./app/marlowe.hs" + component: "exe:marlowe" + - path: "./app/solution.hs" + component: "exe:solution" diff --git a/code/week09/plutus-pioneer-program-week09.cabal b/code/week09/plutus-pioneer-program-week09.cabal new file mode 100644 index 0000000..e1ae9da --- /dev/null +++ b/code/week09/plutus-pioneer-program-week09.cabal @@ -0,0 +1,25 @@ +Cabal-Version: 2.4 +Name: plutus-pioneer-program-week09 +Version: 0.1.0.0 +Author: Lars Bruenjes +Maintainer: brunjlar@gmail.com +Build-Type: Simple +Copyright: © 2021 Lars Bruenjes +License: Apache-2.0 +License-files: LICENSE + +executable marlowe + hs-source-dirs: app + main-is: marlowe.hs + build-depends: base ^>=4.14.1.0 + , marlowe + default-language: Haskell2010 + ghc-options: -Wall -O2 + +executable solution + hs-source-dirs: app + main-is: solution.hs + build-depends: base ^>=4.14.1.0 + , marlowe + default-language: Haskell2010 + ghc-options: -Wall -O2 diff --git a/code/week10/.devcontainer/devcontainer.json b/code/week10/.devcontainer/devcontainer.json new file mode 100644 index 0000000..51f7dce --- /dev/null +++ b/code/week10/.devcontainer/devcontainer.json @@ -0,0 +1,23 @@ +{ + "name": "Plutus Starter Project", + "image": "plutus-devcontainer:latest", + + "remoteUser": "plutus", + + "mounts": [ + // This shares cabal's remote repository state with the host. We don't mount the whole of '.cabal', because + // 1. '.cabal/config' contains absolute paths that will only make sense on the host, and + // 2. '.cabal/store' is not necessarily portable to different version of cabal etc. + "source=${localEnv:HOME}/.cabal/packages,target=/home/plutus/.cabal/packages,type=bind,consistency=cached", + ], + + "settings": { + // Note: don't change from bash so it runs .bashrc + "terminal.integrated.shell.linux": "/bin/bash" + }, + + // IDs of extensions inside container + "extensions": [ + "haskell.haskell" + ], +} diff --git a/code/week10/.gitignore b/code/week10/.gitignore new file mode 100644 index 0000000..64a29ea --- /dev/null +++ b/code/week10/.gitignore @@ -0,0 +1,6 @@ +dist-newstyle/ +symbol.json +W1.cid +W2.cid +W3.cid +W4.cid diff --git a/code/week10/LICENSE b/code/week10/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/code/week10/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/code/week10/add.sh b/code/week10/add.sh new file mode 100755 index 0000000..f178052 --- /dev/null +++ b/code/week10/add.sh @@ -0,0 +1,9 @@ +#1/bin/sh + +symbol=$( cat symbol.json ) +body="{\"apAmountA\":$2,\"apAmountB\":$4,\"apCoinB\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$5\"}]},\"apCoinA\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$3\"}]}}" +echo $body + +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/add" \ +--header 'Content-Type: application/json' \ +--data-raw $body diff --git a/code/week10/app/Uniswap.hs b/code/week10/app/Uniswap.hs new file mode 100644 index 0000000..676898d --- /dev/null +++ b/code/week10/app/Uniswap.hs @@ -0,0 +1,60 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} + +module Uniswap where + +import Control.Monad (forM_, when) +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Semigroup as Semigroup +import Data.Text.Prettyprint.Doc (Pretty (..), viaShow) +import GHC.Generics (Generic) +import Ledger +import Ledger.Constraints +import Ledger.Value as Value +import Plutus.Contract hiding (when) +import qualified Plutus.Contracts.Currency as Currency +import qualified Plutus.Contracts.Uniswap as Uniswap +import Wallet.Emulator.Types (Wallet (..), walletPubKey) + +data UniswapContracts = + Init + | UniswapStart + | UniswapUser Uniswap.Uniswap + deriving (Eq, Ord, Show, Generic) + deriving anyclass (FromJSON, ToJSON) + +instance Pretty UniswapContracts where + pretty = viaShow + +initContract :: Contract (Maybe (Semigroup.Last Currency.OneShotCurrency)) Currency.CurrencySchema Currency.CurrencyError () +initContract = do + ownPK <- pubKeyHash <$> ownPubKey + cur <- Currency.forgeContract ownPK [(tn, fromIntegral (length wallets) * amount) | tn <- tokenNames] + let cs = Currency.currencySymbol cur + v = mconcat [Value.singleton cs tn amount | tn <- tokenNames] + forM_ wallets $ \w -> do + let pkh = pubKeyHash $ walletPubKey w + when (pkh /= ownPK) $ do + tx <- submitTx $ mustPayToPubKey pkh v + awaitTxConfirmed $ txId tx + tell $ Just $ Semigroup.Last cur + where + amount = 1000000 + +wallets :: [Wallet] +wallets = [Wallet i | i <- [1 .. 4]] + +tokenNames :: [TokenName] +tokenNames = ["A", "B", "C", "D"] + +cidFile :: Wallet -> FilePath +cidFile w = "W" ++ show (getWallet w) ++ ".cid" diff --git a/code/week10/app/uniswap-client.hs b/code/week10/app/uniswap-client.hs new file mode 100644 index 0000000..478d4be --- /dev/null +++ b/code/week10/app/uniswap-client.hs @@ -0,0 +1,228 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Main + ( main + ) where + +import Control.Concurrent +import Control.Exception +import Control.Monad (forM_, when) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (Result (..), ToJSON, decode, encode, fromJSON) +import qualified Data.ByteString.Lazy.Char8 as B8 +import qualified Data.ByteString.Lazy as LB +import Data.Monoid (Last (..)) +import Data.Proxy (Proxy (..)) +import Data.String (IsString (..)) +import Data.Text (Text, pack) +import Data.UUID hiding (fromString) +import Ledger.Value (AssetClass (..), CurrencySymbol, Value, flattenValue, TokenName) +import Network.HTTP.Req +import qualified Plutus.Contracts.Uniswap as US +import Plutus.PAB.Events.ContractInstanceState (PartiallyDecodedResponse (..)) +import Plutus.PAB.Webserver.Types +import System.Environment (getArgs) +import System.Exit (exitFailure) +import Text.Printf (printf) +import Text.Read (readMaybe) +import Wallet.Emulator.Types (Wallet (..)) + +import Uniswap (cidFile, UniswapContracts) + +main :: IO () +main = do + w <- Wallet . read . head <$> getArgs + cid <- read <$> readFile (cidFile w) + mcs <- decode <$> LB.readFile "symbol.json" + case mcs of + Nothing -> putStrLn "invalid symbol.json" >> exitFailure + Just cs -> do + putStrLn $ "cid: " ++ show cid + putStrLn $ "symbol: " ++ show (cs :: CurrencySymbol) + go cid cs + where + go :: UUID -> CurrencySymbol -> IO a + go cid cs = do + cmd <- readCommandIO + case cmd of + Funds -> getFunds cid + Pools -> getPools cid + Create amtA tnA amtB tnB -> createPool cid $ toCreateParams cs amtA tnA amtB tnB + Add amtA tnA amtB tnB -> addLiquidity cid $ toAddParams cs amtA tnA amtB tnB + Remove amt tnA tnB -> removeLiquidity cid $ toRemoveParams cs amt tnA tnB + Close tnA tnB -> closePool cid $ toCloseParams cs tnA tnB + Swap amtA tnA tnB -> swap cid $ toSwapParams cs amtA tnA tnB + go cid cs + +data Command = + Funds + | Pools + | Create Integer Char Integer Char + | Add Integer Char Integer Char + | Remove Integer Char Char + | Close Char Char + | Swap Integer Char Char + deriving (Show, Read, Eq, Ord) + +readCommandIO :: IO Command +readCommandIO = do + putStrLn "Enter a command: Funds, Pools, Create amtA tnA amtB tnB, Add amtA tnA amtB tnB, Remove amt tnA tnB, Close tnA tnB, Swap amtA tnA tnB" + s <- getLine + maybe readCommandIO return $ readMaybe s + +toCoin :: CurrencySymbol -> Char -> US.Coin c +toCoin cs tn = US.Coin $ AssetClass (cs, fromString [tn]) + +toCreateParams :: CurrencySymbol -> Integer -> Char -> Integer -> Char -> US.CreateParams +toCreateParams cs amtA tnA amtB tnB = US.CreateParams (toCoin cs tnA) (toCoin cs tnB) (US.Amount amtA) (US.Amount amtB) + +toAddParams :: CurrencySymbol -> Integer -> Char -> Integer -> Char -> US.AddParams +toAddParams cs amtA tnA amtB tnB = US.AddParams (toCoin cs tnA) (toCoin cs tnB) (US.Amount amtA) (US.Amount amtB) + +toRemoveParams :: CurrencySymbol -> Integer -> Char -> Char -> US.RemoveParams +toRemoveParams cs amt tnA tnB = US.RemoveParams (toCoin cs tnA) (toCoin cs tnB) (US.Amount amt) + +toCloseParams :: CurrencySymbol -> Char -> Char -> US.CloseParams +toCloseParams cs tnA tnB = US.CloseParams (toCoin cs tnA) (toCoin cs tnB) + +toSwapParams :: CurrencySymbol -> Integer -> Char -> Char -> US.SwapParams +toSwapParams cs amtA tnA tnB = US.SwapParams (toCoin cs tnA) (toCoin cs tnB) (US.Amount amtA) (US.Amount 0) + +showCoinHeader :: IO () +showCoinHeader = printf "\n currency symbol token name amount\n\n" + +showCoin :: CurrencySymbol -> TokenName -> Integer -> IO () +showCoin cs tn = printf "%64s %66s %15d\n" (show cs) (show tn) + +getFunds :: UUID -> IO () +getFunds cid = do + callEndpoint cid "funds" () + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right (US.Funds v) -> showFunds v + _ -> go + + showFunds :: Value -> IO () + showFunds v = do + showCoinHeader + forM_ (flattenValue v) $ \(cs, tn, amt) -> showCoin cs tn amt + printf "\n" + +getPools :: UUID -> IO () +getPools cid = do + callEndpoint cid "pools" () + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right (US.Pools ps) -> showPools ps + _ -> go + + showPools :: [((US.Coin US.A, US.Amount US.A), (US.Coin US.B, US.Amount US.B))] -> IO () + showPools ps = do + forM_ ps $ \((US.Coin (AssetClass (csA, tnA)), amtA), (US.Coin (AssetClass (csB, tnB)), amtB)) -> do + showCoinHeader + showCoin csA tnA (US.unAmount amtA) + showCoin csB tnB (US.unAmount amtB) + +createPool :: UUID -> US.CreateParams -> IO () +createPool cid cp = do + callEndpoint cid "create" cp + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right US.Created -> putStrLn "created" + Left err' -> putStrLn $ "error: " ++ show err' + _ -> go + +addLiquidity :: UUID -> US.AddParams -> IO () +addLiquidity cid ap = do + callEndpoint cid "add" ap + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right US.Added -> putStrLn "added" + Left err' -> putStrLn $ "error: " ++ show err' + _ -> go + +removeLiquidity :: UUID -> US.RemoveParams -> IO () +removeLiquidity cid rp = do + callEndpoint cid "remove" rp + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right US.Removed -> putStrLn "removed" + Left err' -> putStrLn $ "error: " ++ show err' + _ -> go + +closePool :: UUID -> US.CloseParams -> IO () +closePool cid cp = do + callEndpoint cid "close" cp + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right US.Closed -> putStrLn "closed" + Left err' -> putStrLn $ "error: " ++ show err' + _ -> go + +swap :: UUID -> US.SwapParams -> IO () +swap cid sp = do + callEndpoint cid "swap" sp + threadDelay 2_000_000 + go + where + go = do + e <- getStatus cid + case e of + Right US.Swapped -> putStrLn "swapped" + Left err' -> putStrLn $ "error: " ++ show err' + _ -> go + +getStatus :: UUID -> IO (Either Text US.UserContractState) +getStatus cid = runReq defaultHttpConfig $ do + w <- req + GET + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show cid) /: "status") + NoReqBody + (Proxy :: Proxy (JsonResponse (ContractInstanceClientState UniswapContracts))) + (port 8080) + case fromJSON $ observableState $ cicCurrentState $ responseBody w of + Success (Last Nothing) -> liftIO $ threadDelay 1_000_000 >> getStatus cid + Success (Last (Just e)) -> return e + _ -> liftIO $ ioError $ userError "error decoding state" + +callEndpoint :: ToJSON a => UUID -> String -> a -> IO () +callEndpoint cid name a = handle h $ runReq defaultHttpConfig $ do + liftIO $ printf "\npost request to 127.0.1:8080/api/new/contract/instance/%s/endpoint/%s\n" (show cid) name + liftIO $ printf "request body: %s\n\n" $ B8.unpack $ encode a + v <- req + POST + (http "127.0.0.1" /: "api" /: "new" /: "contract" /: "instance" /: pack (show cid) /: "endpoint" /: pack name) + (ReqBodyJson a) + (Proxy :: Proxy (JsonResponse ())) + (port 8080) + when (responseStatusCode v /= 200) $ + liftIO $ ioError $ userError $ "error calling endpoint " ++ name + where + h :: HttpException -> IO () + h = ioError . userError . show diff --git a/code/week10/app/uniswap-pab.hs b/code/week10/app/uniswap-pab.hs new file mode 100644 index 0000000..5f80588 --- /dev/null +++ b/code/week10/app/uniswap-pab.hs @@ -0,0 +1,90 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +module Main + ( main + ) where + +import Control.Monad (forM_, void) +import Control.Monad.Freer (Eff, Member, interpret, type (~>)) +import Control.Monad.Freer.Error (Error) +import Control.Monad.Freer.Extras.Log (LogMsg) +import Control.Monad.IO.Class (MonadIO (..)) +import Data.Aeson (Result (..), encode, fromJSON) +import qualified Data.ByteString.Lazy as LB +import qualified Data.Monoid as Monoid +import qualified Data.Semigroup as Semigroup +import Data.Text (Text) +import Plutus.Contract +import qualified Plutus.Contracts.Currency as Currency +import qualified Plutus.Contracts.Uniswap as Uniswap +import Plutus.PAB.Effects.Contract (ContractEffect (..)) +import Plutus.PAB.Effects.Contract.Builtin (Builtin, SomeBuiltin (..), type (.\\)) +import qualified Plutus.PAB.Effects.Contract.Builtin as Builtin +import Plutus.PAB.Monitoring.PABLogMsg (PABMultiAgentMsg) +import Plutus.PAB.Simulator (SimulatorEffectHandlers, logString) +import qualified Plutus.PAB.Simulator as Simulator +import Plutus.PAB.Types (PABError (..)) +import qualified Plutus.PAB.Webserver.Server as PAB.Server +import Prelude hiding (init) +import Wallet.Emulator.Types (Wallet (..)) +import Wallet.Types (ContractInstanceId (..)) + +import Uniswap as US + +main :: IO () +main = void $ Simulator.runSimulationWith handlers $ do + logString @(Builtin UniswapContracts) "Starting Uniswap PAB webserver on port 8080. Press enter to exit." + shutdown <- PAB.Server.startServerDebug + + cidInit <- Simulator.activateContract (Wallet 1) Init + cs <- flip Simulator.waitForState cidInit $ \json -> case fromJSON json of + Success (Just (Semigroup.Last cur)) -> Just $ Currency.currencySymbol cur + _ -> Nothing + _ <- Simulator.waitUntilFinished cidInit + + liftIO $ LB.writeFile "symbol.json" $ encode cs + logString @(Builtin UniswapContracts) $ "Initialization finished. Minted: " ++ show cs + + cidStart <- Simulator.activateContract (Wallet 1) UniswapStart + us <- flip Simulator.waitForState cidStart $ \json -> case (fromJSON json :: Result (Monoid.Last (Either Text Uniswap.Uniswap))) of + Success (Monoid.Last (Just (Right us))) -> Just us + _ -> Nothing + logString @(Builtin UniswapContracts) $ "Uniswap instance created: " ++ show us + + forM_ wallets $ \w -> do + cid <- Simulator.activateContract w $ UniswapUser us + liftIO $ writeFile (cidFile w) $ show $ unContractInstanceId cid + logString @(Builtin UniswapContracts) $ "Uniswap user contract started for " ++ show w + + void $ liftIO getLine + + shutdown + +handleUniswapContract :: + ( Member (Error PABError) effs + , Member (LogMsg (PABMultiAgentMsg (Builtin UniswapContracts))) effs + ) + => ContractEffect (Builtin UniswapContracts) + ~> Eff effs +handleUniswapContract = Builtin.handleBuiltin getSchema getContract where + getSchema = \case + UniswapUser _ -> Builtin.endpointsToSchemas @(Uniswap.UniswapUserSchema .\\ BlockchainActions) + UniswapStart -> Builtin.endpointsToSchemas @(Uniswap.UniswapOwnerSchema .\\ BlockchainActions) + Init -> Builtin.endpointsToSchemas @Empty + getContract = \case + UniswapUser us -> SomeBuiltin $ Uniswap.userEndpoints us + UniswapStart -> SomeBuiltin Uniswap.ownerEndpoint + Init -> SomeBuiltin US.initContract + +handlers :: SimulatorEffectHandlers (Builtin UniswapContracts) +handlers = + Simulator.mkSimulatorHandlers @(Builtin UniswapContracts) [] + $ interpret handleUniswapContract diff --git a/code/week10/cabal.project b/code/week10/cabal.project new file mode 100644 index 0000000..9239082 --- /dev/null +++ b/code/week10/cabal.project @@ -0,0 +1,151 @@ +index-state: 2021-04-13T00:00:00Z + +packages: ./. + +-- You never, ever, want this. +write-ghc-environment-files: never + +-- Always build tests and benchmarks. +tests: true +benchmarks: true + +source-repository-package + type: git + location: https://github.com/input-output-hk/plutus.git + subdir: + freer-extras + playground-common + plutus-core + plutus-contract + plutus-ledger + plutus-ledger-api + plutus-pab + plutus-tx + plutus-tx-plugin + plutus-use-cases + prettyprinter-configurable + quickcheck-dynamic + word-array + tag: 26449c6e6e1c14d335683e5a4f40e2662b9b7e7 + +-- The following sections are copied from the 'plutus' repository cabal.project at the revision +-- given above. +-- This is necessary because the 'plutus' libraries depend on a number of other libraries which are +-- not on Hackage, and so need to be pulled in as `source-repository-package`s themselves. Make sure to +-- re-update this section from the template when you do an upgrade. + +-- This is also needed so evenful-sql-common will build with a +-- newer version of persistent. See stack.yaml for the mirrored +-- configuration. +package eventful-sql-common + ghc-options: -XDerivingStrategies -XStandaloneDeriving -XUndecidableInstances -XDataKinds -XFlexibleInstances + +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 + , size-based:template-haskell + + -- The following two dependencies are needed by plutus. + , eventful-sql-common:persistent + , eventful-sql-common:persistent-template + +constraints: + -- aws-lambda-haskell-runtime-wai doesn't compile with newer versions + aws-lambda-haskell-runtime <= 3.0.3 + -- big breaking change here, inline-r doens't have an upper bound + , singletons < 3.0 + -- breaks eventful even more than it already was + , persistent-template < 2.12 + +-- See the note on nix/pkgs/default.nix:agdaPackages for why this is here. +-- (NOTE this will change to ieee754 in newer versions of nixpkgs). +extra-packages: ieee, filemanip + +-- Drops an instance breaking our code. Should be released to Hackage eventually. +source-repository-package + type: git + location: https://github.com/Quid2/flat.git + tag: 95e5d7488451e43062ca84d5376b3adcc465f1cd + +-- Needs some patches, but upstream seems to be fairly dead (no activity in > 1 year) +source-repository-package + type: git + location: https://github.com/shmish111/purescript-bridge.git + tag: 6a92d7853ea514be8b70bab5e72077bf5a510596 + +source-repository-package + type: git + location: https://github.com/shmish111/servant-purescript.git + tag: a76104490499aa72d40c2790d10e9383e0dbde63 + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-crypto.git + tag: f73079303f663e028288f9f4a9e08bcca39a923e + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-base + tag: 4251c0bb6e4f443f00231d28f5f70d42876da055 + subdir: + binary + binary/test + slotting + cardano-crypto-class + cardano-crypto-praos + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-prelude + tag: ee4e7b547a991876e6b05ba542f4e62909f4a571 + subdir: + cardano-prelude + cardano-prelude-test + +source-repository-package + type: git + location: https://github.com/input-output-hk/ouroboros-network + tag: 6cb9052bde39472a0555d19ade8a42da63d3e904 + subdir: + typed-protocols + typed-protocols-examples + ouroboros-network + ouroboros-network-testing + ouroboros-network-framework + io-sim + io-sim-classes + network-mux + Win32-network + +source-repository-package + type: git + location: https://github.com/input-output-hk/iohk-monitoring-framework + tag: a89c38ed5825ba17ca79fddb85651007753d699d + subdir: + iohk-monitoring + tracer-transformers + contra-tracer + plugins/backend-ekg + +source-repository-package + type: git + location: https://github.com/input-output-hk/cardano-ledger-specs + tag: 097890495cbb0e8b62106bcd090a5721c3f4b36f + subdir: + byron/chain/executable-spec + byron/crypto + byron/crypto/test + byron/ledger/executable-spec + byron/ledger/impl + byron/ledger/impl/test + semantics/executable-spec + semantics/small-steps-test + shelley/chain-and-ledger/dependencies/non-integer + shelley/chain-and-ledger/executable-spec + shelley-ma/impl + +source-repository-package + type: git + location: https://github.com/input-output-hk/goblins + tag: cde90a2b27f79187ca8310b6549331e59595e7ba diff --git a/code/week10/close.sh b/code/week10/close.sh new file mode 100755 index 0000000..00666e8 --- /dev/null +++ b/code/week10/close.sh @@ -0,0 +1,9 @@ +#1/bin/sh + +symbol=$( cat symbol.json ) +body="{\"clpCoinB\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$3\"}]},\"clpCoinA\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$2\"}]}}" +echo $body + +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/close" \ +--header 'Content-Type: application/json' \ +--data-raw $body diff --git a/code/week10/create.sh b/code/week10/create.sh new file mode 100755 index 0000000..ef6c026 --- /dev/null +++ b/code/week10/create.sh @@ -0,0 +1,9 @@ +#1/bin/sh + +symbol=$( cat symbol.json ) +body="{\"cpAmountA\":$2,\"cpAmountB\":$4,\"cpCoinB\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$5\"}]},\"cpCoinA\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$3\"}]}}" +echo $body + +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/create" \ +--header 'Content-Type: application/json' \ +--data-raw $body diff --git a/code/week10/funds.sh b/code/week10/funds.sh new file mode 100755 index 0000000..5edd5b1 --- /dev/null +++ b/code/week10/funds.sh @@ -0,0 +1,4 @@ +#1/bin/sh +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/funds" \ +--header 'Content-Type: application/json' \ +--data-raw '[]' diff --git a/code/week10/hie.yaml b/code/week10/hie.yaml new file mode 100644 index 0000000..e7e5566 --- /dev/null +++ b/code/week10/hie.yaml @@ -0,0 +1,6 @@ +cradle: + cabal: + - path: "./app/uniswap-pab.hs" + component: "exe:uniswap-pab" + - path: "./app/uniswap-client.hs" + component: "exe:uniswap-client" diff --git a/code/week10/logs.sh b/code/week10/logs.sh new file mode 100755 index 0000000..1bbeb4f --- /dev/null +++ b/code/week10/logs.sh @@ -0,0 +1,2 @@ +#1/bin/sh +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/status" | jq ".cicCurrentState.logs" diff --git a/code/week10/plutus-pioneer-program-week10.cabal b/code/week10/plutus-pioneer-program-week10.cabal new file mode 100644 index 0000000..a554631 --- /dev/null +++ b/code/week10/plutus-pioneer-program-week10.cabal @@ -0,0 +1,72 @@ +Cabal-Version: 2.4 +Name: plutus-pioneer-program-week10 +Version: 0.1.0.0 +Author: Lars Bruenjes +Maintainer: lars.bruenjes@iohk.io +Build-Type: Simple +Copyright: © 2021 Lars Bruenjes +License: Apache-2.0 +License-files: LICENSE + +source-repository head + type: git + location: https://github.com/input-output-hk/plutus-pioneer-program + +flag defer-plugin-errors + description: + Defer errors from the plugin, useful for things like Haddock that can't handle it. + default: False + manual: True + +common lang + default-language: Haskell2010 + default-extensions: ExplicitForAll ScopedTypeVariables + DeriveGeneric StandaloneDeriving DeriveLift + GeneralizedNewtypeDeriving DeriveFunctor DeriveFoldable + DeriveTraversable + ghc-options: -Wall -Wnoncanonical-monad-instances + -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wredundant-constraints -Widentities + -- See Plutus Tx readme + -fobject-code -fno-ignore-interface-pragmas -fno-omit-interface-pragmas + if flag(defer-plugin-errors) + ghc-options: -fplugin-opt PlutusTx.Plugin:defer-errors + +executable uniswap-pab + main-is: uniswap-pab.hs + other-modules: Uniswap + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Wcompat + -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wno-missing-import-lists -Wredundant-constraints -O0 + build-depends: + base >=4.9 && <5, + aeson -any, + bytestring -any, + containers -any, + freer-extras -any, + freer-simple -any, + plutus-contract -any, + plutus-ledger -any, + plutus-pab, + plutus-use-cases -any, + prettyprinter -any, + text -any + +executable uniswap-client + main-is: uniswap-client.hs + other-modules: Uniswap + hs-source-dirs: app + ghc-options: -Wall + build-depends: aeson + , base ^>= 4.14.1.0 + , bytestring + , plutus-contract + , plutus-ledger + , plutus-pab + , plutus-use-cases + , prettyprinter + , req ^>= 3.9.0 + , text + , uuid diff --git a/code/week10/pools.sh b/code/week10/pools.sh new file mode 100755 index 0000000..00401b6 --- /dev/null +++ b/code/week10/pools.sh @@ -0,0 +1,4 @@ +#1/bin/sh +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/pools" \ +--header 'Content-Type: application/json' \ +--data-raw '[]' diff --git a/code/week10/remove.sh b/code/week10/remove.sh new file mode 100755 index 0000000..4e130a5 --- /dev/null +++ b/code/week10/remove.sh @@ -0,0 +1,9 @@ +#1/bin/sh + +symbol=$( cat symbol.json ) +body="{\"rpCoinB\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$4\"}]},\"rpDiff\":$2,\"rpCoinA\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$3\"}]}}" +echo $body + +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/remove" \ +--header 'Content-Type: application/json' \ +--data-raw $body diff --git a/code/week10/status.sh b/code/week10/status.sh new file mode 100755 index 0000000..3cef099 --- /dev/null +++ b/code/week10/status.sh @@ -0,0 +1,2 @@ +#1/bin/sh +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/status" | jq ".cicCurrentState.observableState" diff --git a/code/week10/swap.sh b/code/week10/swap.sh new file mode 100755 index 0000000..b6c229e --- /dev/null +++ b/code/week10/swap.sh @@ -0,0 +1,9 @@ +#1/bin/sh + +symbol=$( cat symbol.json ) +body="{\"spAmountA\":$2,\"spAmountB\":0,\"spCoinB\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$4\"}]},\"spCoinA\":{\"unAssetClass\":[$symbol,{\"unTokenName\":\"$3\"}]}}" +echo $body + +curl "http://localhost:8080/api/new/contract/instance/$(cat W$1.cid)/endpoint/swap" \ +--header 'Content-Type: application/json' \ +--data-raw $body