How to exercise multiple consuming choices?

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.

2 Likes

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:

  1. Pass the current contract id along using something like foldlA.
  2. 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.

2 Likes

Thank you. I have rewrote the code so there will only be one transaction when a category (but not the product) is updated.

1 Like