dazl

Workflow State Example

The purpose of this example is to demonstrate a multi-contract-creation dependency use-case: a sitution where the application must wait for multiple contracts to be created before it can proceed to the next step. This is similar to the “Message Ingester” sample application, but with the key difference that the state transitions in the “Message Ingester” workflow each depend solely on a single contract creation.

The workflow involving three parties: Alice, Bob, and Operator. When writing a DAML application, we recommend the following steps:

  1. Describe your workflow as a series of contracts in DAML.

  2. Write one or more DAML test scenarios that walk through each step of the workflow, starting from creation of the genesis contract, through a sequence of exercise commands, to the final state of the workflow. (Note: The DAML test scenarios are used for verifying the steps of the workflow in a sequential manner. However, when the workflow is deployed to a live platflorm, the events that prompt steps in the workflow to move forward cannot be assumed to occur sequentially.)

  3. Write your application.

  4. Test your application on the sandbox. The ledger server implements the same API, so performance testing can be done with the same application.

The workflow for this application is as follows:

  1. The GenesisContract, is created. It describes the operation(s) that the operator of this workflow can perform. In this example, we have assigned the party name Operator to the GenesisContract.

  2. Operator invites the Bob to be the ticket seller, and also invites Alice to be the ticket buyer.

  3. Only after BOTH Alice and Bob have accepted their respective invitations can the workflow progress to the next state (TicketTransactionsInProgress)

  4. A ticket transaction occurs.

  5. The workflow is completed.

DAML Model

WorkflowStateExample.daml

daml 1.0
module WorkflowStateExample where

-- The "Workflow State Contract" Best Practice.
-- 
-- Purpose: To encapsulate the state of a workflow within a contract. This results in easier 
-- development & debugging because workflow milestones are clearly defined 

template GenesisContract
  with
    operator: Party
  where
    signatory operator

    controller operator can
      anytime SetInitialWorkflowState
        returning ContractId WorkflowSetupInProgress
        to create WorkflowSetupInProgress with operator=operator
      anytime InviteTicketBuyer with ticketBuyer: Party
        returning ContractId TicketBuyerInvitation
        to do
          create TicketBuyerInvitation with operator; ticketBuyer;
      anytime InviteTicketSeller with ticketSeller: Party
        returning ContractId TicketSellerInvitation
        to create TicketSellerInvitation with operator; ticketSeller;

template TicketBuyerInvitation
  with
    operator: Party
    ticketBuyer: Party
  where
    signatory operator

    controller ticketBuyer can
      AcceptTicketBuyerInvitation
        returning ContractId TicketBuyerRole
        to create TicketBuyerRole with ticketBuyer; operator;
 
template TicketBuyerRole
  with
    ticketBuyer: Party
    operator: Party
  where
    signatory ticketBuyer

template TicketSellerInvitation
  with
    operator: Party
    ticketSeller: Party
  where
    signatory operator

    controller ticketSeller can
      AcceptTicketSellerInvitation
        returning ContractId TicketSellerRole
        to create TicketSellerRole with ticketSeller; operator;

template TicketSellerRole
  with 
    ticketSeller: Party
    operator: Party
  where
    signatory ticketSeller

    controller ticketSeller can
      anytime OfferTicketPurchaseAgreement with ticketBuyer: Party
        returning ContractId TicketPurchaseAgreementOffer 
        to create TicketPurchaseAgreementOffer with ticketSeller; ticketBuyer; operator;

template TicketPurchaseAgreementOffer
  with
    ticketSeller: Party
    ticketBuyer: Party
    operator: Party
  where
    signatory ticketSeller

    controller ticketBuyer can
      PurchaseTicket
        returning ContractId TicketPurchaseAgreement
        to create TicketPurchaseAgreement with ticketSeller; ticketBuyer; operator;

template TicketPurchaseAgreement
  with
    ticketSeller: Party
    ticketBuyer: Party
    operator: Party

  where
    signatory ticketSeller
    signatory ticketBuyer
    agreement toText ticketBuyer <> " agrees to purchase ticket from " <> toText ticketSeller 

-- Workflow state 1 of 3
template WorkflowSetupInProgress
  with
    operator: Party
  where
    signatory operator

    controller operator can
      TicketTransactionsInProgress
        returning ContractId WorkflowTicketTransactionsInProgress
        to create WorkflowTicketTransactionsInProgress with operator

-- Workflow state 2 of 3
template WorkflowTicketTransactionsInProgress
  with 
    operator: Party
  where
    signatory operator

    controller operator can
      WorkflowCompleted
        returning ContractId WorkflowCompleted
        to create WorkflowCompleted with operator

