Setting up a project

In the previous chapters, we saw how to interact with the contract code that we wrote via the REPL.

Now, it's time to setup a proper Indigo project, which will allow us to split files in an organized way, add multiple contracts and give us many other useful commands to interact with them.

Note

Make sure that you have installed the Indigo CLI and gone through all the previous chapters proceeding into this.

Directory Structure

You can set up a new project, here called myproject, using:

indigo new myproject # Creating a boilerplate project
cd myproject
indigo build # Build the project
indigo run list # Run the command `list`

If everything goes well, you will see this in the output:

Available contracts: - Basic

We are going to use our previous chapter's contract as an example on how to organize the files structure.

{-# OPTIONS -Wno-orphans #-}

module Indigo.Tutorial.ContractDocumentation.FunctionsWithDocs
  ( myContract
  ) where

import Indigo

data Parameter
  = IsZero Integer
  | HasDigitOne Natural
  deriving stock (Generic, Show)
  deriving anyclass (IsoValue)

[entrypointDoc| Parameter plain |]

data Storage = Storage
  { sLastInput :: Natural
  , sTotal :: Natural
  }
  deriving stock (Generic, Show)
  deriving anyclass (IsoValue, HasAnnotation)

[typeDoc| Storage "Contract storage description."|]

myContract :: IndigoContract Parameter Storage
myContract param = defContract $ docGroup "My Contract" do
  contractGeneralDefault
  description
    "This documentation describes an example on how to functions and \
    \procedures of Indigo."
  doc $ dStorage @Storage
  entryCaseSimple param
    ( #cIsZero #= checkZero
    , #cHasDigitOne #= checkHasDigitOne
    )

checkZero
  :: forall tp.
     ( tp :~> Integer
     , HasStorage Storage
     )
  => IndigoEntrypoint tp
checkZero val = do
  description "Increment storage by 1 if the input is zero, otherwise fail."
  example (0 int)
  result <- new$ (val == 0 int)
  if result then
    incrementStorage
  else
    failCustom_ @() #isNotZeroError

checkHasDigitOne
  :: forall tp.
     ( tp :~> Natural
     , HasStorage Storage
     )
  => IndigoEntrypoint tp
checkHasDigitOne val = do
  description "Increment storage by 1 if the input has one digit."
  example (1 int)
  currentVal <- new$ val
  setStorageField @Storage #sLastInput currentVal
  base <- new$ 10 nat
  checkRes <- new$ False
  while (val > base && not checkRes) do
    currentVal =: currentVal / base
    remainder <- new$ val % base
    checkRes =: remainder == 1 nat
  updateStorage checkRes

updateStorage :: HasStorage Storage => Var Bool -> IndigoProcedure
updateStorage result = defFunction do
  if result then
    setStorageField @Storage #sTotal $ 0 nat
  else
    incrementStorage

incrementStorage :: HasStorage Storage => IndigoProcedure
incrementStorage = defFunction do
  currentTotal <- getStorageField  @Storage #sTotal
  setStorageField @Storage #sTotal (currentTotal + (1 nat))

-- | Custom error which can be used via: @failCustom_ #myError@
[errorDocArg| "isNotZeroError" exception "Fail when the input to `IsZero` entrypoint is not zero." ()|]

We are going to split this file into 3 parts: Doc.hs, Types.hs, and Impl.hs like below:

  ( contractDoc
  , checkZeroDoc
  , checkHasDigitOneDoc
  ) where

import Indigo
import Morley.Util.Markdown (md)

contractDoc :: Markdown
contractDoc = [md|
  This documentation describes an example on how to functions and
  procedures of Indigo.
  |]

checkZeroDoc :: Markdown
checkZeroDoc =
  "Increment storage by 1 if the input is zero, otherwise fail."

checkHasDigitOneDoc :: Markdown
checkHasDigitOneDoc =
  "Increment storage by 1 if the input has one digit."
{-# OPTIONS -Wno-orphans #-}

module MyContract.Types
  ( Parameter(..)
  , Storage(..)
  ) where

import Indigo

data Parameter
  = IsZero Integer
  | HasDigitOne Natural
  deriving stock (Generic, Show)
  deriving anyclass (IsoValue)

[entrypointDoc| Parameter plain |]

data Storage = Storage
  { sLastInput :: Natural
  , sTotal :: Natural
  }
  deriving stock (Generic, Show)
  deriving anyclass (IsoValue, HasAnnotation)

[typeDoc| Storage "Contract storage description."|]

-- | Custom error which can be used via: @failCustom_ #myError@
[errorDocArg| "isNotZeroError" exception "Fail when the input to `IsZero` entrypoint is not zero." ()|]
module MyContract.Impl
  ( myContractLorentz
  , emptyStorage
  ) where

import Indigo

import MyContract.Doc
import MyContract.Types

myContractLorentz :: ContractCode Parameter Storage
myContractLorentz = compileIndigoContract myContract

emptyStorage :: Storage
emptyStorage = Storage (0 nat) (0 nat)

myContract :: IndigoContract Parameter Storage
myContract param = defContract $ docGroup "My Contract" do
  contractGeneralDefault
  description contractDoc
  doc $ dStorage @Storage
  entryCaseSimple param
    ( #cIsZero #= checkZero
    , #cHasDigitOne #= checkHasDigitOne
    )

checkZero
  :: forall tp.
     ( tp :~> Integer
     , HasStorage Storage
     )
  => IndigoEntrypoint tp
checkZero val = do
  description checkZeroDoc
  example (0 int)
  result <- new$ (val == 0 int)
  if result then
    incrementStorage
  else
    failCustom_ @() #isNotZeroError

checkHasDigitOne
  :: forall tp.
     ( tp :~> Natural
     , HasStorage Storage
     )
  => IndigoEntrypoint tp
checkHasDigitOne val = do
  description checkHasDigitOneDoc
  example (1 int)
  currentVal <- new$ val
  setStorageField @Storage #sLastInput currentVal
  base <- new$ 10 nat
  checkRes <- new$ False
  while (val > base && not checkRes) do
    currentVal =: currentVal / base
    remainder <- new$ val % base
    checkRes =: remainder == 1 nat
  updateStorage checkRes

updateStorage :: HasStorage Storage => Var Bool -> IndigoProcedure
updateStorage result = defFunction do
  if result then
    setStorageField @Storage #sTotal $ 0 nat
  else
    incrementStorage

incrementStorage :: HasStorage Storage => IndigoProcedure
incrementStorage = defFunction do
  currentTotal <- getStorageField  @Storage #sTotal
  setStorageField @Storage #sTotal (currentTotal + (1 nat))

Then we have another module that imports these all together.

module MyContract
    ( Parameter(..)
    , Storage(..)
    , myContractLorentz
    , emptyStorage
    ) where

import MyContract.Impl
import MyContract.Types

Note

Up to this point we have used the export list to make contracts available to the REPL and import to made Indigo functions and types available to a module. In the same way, we can also export contracts/types/functions from a module and use import in another one to make them available in the latter.

Setting up CLI

Next, we are going to import our contract in our Main.hs so that we can interact with the contract.

Do not worry about understanding everything, most of the things are boilerate and generated by Indigo CLI.

module Main
  ( main
  ) where

import Universum

import Data.Map qualified as Map
import Lorentz (DGitRevision(..), mkContract)
import Lorentz.ContractRegistry
import Main.Utf8 (withUtf8)
import Options.Applicative qualified as Opt
import Options.Applicative.Help.Pretty (Doc, linebreak)
import System.Environment (withProgName)

import Basic qualified
import MyContract qualified

programInfo :: DGitRevision -> Opt.ParserInfo CmdLnArgs
programInfo gitRev = Opt.info (Opt.helper <*> argParser contracts gitRev) $
  mconcat
  [ Opt.fullDesc
  , Opt.progDesc "Indigo CLI provides commands for development and interaction with Indigo project."
  , Opt.header "Indigo CLI"
  , Opt.footerDoc $ Just usageDoc
  ]

usageDoc :: Doc
usageDoc = mconcat
   [ "You can use help for specific COMMAND", linebreak
   , "EXAMPLE:", linebreak
   , "  indigo run print --help", linebreak
   ]

contracts :: ContractRegistry
contracts = ContractRegistry $ Map.fromList
  [ "Basic" ?:: ContractInfo
    { ciContract = mkContract $ Basic.basicContractLorentz
    , ciIsDocumented = True
    , ciStorageParser = Just (pure Basic.emptyStorage)
    , ciStorageNotes = Nothing
    }
  , "MyContract" ?:: ContractInfo
    { ciContract = mkContract $ MyContract.myContractLorentz
    , ciIsDocumented = True
    , ciStorageParser = Just (pure MyContract.emptyStorage)
    , ciStorageNotes = Nothing
    }
  ]

main :: IO ()
main = withUtf8 $ withProgName "indigo run" $ do
  cmdLnArgs <- Opt.execParser (programInfo DGitRevisionUnknown)
  runContractRegistry contracts cmdLnArgs `catchAny` (die . displayException)

Pay attention to this part, this is how you add multiple contracts to the project.

   ]

contracts :: ContractRegistry
contracts = ContractRegistry $ Map.fromList
  [ "Basic" ?:: ContractInfo
    { ciContract = mkContract $ Basic.basicContractLorentz
    , ciIsDocumented = True
    , ciStorageParser = Just (pure Basic.emptyStorage)
    , ciStorageNotes = Nothing
    }
  , "MyContract" ?:: ContractInfo
    { ciContract = mkContract $ MyContract.myContractLorentz
    , ciIsDocumented = True
    , ciStorageParser = Just (pure MyContract.emptyStorage)
    , ciStorageNotes = Nothing

To include our contract in the CLI, you have to specify these 4 fields:

  • ciContract: Simply put our contract here
  • ciIsDocumented: Set to True to allow generating documentation for this contract
  • ciStorageParser: Set to Just (pure MyContract.emptyStorage) to add the
    commands storage-MyContract for printing the initial storage of our contract.
  • ciStorageNotes: is for customizing the generated annotations of our contract. Since we do not want to change any annotations for now, we can simply set to Nothing.

With this setup, running indigo run list will output

Available contracts:
  - Basic
  - MyContract

Testing

We have a simple test for our contract:

{-# OPTIONS_GHC -Wno-orphans #-}

module Test.MyContract
  ( test_MyContract_updates_storage_properly
  ) where

import MyContract (Parameter(..), Storage(..), emptyStorage, myContractLorentz)

import Lorentz
import Test.Cleveland
import Test.Tasty (TestTree)

deriveRPC "Storage"

test_MyContract_updates_storage_properly :: TestTree
test_MyContract_updates_storage_properly = testScenario "mycontract test" $ scenario do
  contractAddr <- originate "my-contract" emptyStorage $ mkContract myContractLorentz

  transfer contractAddr $ calling def (HasDigitOne 1)

  storage <- getStorage contractAddr
  sLastInputRPC storage @== 1
  sTotalRPC storage @== 1

We will go into the details of writing the tests for indigo later on, but basically in this test, we call the entrypoint HasDigitOne with the value 1.

And we expect the updated storage to be:

Storage
  { sLastInput = 1
  , sTotal = 1
  }

To run the test, simply run indigo test.

Note

If your indigo CLI is installed via docker, this command is not supported yet.

Indigo CLI Usage

The advantage of setting up the project is that it allows us to do more things compared to using the REPL.

As you can see above, we have introduced a few new commands and now it is time to go into details.

The main commands of Indigo CLI are:

  • indigo new <project-name>: Generate a starter indigo project.
  • indigo build: Build the current project.
  • indigo run <command>: Run a command in the current project.
  • indigo test: Run all the tests in the project.
  • indigo repl: Open the Indigo REPL.
  • indigo upgrade: Build and install the latest indigo version.

The main way that we are going to interact with our contract are through indigo run commands.

To see what are the available run commands, we can execute indigo run --help.

The arguments to the indigo run commands are:

  • list: This is the commands that we already encounter. It basically prints out all the contracts we have in our project.

  • print: Print a contract in its Michelson form.

    • Example: indigo run print -n MyContract
    • Result: This will output the michelson code in MyContract.tz
  • document: Print the documentation of a contract. This is similar to the REPL function: saveDocumentation.

    • Example: indigo run document -n MyContract
    • Result: This will output the documentation in MyContract.md
  • analyze: Analyze the contract and prints statistic about it.

    • Example: indigo run analyze -n MyContract
  • storage-<contract-name>: Print initial storage for the given contract.

    • Example: indigo run storage-MyContract
    • Result: This simply outputs Pair 0 0.

This concludes our tutorial, we hope you can use the informations that you now possess to develop new and exciting Michelson contracts with Indigo.