In the code below, Catalogue_ImportCategoryArray, Catalogue_AddCategory and Catalogue_AddProduct are all consuming and are defined from the same template:
controller owner can
Catalogue_ImportCategoryArray: ()
with
categoryArray: [CategoryDetails]
do
forA_ categoryArray $ \cc -> do
exercise self Catalogue_AddCategory with
categoryCode = cc.categoryCode
hsCode = cc.hsCode
description = cc.description
forA_ cc.productDetails $ \pp -> do
exercise self Catalogue_AddProduct with
categoryCode = cc.categoryCode
description = pp.description
unitCost = pp.unitCost
When importCategoryArray() is called, I get the following error:
**Attempt to exercise a contract that was consumed in same transaction.**
Then I rewrite it with exerciseByKey
controller owner can
Catalogue_ImportCategoryArray: ()
with
categoryArray: [CategoryDetails]
do
forA_ categoryArray $ \cc -> do
exerciseByKey @Catalogue (owner, catalogueId) Catalogue_AddCategory with
categoryCode = cc.categoryCode
hsCode = cc.hsCode
description = cc.description
forA_ cc.productDetails $ \pp -> do
exerciseByKey @Catalogue (owner, catalogueId) Catalogue_AddProduct with
categoryCode = cc.categoryCode
description = pp.description
unitCost = pp.unitCost
And I get another error:
CRASH: Could not find key GlobalKey(-homePackageId-:Keplaax.Catalogue:Catalogue, ValueRecord(Some(40f452260bef3f29dede136108fc08a88d5a5250310281067087da6f0baddff7:DA.Types:Tuple2),ImmArray((Some(_1),ValueParty(dragonflyCN)),(Some(_2),ValueInt64(1)))))
That’s odd, as the contract has already been created and the key is unique.
The issue here is that ImportCategoryArray itself is also a consuming choice. Consuming choices archive the contract before executing the choice body. So by the point you get to exercise or exerciseByKey the contract has been archived and you get the errors you’re seeing.
I recommend making Catalogue_ImportCategoryArray ad non-consuming choice. At that point the first exercise will succeed since self is not archived at that point. However, a second exercise would still fail since after the first consuming exercise of AddCategory the contract has been archived. There are two ways to solve this:
Pass the current contract id along using something like foldlA.
Use a contract key and exerciseByKey in your example.
Here is a minimized example that demonstrates the different options and the issues in the first two solutions:
module Main where
import DA.Action
import DA.Foldable
import Daml.Script
template T
with
p : Party
id : Text
where
signatory p
key (p, id): (Party, Text)
maintainer key._1
choice DoSomething : ContractId T
controller p
do create this
choice DoNTimesFails : ()
with
n : Int
controller p
do -- Fails for n >= 1 because this is a consuming choice so the contract
-- has already been archived at this point.
forA_ [1..n] $ \_ -> exercise self DoSomething
nonconsuming choice DoNTimesNonConsumingFails : ()
with
n : Int
controller p
do -- Succeeds for n = 1 but fails for n > 1 because self points to the original
-- contract id which has been archived at that point.
forA_ [1..n] $ \_ -> exercise self DoSomething
nonconsuming choice DoNTimesPassAlong : ()
with
n : Int
controller p
do foldlA (\cid _ -> exercise cid DoSomething) self [1..n]
pure ()
nonconsuming choice DoNTimesKey : ()
with
n : Int
controller p
do forA_ [1..n] $ \_ -> exerciseByKey @T (p, id) DoSomething
test = do
p <- allocateParty "p"
submitMustFail p $ createAndExerciseCmd (T p "1") (DoNTimesFails 1)
submit p $ createAndExerciseCmd (T p "2") (DoNTimesNonConsumingFails 1)
submitMustFail p $ createAndExerciseCmd (T p "3") (DoNTimesNonConsumingFails 2)
submit p $ createAndExerciseCmd (T p "4") (DoNTimesPassAlong 42)
submit p $ createAndExerciseCmd (T p "5") (DoNTimesKey 42)
Lastly, I think it’s worth asking yourself whether you really want to exercise choices from ImportCategoryArray at all. It looks like in the end all you’re doing is a bunch of data transformation on self at which point, you could potentially do everything directly in ImportCategoryArray. Each exercise creates an extra transaction node so if you don’t need the extra exercise nodes, you might as well avoid them. If all you’re after is to share logic you can move it to a template let or top-level functions.