Unable to fetchByKey in a choice despite being observer

I have some code that is analogous to this:

type ProductKey = (Party, Text)

template Product
  with
    owner : Party
    productId : Text
    handler : Party
    requestObserver : Party
  where
    signatory owner
    observer requestObserver
    key (owner, productId) : ProductKey
    maintainer key._1
    controller handler can
      MakeHandoverRequest : (ContractId Product, ContractId HandoverRequest)
        with
          newHandler : Party
        do
         productCid <- create this with
            requestObserver = newHandler
 
        requestCid <- createHandoverRequest with
          newHandler
          productKey = (owner, productId)
          productOwner = owner
          oldHandler = handler

        return (productCid, requestCid)

template HandoverRequest
  with
    oldHandler : Party
    productOwner : Party
    productKey : ProductKey
    newHandler : Party
  where
    signatory productOwner, oldHandler
    controller newHandler can
      (productCid, product) <- fetchByKey @Product productKey
      ....

      archive productCid
      create product with ...

with script like:

...
  (orangesCid) <- submit producer do
    createCmd Product with
      owner = producer
      productId = "abc"
      handler = producer
      requestObserver = producer

  (orangesCid, handoverRequestCid) <- submit producer do
    exerciseCmd orangesCid MakeHandoverRequest with
      newHandler = processor

  (orangesCid, recordCid, oldRecordCids) <- submit processor do
    exerciseCmd handoverRequestCid AcceptHandoverRequest with
      recordId = "123"
      recordTime = now

I get the error message:

Error: User abort: Submit failed with code 3: Command interpretation error in LF-DAMLe: dependency error: couldn’t find key com.daml.lf.transaction.Node$GlobalKey@4b148dba. Details: N/A.

I believe this occurs because newHandler cannot see the Product. The thing that confuses me is that I made newHandler an observer in the original choice. It works if I set newHandler = owner in the choice.

Thanks!

1 Like

I suspect that there might be something else going on in your actual code. I’ve tried to isolate it to a standalone example. However, that actually works as expected. Can you share some more context on your example?

module Main where

type ProductKey = (Party, Text)

template Product
  with
    owner : Party
    productId : Text
    handler : Party
    requestObserver : Party
  where
    signatory owner
    observer requestObserver
    key (owner, productId) : ProductKey
    maintainer key._1
    controller handler can
      MakeHandoverRequest : (ContractId Product, ContractId HandoverRequest)
        with
          newHandler : Party
        do
         productCid <- create this with
            requestObserver = newHandler

         requestCid <- create HandoverRequest with
          newHandler
          productKey = (owner, productId)
          productOwner = owner
          oldHandler = handler

         return (productCid, requestCid)

template HandoverRequest
  with
    oldHandler : Party
    productOwner : Party
    productKey : ProductKey
    newHandler : Party
  where
    signatory productOwner, oldHandler
    controller newHandler can
      AcceptHandoverRequest : ContractId Product
        do (productCid, product) <- fetchByKey @Product productKey
           archive productCid
           create product


test = do
  producer <- getParty "producer"
  processor <- getParty "processor"
  (orangesCid) <- submit producer do
    create Product with
      owner = producer
      productId = "abc"
      handler = producer
      requestObserver = producer

  (orangesCid, handoverRequestCid) <- submit producer do
    exercise orangesCid MakeHandoverRequest with
      newHandler = processor

  _ <- submit processor do
    exercise handoverRequestCid AcceptHandoverRequest
  pure ()
1 Like

Sure, I’ll send all the code (with completely irrelevant parts redacted):

template Product
  with
    productId : Text
    owner : Party
    handler : Party
    productType : Text
    recordKeys : [RecordKey]
    requestObserver : Party
  where
    signatory owner
    observer requestObserver
    key (owner, productId) : ProductKey
    maintainer key._1

    controller owner can
      MakeHandoverRequest : (ContractId Product, ContractId HandoverRequest)
        with
          newHandler : Party
        do
          productCid <- create this with
            requestObserver = newHandler

          handoverRequestCid <- create HandoverRequest with
            productKey = (owner, productId)
            newHandler
            oldHandler = handler
            productOwner = owner
            productId

          return (productCid, handoverRequestCid)
...

template HandoverRequest
  with
    productId : Text
    productKey : ProductKey
    productOwner : Party
    oldHandler : Party
    newHandler : Party
  where
    signatory productOwner, oldHandler

    controller newHandler can
      AcceptHandoverRequest : (ContractId Product, ContractId Record, [ContractId Record])
        with
          recordId : Text
          recordTime : Time
        do
          (productCid, product) <- fetchByKey @Product productKey

          assert (product.handler == oldHandler)
          assert (recordTime > product.lastUpdated)

          let recordKey = (oldHandler, recordId)

          recordCid <- create Record with
            recordType = HANDOVER
            productId
            recordId
            completionTime = recordTime
            actor = oldHandler
            productOwner
            details = HandoverRecord HandoverRecordDetails with
              newHandler = newHandler
              time = recordTime
            productCurrentHandler = newHandler

          archive productCid

          productCid <- create product with
            recordKeys = product.recordKeys ++ [recordKey]
            handler = newHandler

          oldRecordCids <- forA product.recordKeys $ \k -> do
            exerciseByKey @Record k ListNewHandlerOnRecord with newHandler = newHandler

          return (productCid, recordCid, oldRecordCids)

I think I have just realised my problem: that second to last line (with the loop on exerciseByKey)… The oldHandler is an observer on the Records but not the newHandler

2 Likes

@cocreature I’ll delete this post, feel free to undelete if you think it is actually useful to others

1 Like

Let’s keep it, I think it’s useful for others that might run into similar problems and it shows that we probably should have better error messages that allow you to track this down more quickly.

2 Likes

@cocreature the code I last shared does raise an important point. If the HandoverRequest is accepted, there are some things that need to be done (like that final looped choice on the Records) that would be better done as soon as the request is accepted. I could do this by making another contract like HandoverAcceptedRecord that has a choice where the oldHandler can call the ListNewHandlerOnRecord choice, but this is not the workflow I want ([Request, Accept, Extra step] as opposed to [Request, Accept]). Is it possible to do this without the extra workflow step?

2 Likes