-- Workflow state 3 of 3
template WorkflowCompleted
  with 
    operator: Party
  where
    signatory operator

test ticketTransactionTest = 
  scenario
    -- create the genesis contract
    genesisContract <- 'Operator' commits create GenesisContract with operator='Operator'
    -- set the initial workflow state (InviteParticipantsInProgress)
    workflowSetupInProgress <- 'Operator' commits exercise genesisContract SetInitialWorkflowState

    -- initial state: InviteParticipantsInProgress
    -- operator shall invite all the participants to the workflow, and respective participant role contracts 
    -- shall be created

    -- note: scenarios list actions in series, but these actions do not have a guaranteed order when running on the Platform 

    ticketSellerInvitation <- 'Operator' commits exercise genesisContract InviteTicketSeller with ticketSeller='Bob'
    ticketSellerRole <- 'Bob' commits exercise ticketSellerInvitation AcceptTicketSellerInvitation

    ticketBuyerInvitation <- 'Operator' commits exercise genesisContract InviteTicketBuyer with ticketBuyer='Alice'
    ticketBuyerRole <- 'Alice' commits exercise ticketBuyerInvitation AcceptTicketBuyerInvitation
    
    -- next state: TicketTransactionsInProgress
    -- the application must detect when this state transition can occur, and then perform this exercise
    workflowTicketTransactionsInProgress <- 'Operator' commits exercise workflowSetupInProgress TicketTransactionsInProgress

    -- In this phase of the workflow, one or more ticket purchases may occur

    -- a ticket is purchsed
    offerTicketPurchaseAgreement <- 'Bob' commits exercise ticketSellerRole OfferTicketPurchaseAgreement with ticketBuyer='Alice'
    'Alice' commits exercise offerTicketPurchaseAgreement PurchaseTicket

    -- ...more ticket purchases may occur here
    
    -- next state: WorkflowCompleted
    -- the application must detect when this state transition shall occur, and then perform this exercise
    workflowCompleted <- 'Operator' commits exercise workflowTicketTransactionsInProgress WorkflowCompleted

    return {
      genesisContract=genesisContract;
      ticketBuyerInvitation=ticketBuyerInvitation;
      ticketBuyerRole=ticketBuyerRole;
      ticketSellerInvitation=ticketSellerInvitation;
      ticketSellerRole=ticketSellerRole;
      workflowCompleted=workflowCompleted;
    }

The ticketTransactionTest scenario describes a sample execution of the workflow, and is the basis from from which our Python application will be designed.

Python Application

store.py

# Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import asyncio
import logging

LOG = logging.getLogger('store')


class ContractStore:
    def __init__(self, loop=None, verbose=False):
        self.verbose = verbose
        self.binds = dict()
        self.loop = loop or asyncio.get_event_loop()

    def save(self, name, cid, cdata):
        if self.verbose:
            LOG.critical('Saving %s %s %s', name, cid, cdata)
        name = self._resolve_name(name)
        future = self.binds.get(name)
        if future is None or future.done():
            future = self.loop.create_future()
            self.binds[name] = future
        future.set_result((cid, cdata))

    def find(self, name):
        """
        Find the contract under the given name.

        :param name: The "name" of the contract.
        :return: A ``Future`` that resolves to a (cid, cdata) tuple.
        """
        if self.verbose:
            LOG.critical('Finding in store %s', name)
        else:
            LOG.info('Finding in store %s', name)
        name = self._resolve_name(name)
        future = self.binds.get(name)
        if future is None:
            future = self.loop.create_future()
            self.binds[name] = future
        return future

    def delete(self, name):
        if self.verbose:
            LOG.critical('Deleting from store %s', name)
        name = self._resolve_name(name)
        future = self.binds.get(name)
        if future is not None:
            future.cancel()
            del self.binds[name]

    def archive(self, cid):
        """
        Mark the contract as archived.

        :param cid: The contract ID that is no longer active.
        """
        for key, future in self.binds.items():
            if future.done():
                if future.result() == cid:
                    del self.binds[key]

    def _resolve_name(self, name):
        if isinstance(name, list):
            return '_'.join(name)
        return name

    def match(self, is_match):
        xs = []
        for key, future in self.binds.items():
            cid, value = future.result()
            if is_match(key, cid, value):
                xs.append((key, cid, value))
        return xs

workflow_state_example.py

# Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from dazl import create, exercise
from dazl.client import create_client
from dazl.plugins import LedgerCapturePlugin
from store import ContractStore
import subprocess

operator = 'Operator'
alice = 'Alice'
bob = 'Bob'

