Add documentation to the contract

In this chapter, we will show how to add documentation to our smart contract directly in the code.

We will use a contract that resembles the one in section Functions and Procedures.

You can load the file below into the REPL to follow along with the tutorial.

Contract Name and Description

First let's get started with something simple, we can specify the contract name and description by using docGroup and description like below:

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."

You can generate the documentation of this contract by using:

saveDocumentation @Parameter @Storage myContract "my-documentation.md"

Note

You can also see the documentation immediately in the REPL using:

printDocumentation @Parameter @Storage myContract

If there is no error, you will be able to see a file named my-documentation.md .

We can see that, at the top of the documentation, there are the name and description of our contract. There is also Table of Contents which is generated automatically for us.

# My Contract





This documentation describes an example on how to functions and procedures of Indigo.

<a name="section-Table-of-contents"></a>

## Table of contents

Beside docGroup and description, there are also other doc items that we could use:

  • anchor <link>: Create custom anchor in the generated documentation.
  • example <value>: Use inside an entrypoint to specify an example value for that entrypoint.

There are also many other things that are generated automatically for us, including: Entrypoints and Definitions.

Entrypoints' documentation

We can use description and example as described above to add some information about our entrypoints in the documentation.

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

Now we should have description that looks like this:

  * ***lastInput*** :[`Natural`](#types-Natural)
  * ***total*** :[`Natural`](#types-Natural)
1. Call contract's `isZero` entrypoint passing the constructed argument.
</details>
<p>

Note

There are a lot of other useful things that are generated as well, such as:
- How to call the entrypoint: Instruction on how to call the entrypoint
- Type: Haskell and Michelson Types.
- Example: Example value for Michelson
  - We can modify this value with example as described above

Custom error messages

When handling an entrypoint's input, there are times when the user gives an unexpected value. In this case, we would like to fail with a nice error message.

This is where custom error messages come into play.

We can define custom error messages like below:

[errorDocArg| "isNotZeroError" exception "Fail when the input to `IsZero` entrypoint is not zero." ()|]

As you can see, there are 3 parts to defining a custom error message:

  • "isNotZeroError" is the name of the error.
  • exception is the type of the error. There are 4 types of error you can specify:
    • exception: Normal expected error. Examples: "insufficient balance", "wallet does not exist".
    • bad-argument: Invalid argument passed to entrypoint. Examples: your entrypoint accepts an enum represented as nat, and unknown value is provided.
    • contract-internal: Unexpected error. Most likely it means that there is a bug in the contract or the contract has been deployed incorrectly.
    • unknown: When the error type is beside the other 3.
  • "Fail when the input to `IsZero` entrypoint is not zero." is the description of the error.

This is how you would use your custom error message.

    failCustom_ @() #isNotZeroError

The error message entry will be generated like below:

<a name="errors-InternalError"></a>

---

### `InternalError`

Generate documentations for storage

Let's say that we have a storage type like this:

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

We may want to document this type in our documentation. Here's how we can do it:

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

We also need to add docStorage in the contract code as well:

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

This will generate the doc entry like below:

  Reverse conversion from Michelson value to the Haskell value can be done by serializing Michelson value using `tezos-client hash data` command, resulting `Raw packed data` should be decoded from the hexadecimal representation using `decodeHex` and deserialized to the Haskell value via `lUnpackValue` function from [`Lorentz.Pack`](https://gitlab.com/morley-framework/morley/-/blob/2441e26bebd22ac4b30948e8facbb698d3b25c6d/code/lorentz/src/Lorentz/Pack.hs).

* Construct values for this contract directly on Michelson level using types provided in the documentation.

<a name="section-Storage"></a>

## Storage

Your final code should look like this:

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 can now proceed to the final chapter that explains how to setup a proper Indigo project, which will improve your development with indigo, and also give an overview on the usage of the Indigo CLI: Setting up a project.