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 hereciIsDocumented
: Set toTrue
to allow generating documentation for this contractciStorageParser
: Set toJust (pure MyContract.emptyStorage)
to add the
commandsstorage-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 toNothing
.
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 latestindigo
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
- Example:
-
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
- Example:
-
analyze
: Analyze the contract and prints statistic about it.- Example:
indigo run analyze -n MyContract
- Example:
-
storage-<contract-name>
: Print initial storage for the given contract.- Example:
indigo run storage-MyContract
- Result: This simply outputs
Pair 0 0
.
- Example:
This concludes our tutorial, we hope you can use the informations that you now possess to develop new and exciting Michelson contracts with Indigo.