packages feed

lorentz-0.2.0: test/Test/Lorentz/EntryPoints.hs

{-# LANGUAGE DeriveAnyClass #-}

-- | Tests for Lorentz compilation which uses 'LorentzCompilationWay'.
module Test.Lorentz.EntryPoints
  ( test_FieldAnnotations
  , test_TypeAnnotations
  , test_Entrypoints_lookup
  , test_Contract_call
  , test_Self_call
  ) where

import Fcf (Eval)
import Unsafe.Coerce (unsafeCoerce)
import Data.Constraint (Dict (..))
import Test.HUnit ((@?=))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase)
import Test.Tasty.TypeSpec (typeTest)
import Test.TypeSpec (Is, TypeSpec(..))
import Test.Util.TypeSpec (ExactlyIs)

import Lorentz ((:!), ( # ), (/->), HasTypeAnn)
import qualified Lorentz as L
import Lorentz.Constraints
import Lorentz.EntryPoints
import Lorentz.Run
import Lorentz.Test
import Lorentz.Value
import Michelson.Typed
import Michelson.Untyped (FieldAnn, TypeAnn, ann, noAnn)

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

data MyEntryPoints1
  = Do1 Integer
  | Do2 (Integer, Integer)
  | Do3 MyEntryPoints2
  | Do4 MyParams
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

data MyEntryPoints1a
  = Do1a Integer
  | Do2a (Integer, Integer)
  | Do3a MyEntryPoints2
  | Do4a MyParams
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

data MyEntryPoints2
  = Do10
  | Do11 Natural
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

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

data MyEntryPoints4
  = Do14 ("viewarg1" :! L.View ("owner" :! L.Address) Natural)
  | Do15 ()
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

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

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

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

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

data MyEntryPoints9
  = Do24 ("maybearg" L.:? ("maybeinner" :! Natural))
  | Do25 ()
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

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

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

data MyEntryPointsWithDef
  = Default Integer
  | NonDefault Natural
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

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

-- 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, HasTypeAnn)

data MyEntryPointsDelegated
  = Dod1
  | Dod2 MyEntryPointsSubDelegated
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

data MyEntryPointsSubDelegated
  = Dosd1
  | Dosd2
  deriving stock Generic
  deriving anyclass (IsoValue, HasTypeAnn)

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

dummyContract :: L.ContractCode param ()
dummyContract = L.drop L.# L.unit L.# L.nil L.# L.pair

-- | Helper datatype which contains field annotations from 'NTOr'.
data FieldAnnTree t where
  Leaf :: ForbidOr t => FieldAnnTree t
  Node :: FieldAnn -> FieldAnnTree a
       -> FieldAnn -> FieldAnnTree b
       -> FieldAnnTree ('TOr a b)

deriving stock instance Eq (FieldAnnTree t)
deriving stock instance Show (FieldAnnTree t)

data TypeAnnTree t where
  TALeaf :: TypeAnn -> TypeAnnTree t
  TANodeOption :: TypeAnn -> TypeAnnTree a -> TypeAnnTree ('TOption a)
  TANodePair :: TypeAnn -> TypeAnnTree a -> TypeAnnTree b -> TypeAnnTree ('TPair a b)
  TANodeOr :: TypeAnn -> TypeAnnTree a -> TypeAnnTree b -> TypeAnnTree ('TOr a b)
  TANodeLambda :: TypeAnn -> TypeAnnTree a -> TypeAnnTree b -> TypeAnnTree ('TLambda a b)
  TANodeList :: TypeAnn -> TypeAnnTree a -> TypeAnnTree ('TList a)
  TANodeMap :: TypeAnn -> TypeAnnTree b -> TypeAnnTree ('TMap a b)
  TANodeBigMap :: TypeAnn -> TypeAnnTree b -> TypeAnnTree ('TBigMap a b)

deriving stock instance Eq (TypeAnnTree t)
deriving stock instance Show (TypeAnnTree t)

extractTypeAnnTres :: Notes t -> TypeAnnTree t
extractTypeAnnTres = \case
  NTc ta -> TALeaf ta
  NTKey ta -> TALeaf ta
  NTUnit ta -> TALeaf ta
  NTSignature ta -> TALeaf ta
  NTOption ta n1 -> TANodeOption ta (extractTypeAnnTres n1)
  NTList ta n1 -> TANodeList ta (extractTypeAnnTres n1)
  NTSet ta _ -> TALeaf ta
  NTOperation ta -> TALeaf ta
  NTContract ta _ -> TALeaf ta
  NTPair ta _ _ n1 n2 -> TANodePair ta (extractTypeAnnTres n1) (extractTypeAnnTres n2)
  NTOr ta _ _ n1 n2 -> TANodeOr ta (extractTypeAnnTres n1) (extractTypeAnnTres n2)
  NTLambda ta n1 n2 -> TANodeLambda ta (extractTypeAnnTres n1) (extractTypeAnnTres n2)
  NTMap ta _ n1 -> TANodeMap ta (extractTypeAnnTres n1)
  NTBigMap ta _ n1 -> TANodeBigMap ta (extractTypeAnnTres n1)
  NTChainId ta -> TALeaf ta

extractFieldAnnTree :: Notes t -> FieldAnnTree t
extractFieldAnnTree = \case
  NTOr _ lann rann lnotes rnotes ->
    Node lann (extractFieldAnnTree lnotes) rann (extractFieldAnnTree rnotes)
  (_ :: Notes t) ->
    -- Here we know that type is not 'TOr',
    -- and tests don't require high-quality code
    case unsafeCoerce @(Dict ()) @(Dict (ForbidOr t)) Dict of
      Dict -> Leaf

test_FieldAnnotations :: [TestTree]
test_FieldAnnotations =
  [ testCase "Simple parameter" $
      (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPoints2))
      @?=
      Node
        (ann "do10") Leaf
        (ann "do11") Leaf

  , testGroup "Complex parameter"
    [ testCase "Interpreting as direct list of entrypoints" $
        (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPoints1a))
        @?=
        Node
          noAnn (Node (ann "do1a") Leaf (ann "do2a") Leaf)
          noAnn (Node (ann "do3a") (Node noAnn Leaf noAnn Leaf) (ann "do4a") Leaf)

    , testCase "Recursive entrypoints traversal" $
        (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPoints1))
        @?=
        Node
          noAnn (Node (ann "do1") Leaf (ann "do2") Leaf)
          noAnn (Node
                  noAnn (Node (ann "do10") Leaf (ann "do11") Leaf)
                  (ann "do4") Leaf
                )

    , testCase "Delegating entrypoints traversal" $
        (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPointsDelegated))
        @?=
        Node (ann "dod1") Leaf
             (ann "dod2") (Node noAnn Leaf noAnn Leaf)
    ]
  ]
  where
    paramAnnTree :: FullContract cp st -> FieldAnnTree cp
    paramAnnTree = extractFieldAnnTree . fcParamNotes

test_TypeAnnotations :: [TestTree]
test_TypeAnnotations =
  [ testCase "Named field parameter" $
      (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPoints3))
      @?=
      (TANodeOr noAnn
       (TANodePair noAnn (TANodePair (ann "tuplearg") (TALeaf (ann "TL")) (TALeaf (ann "TR"))) (TALeaf (ann "boolarg")))
       (TANodePair noAnn (TALeaf (ann "integerarg")) (TALeaf (ann "boolarg")))
      )
  , testCase "Named field parameter for views" $
      (paramAnnTree $ compileLorentzContract (dummyContract @MyEntryPoints4))
      @?=
      (TANodeOr noAnn
        (TANodePair (ann "viewarg1") (TALeaf (ann "owner")) (TALeaf noAnn)) (TALeaf noAnn))

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

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

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

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

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

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

  , testCase "Newtype" $
      (paramAnnTree $ compileLorentzContract (dummyContract @(ShouldHaveEntryPoints MySingleEntryPoint)))
      @?=
      TALeaf noAnn

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

----------------------------------------------------------------------------
-- @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
  -> L.ContractCode (TAddress cp) ()
callerContract epRef arg =
  L.car # L.contractCalling epRef #
  L.assertSome [mt|Contract lookup failed|] #
  L.push (toMutez 1) # L.push arg # L.transferTokens #
  L.dip (L.unit # L.nil) # L.cons # L.pair

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

        caller <- lOriginate myCallerContract "Caller" () (toMutez 10)
        target <- lOriginateEmpty myTargetContract "Call target"
        lCallDef caller target
        validate . Right $
          lExpectStorageConst target (5 :: Natural)

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

        caller <- lOriginate myCallerContract "Caller" () (toMutez 10)
        target <- lOriginateEmpty myTargetContract "Call target"
        lCallDef caller target
        validate . Right $
          lExpectStorageConst target (3 :: Natural)
  ]

test_Self_call :: [TestTree]
test_Self_call =
  [ testCase "Calling entrypoint" $
      integrationalTestExpectation $ do
        let myContract = L.car # L.caseT @MyEntryPoints2
              ( #cDo10 /->
                  L.selfCalling @MyEntryPoints2 (Call @"Do11") #
                  L.push (toMutez 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 <- lOriginate myContract "Contract" 0 (toMutez 10)
        lCallDef contractRef Do10
        validate . Right $
          lExpectStorageConst contractRef (15 :: Natural)
  ]