packages feed

cleveland-0.1.1: lorentz-test/Test/Lorentz/Entrypoints.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

-- | Tests for Lorentz compilation which uses 'LorentzCompilationWay'.
module Test.Lorentz.Entrypoints
  ( test_FieldEntrypoints
  , test_TypeEntrypoints
  , test_RootEntrypoints
  , test_Entrypoints_lookup
  , test_Contract_call
  , test_Self_call
  ) where


import Fcf (Eval)
import Test.HUnit ((@?=))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase)
import Test.Tasty.TypeSpec (typeTest)
import Test.TypeSpec (Is, TypeSpec(..))

import Lorentz ((#), (/->))
import Lorentz qualified as L
import Lorentz.Annotation
import Lorentz.Constraints
import Lorentz.Entrypoints
import Lorentz.Run
import Lorentz.Value
import Morley.Michelson.Typed hiding (Contract)
import Morley.Michelson.Untyped (RootAnn, noAnn)
import Morley.Util.Named
import Test.Cleveland

import Test.Util.Annotation
import Test.Util.TypeSpec (ExactlyIs)

----------------------------------------------------------------------------
-- Entrypoints declarations
----------------------------------------------------------------------------

data MyEntrypoints1
  = Do1 Integer
  | Do2 (Integer, Integer)
  | Do3 MyEntrypoints2
  | Do4 MyParams
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints1a
  = Do1a Integer
  | Do2a (Integer, Integer)
  | Do3a MyEntrypoints2
  | Do4a MyParams
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints2
  = Do10
  | Do11 Natural
  deriving stock Generic
  deriving anyclass (IsoValue, HasAnnotation)

data MyEntrypoints3
  = Do12 ("tuplearg" :! ("TL" :! Integer, "TR" :!  Natural), "boolarg" :! Bool)
  | Do13 ("integerarg" :! Integer, "boolarg" :! Bool)
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints4
  = Do14 ("viewarg1" :! L.View_ ("owner" :! L.Address) Natural)
  | Do15 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints5
  = Do16 ("maybearg" :! Maybe ("maybeinner" :! Natural))
  | Do17 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints6
  = Do18 ("lambdaarg" :! L.Lambda Natural Natural)
  | Do19 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints7
  = Do20 ("listarg" :! [("balance" :! Natural , "address" :! L.Address)])
  | Do21 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints8
  = Do22 ("maparg" :! (Map Natural ("balance" :! Natural , "address" :! L.Address)))
  | Do23 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints9
  = Do24 ("maybearg" :? ("maybeinner" :! Natural))
  | Do25 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints10
  = Do26 ("bigmaparg" :! L.Lambda (BigMap Natural ("balance" :! Natural , "address" :! L.Address)) ())
  | Do27 ()
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypoints11
  = Do28 ("kek" :! Natural, "pek" :! Integer)
  | Do29
  deriving stock Generic
  deriving anyclass (IsoValue, HasAnnotation)

data MyEntrypointsWithDef
  = Default Integer
  | NonDefault Natural
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypointsWithRoot
  = Dor1
  | Dor2 Natural
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypointsWithCustomAnn
  = Doc1 Natural
  | Doc2 CustomProd
  deriving stock Generic
  deriving anyclass (IsoValue)

data CustomProd = CustomProd
  { cp1 :: Natural
  , cp2 :: CustomSum
  }
  deriving stock Generic
  deriving anyclass (IsoValue)

instance HasAnnotation CustomProd where
  annOptions = defaultAnnOptions {fieldAnnModifier = \a -> "prod_" <> a}

data CustomSum = Cs1 | Cs2
  deriving stock Generic
  deriving anyclass (IsoValue)

instance HasAnnotation CustomSum where
  annOptions = defaultAnnOptions {fieldAnnModifier = \a -> "sum_" <> a}

data MyParams = MyParams
  { param1 :: ()
  , param2 :: ByteString
  }
  deriving stock Generic
  deriving anyclass (IsoValue, HasAnnotation)

-- Normally this cannot declare entrypoints because this is not a sum type.
-- But we will declare them forcibly
data MySingleEntrypoint = Dos1 Integer
  deriving stock Generic
  deriving anyclass (IsoValue, HasAnnotation)

data MyEntrypointsDelegated
  = Dod1
  | Dod2 MyEntrypointsSubDelegated
  deriving stock Generic
  deriving anyclass (IsoValue)

data MyEntrypointsSubDelegated
  = Dosd1
  | Dosd2
  deriving stock Generic
  deriving anyclass (IsoValue, HasAnnotation)

instance ParameterHasEntrypoints MyEntrypoints1 where
  type ParameterEntrypointsDerivation MyEntrypoints1 = EpdRecursive

instance ParameterHasEntrypoints MyEntrypoints1a where
  type ParameterEntrypointsDerivation MyEntrypoints1a = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints2 where
  type ParameterEntrypointsDerivation MyEntrypoints2 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints3 where
  type ParameterEntrypointsDerivation MyEntrypoints3 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints4 where
  type ParameterEntrypointsDerivation MyEntrypoints4 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints5 where
  type ParameterEntrypointsDerivation MyEntrypoints5 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints6 where
  type ParameterEntrypointsDerivation MyEntrypoints6 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints7 where
  type ParameterEntrypointsDerivation MyEntrypoints7 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints8 where
  type ParameterEntrypointsDerivation MyEntrypoints8 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints9 where
  type ParameterEntrypointsDerivation MyEntrypoints9 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints10 where
  type ParameterEntrypointsDerivation MyEntrypoints10 = EpdPlain

instance ParameterHasEntrypoints MyEntrypoints11 where
  type ParameterEntrypointsDerivation MyEntrypoints11 = EpdNone

instance ParameterHasEntrypoints MyEntrypointsWithDef where
  type ParameterEntrypointsDerivation MyEntrypointsWithDef = EpdPlain

instance ParameterHasEntrypoints (ShouldHaveEntrypoints MySingleEntrypoint) where
  type ParameterEntrypointsDerivation (ShouldHaveEntrypoints MySingleEntrypoint) = EpdPlain

instance ParameterHasEntrypoints MyEntrypointsDelegated where
  type ParameterEntrypointsDerivation MyEntrypointsDelegated = EpdDelegate

instance ParameterHasEntrypoints MyEntrypointsSubDelegated where
  type ParameterEntrypointsDerivation MyEntrypointsSubDelegated = EpdNone

instance ParameterHasEntrypoints MyEntrypointsWithRoot where
  type ParameterEntrypointsDerivation MyEntrypointsWithRoot = EpdWithRoot "root" EpdPlain

instance ParameterHasEntrypoints MyEntrypointsWithCustomAnn where
  type ParameterEntrypointsDerivation MyEntrypointsWithCustomAnn = EpdPlain

dummyContract :: NiceParameterFull param => Contract param () ()
dummyContract = L.defaultContract $
  L.drop L.# L.unit L.# L.nil L.# L.pair

test_FieldEntrypoints :: [TestTree]
test_FieldEntrypoints =
  [ testCase "Simple parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints2)
      @?=
      FANodeOr
        "do10" FALeaf
        "do11" FALeaf

  , testGroup "Complex parameter"
    [ testCase "Interpreting as direct list of entrypoints" $
        (paramAnnTree $ dummyContract @MyEntrypoints1a)
        @?=
        FANodeOr
          noAnn (FANodeOr "do1a" FALeaf "do2a" FALeaf)
          noAnn
            (FANodeOr
              "do3a"
                (FANodeOr noAnn FALeaf noAnn FALeaf)
              "do4a"
                (FANodePair "param1" FALeaf "param2" FALeaf))

    , testCase "Recursive entrypoints traversal" $
        (paramAnnTree $ dummyContract @MyEntrypoints1)
        @?=
        FANodeOr
          noAnn (FANodeOr "do1" FALeaf "do2" FALeaf)
          noAnn (FANodeOr
                  noAnn
                    (FANodeOr "do10" FALeaf "do11" FALeaf)
                  "do4"
                    (FANodePair
                      "param1" FALeaf
                      "param2" FALeaf)
                )

    , testCase "Delegating entrypoints traversal" $
        (paramAnnTree $ dummyContract @MyEntrypointsDelegated)
        @?=
        FANodeOr "dod1" FALeaf
             "dod2" (FANodeOr noAnn FALeaf noAnn FALeaf)
    , testCase "Parameter with custom fieldAnnotation" $
        (paramAnnTree $ dummyContract @MyEntrypointsWithCustomAnn)
        @?=
        FANodeOr
          "doc1" FALeaf
          "doc2"
          (FANodePair
            "prod_cp1" FALeaf
            "prod_cp2"
            (FANodeOr
              "sum_cs1" FALeaf
              "sum_cs2" FALeaf
            )
          )
    ]
  ]
  where
    paramAnnTree :: Contract cp st () -> FieldAnnTree (ToT cp)
    paramAnnTree = extractFieldAnnTree . pnNotes . cParamNotes . L.toMichelsonContract

test_TypeEntrypoints :: [TestTree]
test_TypeEntrypoints =
  [ testCase "Named field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints3)
      @?=
      (TANodeOr noAnn
       (TANodePair noAnn (TANodePair "tuplearg" (TALeaf "TL") (TALeaf "TR")) (TALeaf "boolarg"))
       (TANodePair noAnn (TALeaf "integerarg") (TALeaf "boolarg"))
      )
  , testCase "Named field parameter for views" $
      (paramAnnTree $ dummyContract @MyEntrypoints4)
      @?=
      (TANodeOr noAnn
        (TANodePair "viewarg1" (TALeaf "owner") (TALeaf noAnn)) (TALeaf noAnn))

  , testCase "Maybe field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints5)
      @?=
      (TANodeOr noAnn
        (TANodeOption "maybearg" (TALeaf "maybeinner")) (TALeaf noAnn))

  , testCase "Lambda field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints6)
      @?=
      (TANodeOr noAnn
        (TANodeLambda "lambdaarg" (TALeaf noAnn) (TALeaf noAnn)) (TALeaf noAnn))

  , testCase "List field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints7)
      @?=
      (TANodeOr noAnn
       (TANodeList "listarg" (TANodePair noAnn (TALeaf "balance") (TALeaf "address"))) (TALeaf noAnn))

  , testCase "Map field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints8)
      @?=
      (TANodeOr noAnn
        (TANodeMap
          "maparg"
          (TALeaf noAnn)
          (TANodePair noAnn (TALeaf "balance") (TALeaf "address"))
        )
        (TALeaf noAnn)
      )

  , testCase "Maybe field parameter 2" $
      (paramAnnTree $ dummyContract @MyEntrypoints9)
      @?=
      (TANodeOr noAnn
        (TANodeOption "maybearg" (TALeaf "maybeinner")) (TALeaf noAnn))

  , testCase "Big map field parameter" $
      (paramAnnTree $ dummyContract @MyEntrypoints10)
      @?=
      (TANodeOr noAnn
        (TANodeLambda "bigmaparg"
          (TANodeBigMap
            noAnn
            (TALeaf noAnn)
            (TANodePair noAnn (TALeaf "balance") (TALeaf "address"))
          )
          (TALeaf noAnn)
        )
        (TALeaf noAnn)
      )

  , testCase "Newtype" $
      (paramAnnTree $ dummyContract @(ShouldHaveEntrypoints MySingleEntrypoint))
      @?=
      TALeaf noAnn

  , testGroup "Primitive type parameter"
      -- Parameters used in these test cases should not require any instances
      [ testCase "Address" $
          (paramAnnTree $ dummyContract @Address)
          @?=
          TALeaf noAnn
      , testCase "Void" $
          (paramAnnTree $ dummyContract @(L.Void_ Integer Natural))
          @?=
          TANodePair noAnn (TALeaf noAnn) (TANodeLambda noAnn (TALeaf noAnn) (TALeaf noAnn))
      ]
  , testCase "EpdNone case (type annotations are preserved)" $
      (paramAnnTree $ dummyContract @MyEntrypoints11)
      @?=
      (TANodeOr noAnn
        (TANodePair noAnn (TALeaf "kek") (TALeaf "pek"))
        (TALeaf noAnn)
      )
  ]
  where
    paramAnnTree :: Contract cp st () -> TypeAnnTree (ToT cp)
    paramAnnTree = extractTypeAnnTree . pnNotes . cParamNotes . L.toMichelsonContract

test_RootEntrypoints :: [TestTree]
test_RootEntrypoints =
  [ testCase "No root entrypoint" $
      (rootAnn $ dummyContract @MyEntrypoints1)
      @?=
      noAnn

  , testCase "With root entrypoint" $
      (rootAnn $ dummyContract @MyEntrypointsWithRoot)
      @?=
      "root"
  ]
  where
    rootAnn :: Contract cp st () -> RootAnn
    rootAnn = pnRootAnn . cParamNotes . L.toMichelsonContract

----------------------------------------------------------------------------
-- @contract@ instruction
----------------------------------------------------------------------------

test_Entrypoints_lookup :: [TestTree]
test_Entrypoints_lookup =
  [ testGroup "Flat parameter type"
    [ typeTest "Default entrypoint arg" $ Valid @
        (GetDefaultEntrypointArg MyEntrypoints1a `Is` MyEntrypoints1a)
    , typeTest "Can get entrypoint on surface" $ Valid @
        (GetEntrypointArg MyEntrypoints1a "Do1a" `Is` Integer)
    , typeTest "Cannot get entrypoint in deep" $ Valid @
        (Eval (LookupParameterEntrypoint MyEntrypoints1a "Do11")
           `ExactlyIs` 'Nothing
        )
    ]
  , testGroup "Nested parameter type"
    [ typeTest "Default entrypoint arg" $ Valid @
        (GetDefaultEntrypointArg MyEntrypoints1 `Is` MyEntrypoints1)
    , typeTest "Can get entrypoint on surface" $ Valid @
        (GetEntrypointArg MyEntrypoints1 "Do1" `Is` Integer)
    , typeTest "Can get entrypoint in deep" $ Valid @
        (GetEntrypointArg MyEntrypoints1 "Do11" `Is` Natural)
    , typeTest "Can get entrypoint without arg" $ Valid @
        (GetEntrypointArg MyEntrypoints1 "Do10" `Is` ())
    ]
  , testGroup "Parameter type with default entrypoint"
    [ typeTest "Default entrypoint arg" $ Valid @
        (GetDefaultEntrypointArg MyEntrypointsWithDef `Is` Integer)
    , typeTest "Can get non-default entrypoint" $ Valid @
        (GetEntrypointArg MyEntrypointsWithDef "NonDefault" `Is` Natural)
    ]
  , testGroup "Delegation"
    [ typeTest "Calling immediate entrypoint works" $ Valid @
        (GetEntrypointArg MyEntrypointsDelegated "Dod1" `Is` ())
    ]
  ]

-- | A contract which accepts 'Address' as parameter and calls specific
-- entrypoint of another contract.
callerContract
  :: forall cp mname arg.
     ( arg ~ GetEntrypointArgCustom cp mname
     , NiceConstant arg, NiceParameter arg, NiceParameterFull cp
     )
  => EntrypointRef mname
  -> arg
  -> Contract (TAddress cp ()) () ()
callerContract epRef argument = L.defaultContract $
  L.car # L.contractCalling epRef #
  L.assertSome ("Contract lookup failed" :: MText) #
  L.push 1 # L.push argument # L.transferTokens #
  L.dip (L.unit # L.nil) # L.cons # L.pair

test_Contract_call :: [TestTree]
test_Contract_call =
  [ testScenario "Calling entrypoint" $ scenario do
      let myCallerContract = callerContract (Call @"Do11") 5
      let myTargetContract = L.defaultContract $ L.car # L.caseT @MyEntrypoints2
            ( #cDo10 /-> L.push 0
            , #cDo11 /-> L.nop
            ) # L.nil # L.pair

      caller <- originate $ OriginateData "Caller" 10 () myCallerContract
      target <- originateSimple "Call target" def myTargetContract
      call caller CallDefault (toTAddress target)
      getStorage target @@== 5

  , testScenario "Calling default entrypoint" $ scenario do
      let myCallerContract = callerContract CallDefault 3
      let myTargetContract = L.defaultContract $ L.car # L.caseT @MyEntrypointsWithDef
            ( #cDefault /-> L.nop
            , #cNonDefault /-> L.neg
            ) # L.nil # L.pair

      caller <- originate$ OriginateData "Caller" 10 () myCallerContract
      target <- originateSimple "Call target" def myTargetContract
      call caller CallDefault (toTAddress target)
      getStorage target @@== 3

  , testScenario "Calling root entrypoint" $ scenario do
      let myCallerContract = callerContract (Call @"root") (Dor2 5)
      let myTargetContract = L.defaultContract $ L.car # L.caseT @MyEntrypointsWithRoot
            ( #cDor1 /-> L.push 0
            , #cDor2 /-> L.nop
            ) # L.nil # L.pair

      caller <- originate $ OriginateData "Caller" 10 () myCallerContract
      target <- originateSimple "Call target" def myTargetContract
      call caller CallDefault (toTAddress target)
      getStorage target @@== 5
  ]

test_Self_call :: [TestTree]
test_Self_call =
   [ testScenario "Calling entrypoint" $ scenario do
      let myContract = mkContract $ L.car # L.caseT @MyEntrypoints2
            ( #cDo10 /->
                L.selfCalling @MyEntrypoints2 (Call @"Do11") #
                L.push 1 # L.push 5 # L.transferTokens #
                L.dip (L.push @Integer 1 # L.nil) # L.cons # L.pair
            , #cDo11 /-> L.push @Integer 10 # L.add # L.nil # L.pair
            )

      contractRef <- originate $ OriginateData "Contract" 10 0 myContract
      call contractRef CallDefault Do10
      getStorage contractRef @@== 15

  , testScenario "Calling root entrypoint" $ scenario do
      let myContract = L.defaultContract $ L.car # L.caseT @MyEntrypointsWithRoot
            ( #cDor1 /->
                L.selfCalling @MyEntrypointsWithRoot (Call @"root") #
                L.push 1 # L.push (Dor2 5) # L.transferTokens #
                L.dip (L.push @Integer 1 # L.nil) # L.cons # L.pair
            , #cDor2 /->
                L.push @Integer 10 # L.add # L.nil # L.pair
            )
      contractRef <- originate $ OriginateData "Contract" 10 0 myContract
      call contractRef CallDefault Dor1
      getStorage contractRef @@== 15
  ]