Datatypes for long data-enrichment workflows

Heya, lately I’ve been having to write long sequential workflows for internal use-cases. So multiple desks and people are contributing, in turn, to what will ultimately be a single contract having dozens of fields and nearly as many signatories. My Daml ends up looking inelegant and redundant: my templates simply get longer and longer as participants add their data contributions.

Have people found a nicer way to do this?

Abstractly, I have this currently:

template Step1
  with 
    allParties: AllTheDesks
  where 
    signatory allParties.requester

    controller allParties.firstContributor can 
      ContributeFirstThing: ContractId Step2
        with 
          firstThing: FirstThing 
        do
          assert $ firstThingIsLegit firstThing
          create Step2 with ..

template Step2
  with 
    allParties: AllTheDesks
    firstThing: FirstThing
  where 
    signatory allParties.requester, allParties.firstContributor

    controller allParties.secondContributor can 
      ContributeSecondThing: ContractId Step3
        with 
          secondThing: SecondThing 
        do
          assert $ secondThingIsLegit secondThing
          create Step3 with ..

...
...
...
template LastStep 
  with 
    allParties: AllTheDesks
    firstThing: FirstThing 
    secondThing: SecondThing 
    ...
    ..
    nthThing: NthThing 
  where 
    signatory everybody allParties 

I realize that I could make a datatype containing firstThing .. nthThing, as all optional fields to reduce on verbosity but somehow that doesn’t smell right.

2 Likes

Is it crucial that all steps have all the signatures? I’d probably solve this something like this:

template Workflow
  with
    workflowId : Text
    allParties: AllTheDesks
    step : Int
  where
    ensure (step =< 42 && step >= 0)
    signatory [nthContributor allParties n | n <- [0..step]]
    observer (everybody allParties)

    key (allParties.requester, workflowId) : (Party, Text)
    maintainer key._1

   choice ContributeFirstThing : ContractId FirstContribution
      with
         firstThing : FirstThing
      controller allParties.firstContributor
      do
        assertMsg "Wrong step" (step == 0)
        create this with step = 1
        assert $ firstThingIsLegit firstThing
        create FirstContribution with ..

   choice ContributeSecondThing : ContractId SecondContribution
      with
         secondThing : SecondThing
      controller allParties.secondContributor
      do
        assertMsg "Wrong step" (step == 1)
        create this with step = 2
        assert $ secondThingIsLegit secondThing
        create SecondContribution with ..
...
...
...
   choice ContributeLastThing : ContractId LastContribution
      with
         lastThing : LastThing
      controller allParties.lastContributor
      do
        assertMsg "Wrong step" (step == 41)
        create this with step = 42
        assert $ lastThingIsLegit lastThing
        create LastContribution with ..

template FirstContribution
  with
    workflowId : Text
    allParties: AllTheDesks
    firstThing: FirstThing
  where
    signatory [nthContributor allParties n | n <- [0..1]]
    observer (everybody allParties)

    key (allParties.requester, workflowId) : (Party, Text)
    maintainer key._1

...
...
...

template LastContribution
  with
    workflowId : Text
    allParties: AllTheDesks
    lastThing: LastThing
  where
    signatory [nthContributor allParties n | n <- [0..41]]
    observer (everybody allParties)

    key (allParties.requester, workflowId) : (Party, Text)
    maintainer key._1

This way you write each contribution to ledger only once, and you end-state is identical except for all contributions being signed by everybody. If that’s a requirement, you could pull everything together into a final thing signed by everybody in a last step. You could also keep track of the CIDs of the contributions on the Workflow contract, which would mean malicious parties couldn’t switch them out.

1 Like

I like it! Further benefit is that except in trivial cases, the contributions really ought to have their own workflow; which we can do here. Somebody might object for the use of the step variable to represent workflow state, but in reality the state is being represented by each individual contribution.

Cheers!

1 Like

Taking a more reductionist approach, you might wish to take advantage of the fact that every template X implies a simple record type data X that can be used as ordinary data.

template Step1
  with 
    allParties: AllTheDesks
  where 
    signatory allParties.requester

    controller allParties.firstContributor can 
      ContributeFirstThing: ContractId Step2
        with 
          firstThing: FirstThing 
        do
          assert $ firstThingIsLegit firstThing
          let prior = this
          create Step2 with ..

template Step2
  with 
    prior: Step1
    firstThing: FirstThing
  where 
    signatory prior.allParties.requester, prior.allParties.firstContributor

The prior in Step2 does not exist as a contract on which choices can be exercised, any more than I can write the tuple (2, 3) and insist “these are the prime factors of 5500”. It is merely the data therein.

The signatory clause does add some problems to this approach, but there are a couple ways of cleaning this up, such as treating the signatories differently among the other data in each step, or defining a typeclass instance on each step (with every step after the first being inductive).

2 Likes