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:
Describe your workflow as a series of contracts in DAML.
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.)Write your application.
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:
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 nameOperator
to the GenesisContract.Operator
invites theBob
to be the ticket seller, and also invitesAlice
to be the ticket buyer.Only after BOTH
Alice
andBob
have accepted their respective invitations can the workflow progress to the next state (TicketTransactionsInProgress
)A ticket transaction occurs.
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:
Download the SDK
create a new project:
da new my-project-name-here
cd my-project-name-here
Create a file, WorkflowStateExample.daml, that contains the above listed DAML.
Create a file, workflow_state_sample.py, that contains the Python code for “workflow_state_example.py” listed above.
Create a file, store.py, that contains, that contains the Python code for “store.py” listed above.
Download the dazl-starter template (which also creates a Python venv):
da project add dazl-starter
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.