dazl

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:
  1. start up a Ledger Sandbox in the background, point it to the above DAML model,

  2. run the code against that Ledger Sandbox, and

  3. 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 –