parties = [operator,alice,bob]
url = 'http://localhost:7600/'
contract_store = ContractStore()
transaction_limit = 1


def create_initial_workflow_state(cid, cdata):
    workflow_state = [ exercise(cid, 'SetInitialWorkflowState', {})]
    return workflow_state

def invite_ticket_seller(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_seller_invitation = [ exercise(cid, 'InviteTicketSeller', {'ticketSeller': bob})]
    return ticket_seller_invitation

def invite_ticket_buyer(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_buyer_invitation = [ exercise(cid, 'InviteTicketBuyer', {'ticketBuyer': alice})]
    return ticket_buyer_invitation

# DOC_BEGIN: FUNCTION_ACCEPT_INVITE
def accept_ticket_seller_invite(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_seller_role = [ exercise(cid, 'AcceptTicketSellerInvitation', {})]
    contract_store.save('ticketSellerRole1', cid, cdata)
    return ticket_seller_role

def accept_ticket_buyer_invite(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_buyer_role = [ exercise(cid, 'AcceptTicketBuyerInvitation', {})]
    contract_store.save('ticketBuyerRole1', cid, cdata)
    return ticket_buyer_role
# DOC_END: FUNCTION_ACCEPT_INVITE

# DOC_BEGIN: FUNCTION_MULTI_CREATION_DEPENDENCY
async def transition_to_ticket_transactions_in_progress(cid, cdata):
    await contract_store.find('ticketSellerRole1')
    await contract_store.find('ticketBuyerRole1')
    workflow_ticket_transactions_in_progress = [ exercise(cid, 'TicketTransactionsInProgress', {})]
    contract_store.save('workflowTicketTransactionsInProgress', cid, cdata)
    return workflow_ticket_transactions_in_progress
# DOC_END: FUNCTION_MULTI_CREATION_DEPENDENCY

async def offer_ticket_purchase_agreement(cid, cdata):
    '''Workflow state: WorkflowTicketTransactionsInProgress'''
    await contract_store.find('workflowTicketTransactionsInProgress')
    offer_ticket_purchase_agreement = [exercise(cid, 'OfferTicketPurchaseAgreement', {'ticketBuyer': alice})]
    global transaction_limit
    if transaction_limit > 0:
        transaction_limit -= 1
        return offer_ticket_purchase_agreement

def purchase_ticket(cid, cdata):
    '''Workflow state: WorkflowTicketTransactionsInProgress'''
    ticket_purchase_agreement = [exercise(cid, 'PurchaseTicket', {})]
    return ticket_purchase_agreement

def save_purchase_agreement(cid, cdata):
    '''Workflow state: WorkflowTicketTransactionsInProgress'''
    contract_store.save('purchase_agreement', cid, cdata)

async def transition_to_workflow_completed(cid, cdata):
    await contract_store.find('purchase_agreement')
    #move on to the next phase of the workflow after 1 purchase agreement has been created
    workflow_completed =[exercise(cid, 'WorkflowCompleted', {})]
    return workflow_completed

def register_event_handlers(client_mgr):
    '''
    Register event handlers with the appropriate clients.
    '''

    # initial workflow state (1 of 3): "InviteParticipantsInProgress"

    # Define a ledger client associated with the 'Operator' party, and how it reacts to events
    operator_client = client_mgr.new_client('Operator')
    operator_client.on_created('WorkflowStateExample.GenesisContract', create_initial_workflow_state)
    operator_client.on_created('WorkflowStateExample.GenesisContract', invite_ticket_seller)
    operator_client.on_created('WorkflowStateExample.GenesisContract', invite_ticket_buyer)

    # define a ledger client associated with the ticket seller, and how it reacts to events
    bob_client = client_mgr.new_client(bob)
    bob_client.on_created('WorkflowStateExample.TicketSellerInvitation', accept_ticket_seller_invite)

    # define a ledger client associated with the ticket buyer, and how it reacts to events
    alice_client = client_mgr.new_client(alice)
    alice_client.on_created('WorkflowStateExample.TicketBuyerInvitation', accept_ticket_buyer_invite)

    # transition to workflow state: "TicketTransactionsInProgress"
    operator_client.on_created('WorkflowStateExample.WorkflowSetupInProgress', transition_to_ticket_transactions_in_progress)

    # workflow state (2 of 3): "TicketTransactionsInProgress"
    bob_client.on_created('WorkflowStateExample.TicketSellerRole', offer_ticket_purchase_agreement)

    # alice purchases a ticket
    alice_client.on_created('WorkflowStateExample.TicketPurchaseAgreementOffer', purchase_ticket)
    alice_client.on_created('WorkflowStateExample.TicketPurchaseAgreement', save_purchase_agreement)

    # transition to workflow state (3 of 3): "WorkflowCompleted"
    operator_client.on_created('WorkflowStateExample.WorkflowTicketTransactionsInProgress', transition_to_workflow_completed)

    # all event handlers are defined; create the genesis contract, and all other event handlers shall react thereafter
    operator_client.submit([create('WorkflowStateExample.GenesisContract', { 'operator' : 'Operator' })])

def run():

    with create_client(parties=parties, participant_url=url) as client_mgr:
        inspector = LedgerCapturePlugin.stdout()
        try:
            client_mgr.register(inspector)
            register_event_handlers(client_mgr)
            ledger_run = client_mgr.run_until_complete()
            return ledger_run.exit_code
        finally:
            inspector.dump_all()
            subprocess.Popen(['da', 'stop']).wait()

if __name__ == "__main__":
    subprocess.Popen(['da', 'sandbox']).wait()
    run()

To run this code sample:
  1. Download the SDK

  2. create a new project: da new my-project-name-here

  3. cd my-project-name-here

  4. Create a file, WorkflowStateExample.daml, that contains the above listed DAML.

  5. Create a file, workflow_state_sample.py, that contains the Python code for “workflow_state_example.py” listed above.

  6. Create a file, store.py, that contains, that contains the Python code for “store.py” listed above.

  7. Download the dazl-starter template (which also creates a Python venv): da project add dazl-starter

  8. Run the application: ./venv/bin/python3 workflow_state_example.py

accept_ticket_seller_invite() and accept_ticket_buyer_invite() store their respective contracts into contract_store. This is the first step in setting up a multi-contract-creation dependency.

def accept_ticket_seller_invite(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_seller_role = [ exercise(cid, 'AcceptTicketSellerInvitation', {})]
    contract_store.save('ticketSellerRole1', cid, cdata)
    return ticket_seller_role

def accept_ticket_buyer_invite(cid, cdata):
    '''Workflow state: WorkflowSetupInProgress'''
    ticket_buyer_role = [ exercise(cid, 'AcceptTicketBuyerInvitation', {})]
    contract_store.save('ticketBuyerRole1', cid, cdata)
    return ticket_buyer_role

transition_to_ticket_transactions_in_progress() performs lookups into contract_store. These lookups will wait until the specified contract keys are present in the contract_store, and only perform the exercise command after that point. Thus, BOTH the TicketSellerRole and the TicketBuyerRole must be created before the application can transition to the next step in the workflow.

async def transition_to_ticket_transactions_in_progress(cid, cdata):
    await contract_store.find('ticketSellerRole1')
    await contract_store.find('ticketBuyerRole1')
    workflow_ticket_transactions_in_progress = [ exercise(cid, 'TicketTransactionsInProgress', {})]
    contract_store.save('workflowTicketTransactionsInProgress', cid, cdata)
    return workflow_ticket_transactions_in_progress

Application Output

This application will produce this output:

[Info] Starting:
    Sandbox ledger server
    .../daml/WorkflowStateExample.daml
    with no scenario and binding to port 7600
Waiting for Sandbox...ok
5 total contracts over 5 templates
+-- party 'Alice' (block heights 2 to 9)
|+- party 'Bob' (block heights 2 to 9)
||+ party 'Operator' (block heights 1 to 9)
|||

WorkflowStateExample.GenesisContract (1 contract) ----------------------------------------------------------------------
    #cid operator
  C 0:0_ Operator

WorkflowStateExample.TicketBuyerRole (1 contract) ----------------------------------------------------------------------
    #cid operator ticketBuyer
C   2:2_ Operator Alice

WorkflowStateExample.TicketPurchaseAgreement (1 contract) --------------------------------------------------------------
    #cid operator ticketBuyer ticketSeller
CC  6:2_ Operator Alice       Bob

WorkflowStateExample.TicketSellerRole (1 contract) ---------------------------------------------------------------------
    #cid operator ticketSeller
 C  3:2_ Operator Bob

WorkflowStateExample.WorkflowCompleted (1 contract) --------------------------------------------------------------------
    #cid operator
  C 7:2_ Operator
stopping... Sandbox ledger server
.../daml/WorkflowStateExample.daml
with no scenario and binding to port 7600

Process finished with exit code 0

Thus, a TicketPurchaseAgreement is created. Also note that the contracts representing intermediate steps in the workflow (WorkflowSetupInProgress, and WorkflowTicketTransactionsInProgress) were created and then subsequently archived. Only the contract representing the final state, WorkflowCompleted is active.