DAML Script

WARNING: DAML Script is an experimental feature that is actively being designed and is subject to breaking changes. We welcome feedback about DAML script on our issue tracker or on Slack.

DAML scenarios provide a simple API for experimenting with DAML models and getting quick feedback in DAML studio. However, scenarios are run in a special process and do not interact with an actual ledger. This means that you cannot use scenarios to test other ledger clients, e.g., your UI or DAML triggers.

DAML script addresses this problem by providing you with an API with the simplicity of DAML scenarios and all the benefits such as being able to reuse your DAML types and logic while running against an actual ledger. This means that you can use it to test automation logic, your UI but also for ledger initialization where scenarios cannot be used (with the exception of DAML Sandbox).

Usage

Our example for this tutorial consists of 2 templates.

First, we have a template called Coin:

template Coin
  with
    issuer : Party
    owner : Party
  where
    signatory issuer, owner

This template represents a coin issued to owner by issuer. Coin has both the owner and the issuer as signatories.

Second, we have a template called CoinProposal:

template CoinProposal
  with
    coin : Coin
  where
    signatory coin.issuer
    observer coin.owner

    choice Accept : ContractId Coin
      controller coin.owner
      do create coin

CoinProposal is only signed by the issuer and it provides a single Accept choice which, when exercised by the controller will create the corresponding Coin.

Having defined the templates, we can now move on to write DAML scripts that operate on these templates. To get accees to the API used to implement DAML scripts, you need to add the daml-script library to the dependencies field in daml.yaml.

dependencies:
  - daml-prim
  - daml-stdlib
  - daml-script

In addition to that you also need to import the Daml.Script module and since DAML script provides submit and submitMustFail functions that collide with the ones used in scenarios, we need to hide those. We also enable the ApplicativeDo extension. We will see below why this is useful.

{-# LANGUAGE ApplicativeDo #-}
daml 1.2
module ScriptExample where
import Prelude hiding (submit, submitMustFail)
import Daml.Script

Since on an actual ledger parties cannot be arbitrary strings, we define a record containing all the parties that we will use in our script so that we can easily swap them out.

data LedgerParties = LedgerParties with
  bank : Party
  alice : Party
  bob : Party

Let us now write a function to initialize the ledger with 3 CoinProposal``s and accept 2 of them. This function takes the ``LedgerParties as an argument and return something of type Script () which is DAML script’s equivalent of Scenario ().

initialize : LedgerParties -> Script ()
initialize parties = do

First we create the proposals. To do so, we use the submit function to submit a transaction. The first argument is the party submitting the transaction. In our case, we want all proposals to be created by the bank so we use parties.bank. The second argument must be of type Commands a so in our case Commands (ContractId CoinProposal, ContractId CoinProposal, ContractId CoinProposal) corresponding to the 3 proposals that we create. Commands is similar to Update which is used in the submit function in scenarios. However, Commands requires that the individual commands do not depend on each other. This matches the restriction on the Ledger API where a transaction consists of a list of commands. Using ApplicativeDo we can still use do-notation as long as we respect this. In Commands we use createCmd instead of create and exerciseCmd instead of exercise.

  (coinProposalAlice, coinProposalBob, coinProposalBank) <- submit parties.bank $ do
    coinProposalAlice <- createCmd (CoinProposal (Coin parties.bank parties.alice))
    coinProposalBob <- createCmd (CoinProposal (Coin parties.bank parties.bob))
    coinProposalBank <- createCmd (CoinProposal (Coin parties.bank parties.bank))
    pure (coinProposalAlice, coinProposalBob, coinProposalBank)

Now that we have created the CoinProposal``s, we want ``Alice and Bob to accept the proposal while the Bank will ignore the proposal that it has created for itself. To do so we use separate submit statements for Alice and Bob and call exerciseCmd.

  coinAlice <- submit parties.alice $ exerciseCmd coinProposalAlice Accept
  coinBob <- submit parties.bob $ exerciseCmd coinProposalBob Accept

Finally, we call pure () on the last line of our script to match the type Script ().

  pure ()

We have now defined a way to initialize the ledger so we can write a test that checks that the contracts that we expect exist afterwards.

First, we define the signature of our test. We will create the parties used here in the test, so it does not take any arguments.

test : Script ()
test = do

Now, we create the parties using the allocateParty function. This uses the party management service to create new parties with the given display name. Note that the display name does not identify a party uniquely. If you call allocateParty twice with the same display name, it will create 2 different parties. This is very convenient for testing since a new party cannot see any old contracts on the ledger so using new parties for each test removes the need to reset the ledger.

  alice <- allocateParty "Alice"
  bob <- allocateParty "Bob"
  bank <- allocateParty "Bank"
  let parties = LedgerParties bank alice bob

We now call the initialize function that we defined before on the parties that we have just allocated.

  initialize parties

To verify the contracts on the ledger, we use the query function. We pass it the type of the template and a party. It will then give us all active contracts of the given type visible to the party. In our example, we expect to see one active CoinProposal for bank and one Coin contract for each of Alice and Bob. We get back list of (ContractId t, t) pairs from query. In our tests, we do not need the contract ids, so we throw them away using map snd.

  proposals <- query @CoinProposal bank
  assertEq [CoinProposal (Coin bank bank)] (map snd proposals)

  aliceCoins <- query @Coin alice
  assertEq [Coin bank alice] (map snd aliceCoins)

  bobCoins <- query @Coin bob
  assertEq [Coin bank bob] (map snd bobCoins)

To run our script, we first build it with daml build and then run it by pointing to the DAR, the name of our script and the host and port our ledger is running on.

daml script --dar .daml/dist/script-example-0.0.1.dar --script-name ScriptExample:test --ledger-host localhost --ledger-port 6865

Up to now, we have worked with parties that we have allocated in the test. We can also pass in the path to a file containing the input in the DAML-LF JSON Encoding.

{
  "alice": "Alice",
  "bob": "Bob",
  "bank": "Bank"
}

We can then initialize our ledger passing in the json file via --input-file.

daml script daml script --dar .daml/dist/script-example-0.0.1.dar --script-name ScriptExample:initialize --ledger-host localhost --ledger-port 6865 --input-file ledger-parties.json

If you open Navigator, you can now see the contracts that have been created.

Using DAML Script in Distributed Topologies

So far, we have run DAML script against a single participant node. It is also more possible to run it in a setting where different parties are hosted on different participant nodes. To do so, pass the --participant-config participants.json file to daml script instead of --ledger-host and ledger-port. The file should be of the format

{
    "default_participant": {"host": "localhost", "port": 6866},
    "participants": {
        "one": {"host": "localhost", "port": 6865}
    },
    "party_participants": {"alice": "one"}
}

This will define a participant called one, a default participant and it defines that the party alice is on participant one. Whenever you submit something as party, we will use the participant for that party or if none is specified default_participant. If default_participant is not specified, using a party with an unspecified participant is an error.

allocateParty will also use the default_participant. If you want to allocate a party on a specific participant, you can use allocatePartyOn which accepts the participant name as an extra argument.