Post Office¶
This example sets up a post office, with a Postman
who routes letters, instances of Author
who send letters, and instances of Receiver
who receive letters.
Each author, when instantiated, will immediately send letters to five of their friends.
DAML Model¶
This example assumes the following DAML:
daml 1.2
module Main where
-- Roles -----------------------------------------------------------------------
template PostmanRole
with
postman : Party
where
signatory postman
controller postman can
nonconsuming InviteParticipant : (ContractId InviteAuthorRole, ContractId InviteReceiverRole)
with
party : Party; address: Text
do
c <- create InviteAuthorRole with postman; author = party
d <- create InviteReceiverRole with postman; receiver = party; address
return (c, d)
template AuthorRole
with
postman : Party
author: Party
where
signatory postman
controller author can
nonconsuming CreateLetter : ContractId UnsortedLetter
with
address : Text
content : Text
do
create UnsortedLetter
with
postman
sender = author
address
content
nonconsuming CreateIntLetter : ContractId UnsortedLetter
with
address : Text
content : Int
do
create UnsortedLetter
with
postman
sender = author
address
content = (show content)
nonconsuming CreateDecimalLetter : ContractId UnsortedLetter
with
address : Text
content : Decimal
do
create UnsortedLetter
with
postman
sender = author
address
content = (show content)
nonconsuming CreateTimeLetter : ContractId UnsortedLetter
with
address : Text
content : Time
do
create UnsortedLetter
with
postman
sender = author
address
content = (show content)
nonconsuming CreateListIntLetter : ContractId UnsortedLetter
with
address : Text
content : [Int]
do
create UnsortedLetter
with
postman
sender = author
address
content = (show content)
template ReceiverRole
with
postman : Party
receiver : Party
address : Text
where
signatory postman
controller receiver can
AcceptLetter : ContractId AcknowlegedLetter
with
sentLetterCid : ContractId SentLetter
do
sentLetterCid2 <- fetch sentLetterCid
assert $ sentLetterCid2.receiver == receiver
assert $ sentLetterCid2.receiverAddress == address
create AcknowlegedLetter
with
sender = sentLetterCid2.sender
receiver
receiverAddress = address
content = sentLetterCid2.content
controller postman can
Deactivate : ()
do
assert $ postman == postman
template InviteAuthorRole
with
postman : Party
author : Party
where
signatory postman
controller author can
AcceptInviteAuthorRole : ContractId AuthorRole
do
create AuthorRole with postman; author
template InviteReceiverRole
with
postman : Party
receiver : Party
address : Text
where
signatory postman
controller receiver can
AcceptInviteReceiverRole : ContractId ReceiverRole
do
create ReceiverRole with postman; receiver; address
-- Letters ---------------------------------------------------------------------
template UnsortedLetter
with
postman : Party
sender : Party
address : Text
content : Text
where
signatory sender
controller postman can
Sort : ContractId SortedLetter
with
receiverCid : ContractId ReceiverRole
do
receiverCid2 <- fetch receiverCid
assert $ receiverCid2.address == address
assert $ receiverCid2.postman == postman
create SortedLetter
with
postman
sender
receiver = receiverCid2.receiver
receiverAddress = receiverCid2.address
content
template SortedLetter
with
postman : Party
sender : Party
receiver : Party
receiverAddress : Text
content : Text
where
signatory sender
controller postman can
Deliver : ContractId SentLetter
do
create SentLetter with sender; receiver; receiverAddress; content
template SentLetter
with
sender : Party
receiver : Party
receiverAddress : Text
content : Text
where
signatory sender
controller receiver can
AcceptSentLetter : ContractId AcknowlegedLetter
do
create AcknowlegedLetter with sender; receiver; receiverAddress; content
template AcknowlegedLetter
with
sender : Party
receiver : Party
receiverAddress : Text
content : Text
where
signatory receiver
agreement (show sender) <> " sent" <> content <> " to " <> (show receiver) <> " at " <> (show receiverAddress)
Create the Postman¶
The Postman serves as the operator of this market, and its role contract must be defined before anything else can happen:
- If you are running this code example through the SDK, it will:
start up a Ledger Sandbox in the background, point it to the above DAML model,
run the code against that Ledger Sandbox, and
stop the Ledger Sandbox.
First, a few important imports:
from os import path
from dazl import create, sandbox
from dazl.client import create_client
DAML_FILE = path.realpath(path.join(path.dirname(__file__), './Main.daml'))
POSTMAN_PARTY = 'Postman'
MEMBER_PARTY_COUNT = 10
Then the main dish:
def run_test(url):
all_parties = [POSTMAN_PARTY]
with create_client(parties=all_parties, participant_url=url) as client_mgr:
postman_client = client_mgr.new_client(POSTMAN_PARTY)
postman_client.on_ready(
lambda _, __: create('Main.PostmanRole', dict(postman=POSTMAN_PARTY)))
ledger_run = client_mgr.run_until_complete()
return ledger_run.exit_code
Lastly, the code that actually runs everything:
if __name__ == '__main__':
import sys
with sandbox(DAML_FILE) as server:
exit_code = run_test(server.url)
sys.exit(int(exit_code))
Note
dazl.sandbox()
is a helper function for creating a disposable sandbox, running
a test, and terminating the process. You wouldn’t use it when pointing to a production instance,
but it is very useful for testing. All of these examples assume that you are using a blank
ledger every single time. As you iterate through the steps of the tutorial, make sure to stop and
start the ledger each time if you’re using dazl.sandbox()
.
dazl.simple_client()
is a helper function for creating a LedgerClientManager
.
At a minimum, you must provide it a list of parties to listen as, and a URL to the Sandbox
(or Ledger Server participant node when running against a real instance).
To create a client for a specific party, call LedgerClientManager.new_client()
. There are
several key methods on it; this example introduces ParticipantLedgerClient.on_ready
, which is
called when the connection to the ledger is initialized. The parameters are ignored at this point.
The callback, like most callbacks, can return a Command
to submit to the ledger. In this
example, a Main.postmanRole
contract is to be created with one argument named postman
and a value of 'Postman'
Finally, to actually start the manager and all the clients, call
LedgerClientManager.run_until_complete
. The code should run and return an exit code of 0,
indicating that the script successfully ran. But what if you wanted to actually see what happened
to the ledger afterwards?
Inspect the Ledger¶
Using the convenience sandbox()
method makes development a bit quicker, but it is difficult to
actually see what is happening afterwards because it tears down the ledger and all of its state.
You could either start a Sandbox instance manually through the SDK, or you could output the ledger
after every run:
from dazl.plugins import LedgerCapturePlugin
def run_test(url):
all_parties = [POSTMAN_PARTY]
with create_client(parties=all_parties, participant_url=url) as client_mgr:
inspector = LedgerCapturePlugin.stdout()
try:
postman_client = client_mgr.new_client(POSTMAN_PARTY)
postman_client.on_ready(
lambda _, __: create('Main.PostmanRole', dict(postman=POSTMAN_PARTY)))
client_mgr.register(inspector)
ledger_run = client_mgr.run_until_complete()
return ledger_run.exit_code
finally:
inspector.dump_all()
We have added dazl.plugin.LedgerCapturePlugin
, which listens for events from the ledger and
stores them internally to be drawn out later. The main body of run_test
outputs the result of
the ledger in a try
/finally
block so that the ledger is always printed out, even if an
exception occurs.
dazl.plugin.LedgerCapturePlugin
exposes several class methods for easily creating an
instance; in this example, LedgerCapturePlugin
outputs its results to stdout
when
dump_all
is called.
Set up participants¶
We have now created the postman and can see that on the ledger; now we’ll add the other participants
of this market. For readability, let’s also split out all the registration methods into a separate
set_up
function so that we can keep the focus on adding listeners to the ledger:
def run_test(url):
members = [dict(party=f'Member {i}', address=address(i)) for i in
range(0, MEMBER_PARTY_COUNT)]
all_parties = [POSTMAN_PARTY] + [member['party'] for member in members]
with create_client(parties=all_parties, participant_url=url) as client_mgr:
inspector = LedgerCapturePlugin.stdout()
try:
set_up(client_mgr, members)
client_mgr.register(inspector)
ledger_run = client_mgr.run_until_complete()
return ledger_run.exit_code
finally:
inspector.dump_all()
def set_up(client_mgr, members):
postman_client = client_mgr.new_client(POSTMAN_PARTY)
postman_client.on_ready(
lambda _, __: create('Main.PostmanRole', dict(postman=POSTMAN_PARTY)))
postman_client.on_created(
'Main.PostmanRole',
lambda cid, cdata: [cid.exercise('InviteParticipant', m) for m in members])
def address(index):
return '{} Member Lane'.format(index)
The on_created
method allows you to add an event handler for templates as they are created on
the ledger. Like the on_ready
method above, you can return a Command
(or in this case, a
list
of Command
) that is to be executed in response to this event.
After running the script, you should see a few more columns in the output for all the new parties,
and you can see that the parties now see invitation contracts that they can exercise choices on. To
further progress the workflow, let’s add more callbacks in set_up
:
def set_up(client_mgr, members):
postman_client = client_mgr.new_client(POSTMAN_PARTY)
postman_client.on_ready(
lambda _, __: create('Main.PostmanRole', dict(postman=POSTMAN_PARTY)))
postman_client.on_created(
'Main.PostmanRole',
lambda cid, cdata: [cid.exercise('InviteParticipant', m) for m in members])
member_clients = [client_mgr.new_client(m['party']) for m in members]
for member_client in member_clients:
# every member automatically accepts
member_client.on_created(
'Main.InviteAuthorRole', lambda cid, cdata: cid.exercise('AcceptInviteAuthorRole'))
member_client.on_created(
'Main.InviteReceiverRole', lambda cid, cdata: cid.exercise('AcceptInviteReceiverRole'))
Now notice that the inviteAsAuthor
and inviteAsReceiver
contracts are no longer in the
output, instead replaced with authorRole
and receiverRole
contracts; that’s because the
accept
choice on these contracts is a consuming choice.
In order to respond to these contracts as other parties, we have also created new clients, one for every additional party. Now that we have a universe of participants fully set up and ready to go, let’s do some actual work.
Send some “letters” through the post office¶
Once a participant’s Main.authorRole
is created, that participant is now granted the ability to
send letters to other participants in the market.
– TBC –