Side effects and Errors¶
Smart contracts usually interact with each other and in some cases (provided some conditions) they are supposed to fail. Indigo supports side-effects and errors to handle these needs, respectively.
Failing instead¶
In the contract from the previous chapter we performed a check, based
on the parameter of the contract, and modified the storage
depending on the
result of said check.
When the check didn't pass we "reset" the storage
to 0
, however, this may not
always be what we want. Instead we may only want to increment the storage when
the check passes and stop the execution entirely when it doesn't.
This chapter has an Errors.hs
file that contains errorsContract
:
module Indigo.Tutorial.SideEffects.Errors
( errorsContract
) where
import Indigo
data IncrementIf
= IsZero Integer
| HasDigitOne Natural
deriving stock (Generic, Show)
deriving anyclass (IsoValue)
instance ParameterHasEntrypoints IncrementIf where
type ParameterEntrypointsDerivation IncrementIf = EpdPlain
errorsContract :: IndigoContract IncrementIf Natural
errorsContract param = defContract do
case_ param $
( #cIsZero #= checkZero
, #cHasDigitOne #= checkHasDigitOne
)
incrementStorage
checkZero :: Var Integer -> IndigoProcedure
checkZero val = defFunction do
assert [mt|unacceptable zero value|] (val == 0 int)
checkHasDigitOne :: Var Natural -> IndigoProcedure
checkHasDigitOne val = defFunction do
base <- new$ 10 nat
while (val > base) do
val =: val / base
remainder <- new$ val % base
checkSingleDigitOne remainder
checkSingleDigitOne val
checkSingleDigitOne :: Var Natural -> IndigoProcedure
checkSingleDigitOne digit = do
assert [mt|unacceptable non-one digit|] (digit == 1 nat)
incrementStorage :: HasStorage Natural => IndigoProcedure
incrementStorage = defFunction do
storage += 1 nat
storage :: HasStorage Natural => Var Natural
storage = storageVar
It differs from the previous contract in two ways:
- as said we want to stop execution when a check does not succeed
- we changed the logic of the
checkHasDigitOne
from "needs at least one digit equal to 1" to "needs all digits to be equal to 1". This is just for demonstration purposes, it could have had the same logic.
This contract, or rather its functions, use the most common error-throwing
mechanism, which is to check for a Bool
ean condition and fail if it is False
.
In such cases the assert
statement comes in handy: it expects a message for
the possible error and a Bool
ean expression for the condition.
If we take a look at the errorsContract
we can see that, compared to the
previous one:
result <- case_ param $
we do not want the Bool
ean result from the case_
:
case_ param $
instead we let the check end in failure if necessary:
checkZero val = defFunction do
assert [mt|unacceptable zero value|] (val == 0 int)
checkSingleDigitOne digit = do
assert [mt|unacceptable non-one digit|] (digit == 1 nat)
Note that after the case_
we can now just perform incrementStorage
, this is
because a failure short-circuits the execution of the contract, meaning that if
we get to this point we can be certain that the previous check did not fail:
errorsContract :: IndigoContract IncrementIf Natural
errorsContract param = defContract do
case_ param $
( #cIsZero #= checkZero
, #cHasDigitOne #= checkHasDigitOne
)
incrementStorage
You can see how simple the usage of assert
is, by looking at the new
implementation of checkZero
above.
If you are surprised by how the message is constructed, take a look at
the types reference, this is just an MText
.
Note also that the signature for checkZero
has changed and now it is an
IndigoProcedure
, because it now returns nothing instead of a Bool
ean:
checkZero :: Var Integer -> IndigoProcedure
Keeping in mind the difference in logic for checkHasDigitOne
described above,
you should be easily able to understand how it is implemented:
checkHasDigitOne :: Var Natural -> IndigoProcedure
checkHasDigitOne val = defFunction do
base <- new$ 10 nat
while (val > base) do
val =: val / base
remainder <- new$ val % base
checkSingleDigitOne remainder
checkSingleDigitOne val
Apart from the assert
that we show here, Indigo also supports these constructs
for failing:
failWith
that has one argument: an expression for any type, this is the same behavior as Michelson'sFAILWITH
failCustom
that expects aLabel
and an error message to compose a custom failure valuefailCustom_
that has only oneLabel
argument, and is a specialization of the previous one. See an example on how to use this in: Custom error messagesfailUnexpected_
that has one argument: an expression for an error message, just likeassert
assertCustom
that works just likeassert
, but with the same inputs asfailCustom
assertCustom_
that works just likeassert
, but with the same input asfailCustom_
Side Effects¶
Michelson provides three instructions to perform external operation
s:
- CREATE_CONTRACT
- TRANSFER_TOKENS
- SET_DELEGATE
and expects the operation
s you produce to be put into a list and returned with
the new storage of the contract at the end of its execution.
Indigo handles the operation
s you create automatically and makes sure they are
returned as Michelson expects them to be.
So the statements it provides for the instructions above are:
createContract
returns anAddress
and takes: theIndigoContract
to create, aMaybe KeyHash
for the optional delegate, aMutez
for the initial amount to take from the originating contract and the initial storagetransferTokens
returns nothing and takes: a parameter for the destination contract, aMutez
to transfer to it from the calling contract and aContractRef
setDelegate
returns nothing and takes aMaybe KeyHash
In the second file for this chapter, SideEffects.hs
, we can see an example of
a contract with side effects, sideEffectsContract
:
module Indigo.Tutorial.SideEffects.SideEffects
( sideEffectsContract
, textKeeper
) where
import Indigo
sideEffectsContract :: IndigoContract (Maybe MText) Address
sideEffectsContract param = defContract do
deleg <- new$ none
mtz <- new$ zeroMutez
notInit <- new$ [mt|not initialized|]
addr <- createContract textKeeper deleg mtz notInit
ifSome param (initializeContract addr) (return ())
storage =: addr
textKeeper :: IndigoContract MText MText
textKeeper param = defContract do
storageVar =: param
initializeContract :: (HasSideEffects, IsNotInView) => Var Address -> Var MText -> IndigoProcedure
initializeContract addr msg = defFunction do
mtz <- new$ zeroMutez
ifSome (contract addr)
(\cRef -> transferTokens msg mtz cRef)
failForRef
failForRef :: IndigoProcedure
failForRef = failUnexpected_ @() [mt|failed to create contract reference|]
storage :: HasStorage Address => Var Address
storage = storageVar
As you can see this contract uses createContract
with textKeeper
that we took
from a previous example and creates it with the not initialized
string.
It then decides, using ifSome
, to change its storage or not with transferTokens
.
Note that, much like it happens for HasStorage
, functions that want to use
side effects require the constraint HasSideEffects
to be specified.
If you need a function that does both, remember that you can do so by using
the syntax in this formula:
<function_name> :: (HasStorage <storage_type>, HasSideEffects) => IndigoFunction <return_type>
initializeContract
also specifies IsNotInView
constraint; jakarta protocol
forbids using some instructions in on-chain views, transferTokens
is one of
those; defContract
automatically provides this constraint, but in a standalone
function we have to specify it explicitly.
If you followed up to this point this contract should not be confusing, remember that you can check out the reference pages for types and for expressions if you don't recognize something.
On the next chapter, we will explain how to add simple documentations to our contracts: Contract documentation.
Technical details: SideEffects and Errors¶
There is not much to say about these two.
Regarding side effects, contract compilation makes sure to create a list of
Operation
s and assign a Var
iable to it that is accessible using Given
.
At the end of the contract this list is paired with the storage.
You can check this out in the Indigo.Compilation
module.
Statements with side effects can then get this Var
and update the list it
refers to, adding a new Operation
to it.
Failure is instead handled by using the relative Lorentz instruction
and filling the rest of the resulting GenCode
with error
s.
Safety of using error
We can safely do this because those fields are not going to ever be evaluated, due to being lazy, and so they will never actually throw an error.
You can read more about it by looking at the Indigo.Backend.Error
module.
As a reminder Indigo has Haddock documentation, if you are interested more in learning about its internals and exported content alike.