From 7deca903bb14a2799be7db4df00814275255759a Mon Sep 17 00:00:00 2001 From: Moritz Kiefer Date: Tue, 19 Nov 2019 18:19:57 +0100 Subject: [PATCH] Add documentation for DAML script and bundle it in the SDK (#3527) --- .../src/DA/Daml/Assistant/IntegrationTests.hs | 6 + daml-script/daml/Daml/Script.daml | 4 + daml-script/test/BUILD.bazel | 2 + docs/BUILD.bazel | 15 +- docs/configs/pdf/index.rst | 1 + docs/source/daml-script/index.rst | 191 ++++++++++++++++++ .../template-root/daml.yaml.template | 16 ++ .../template-root/ledger-parties.json | 5 + .../template-root/src/ScriptExample.daml | 88 ++++++++ docs/source/index.rst | 1 + release/BUILD.bazel | 6 + release/sdk-config.yaml.tmpl | 4 + templates/BUILD.bazel | 4 + unreleased.rst | 2 + 14 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 docs/source/daml-script/index.rst create mode 100644 docs/source/daml-script/template-root/daml.yaml.template create mode 100644 docs/source/daml-script/template-root/ledger-parties.json create mode 100644 docs/source/daml-script/template-root/src/ScriptExample.daml diff --git a/daml-assistant/integration-tests/src/DA/Daml/Assistant/IntegrationTests.hs b/daml-assistant/integration-tests/src/DA/Daml/Assistant/IntegrationTests.hs index f02cfcaaa975..786820e67497 100644 --- a/daml-assistant/integration-tests/src/DA/Daml/Assistant/IntegrationTests.hs +++ b/daml-assistant/integration-tests/src/DA/Daml/Assistant/IntegrationTests.hs @@ -136,6 +136,12 @@ packagingTests = testGroup "packaging" withCurrentDirectory projDir $ callCommandQuiet "daml build" let dar = projDir ".daml" "dist" "copy-trigger-0.0.1.dar" assertBool "copy-trigger-0.1.0.dar was not created." =<< doesFileExist dar + , testCase "Build DAML script example" $ withTempDir $ \tmpDir -> do + let projDir = tmpDir "script-example" + callCommandQuiet $ unwords ["daml", "new", projDir, "script-example"] + withCurrentDirectory projDir $ callCommandQuiet "daml build" + let dar = projDir ".daml/dist/script-example-0.0.1.dar" + assertBool "script-example-0.0.1.dar was not created." =<< doesFileExist dar ] <> [ testCaseSteps "Build migration package" $ \step -> withTempDir $ \tmpDir -> do -- it's important that we have fresh empty directories here! diff --git a/daml-script/daml/Daml/Script.daml b/daml-script/daml/Daml/Script.daml index 51adb01beb3e..d66feaec8136 100644 --- a/daml-script/daml/Daml/Script.daml +++ b/daml-script/daml/Daml/Script.daml @@ -60,6 +60,7 @@ data QueryACS a = QueryACS , continue : [AnyTemplate] -> a } deriving Functor +-- TODO This should also return contract ids. query : forall t. Template t => Party -> Script [t] query p = Script $ Free $ Query (QueryACS p (templateTypeRep @t) (pure . map (fromSome . fromAnyTemplate))) @@ -90,6 +91,9 @@ submitMustFail p cmds = Script $ Free (fmap pure $ Submit $ SubmitCmd p (fail <$ newtype Script a = Script (Free ScriptF a) deriving (Functor, Applicative, Action) +instance CanAbort Script where + abort = error + data LedgerValue = LedgerValue {} fromLedgerValue : LedgerValue -> a diff --git a/daml-script/test/BUILD.bazel b/daml-script/test/BUILD.bazel index 168f72bfc2e1..ae8048a45f5a 100644 --- a/daml-script/test/BUILD.bazel +++ b/daml-script/test/BUILD.bazel @@ -15,6 +15,7 @@ genrule( srcs = glob(["**/*.daml"]) + [ "//daml-script/daml:daml-script.dar", + "//docs:source/daml-script/template-root/src/ScriptExample.daml", ], outs = ["script-test.dar"], cmd = """ @@ -22,6 +23,7 @@ genrule( TMP_DIR=$$(mktemp -d) mkdir -p $$TMP_DIR/daml cp -L $(location :daml/ScriptTest.daml) $$TMP_DIR/daml + cp -L $(location //docs:source/daml-script/template-root/src/ScriptExample.daml) $$TMP_DIR/daml cp -L $(location //daml-script/daml:daml-script.dar) $$TMP_DIR/ cat << EOF > $$TMP_DIR/daml.yaml sdk-version: 0.0.0 diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index a73f14296cfe..1d7268483017 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -412,4 +412,17 @@ pkg_tar( visibility = ["//visibility:public"], ) -exports_files(["source/triggers/template-root/src/CopyTrigger.daml"]) +pkg_tar( + name = "script-example-template", + srcs = glob( + ["source/daml-script/template-root/**"], + exclude = ["**/*~"], + ), + strip_prefix = "source/daml-script/template-root", + visibility = ["//visibility:public"], +) + +exports_files([ + "source/triggers/template-root/src/CopyTrigger.daml", + "source/daml-script/template-root/src/ScriptExample.daml", +]) diff --git a/docs/configs/pdf/index.rst b/docs/configs/pdf/index.rst index 8229df233b7b..79ca5410dbd3 100644 --- a/docs/configs/pdf/index.rst +++ b/docs/configs/pdf/index.rst @@ -93,6 +93,7 @@ Experimental features daml-integration-kit/index json-api/index triggers/index + daml-script/index tools/visual Support and updates diff --git a/docs/source/daml-script/index.rst b/docs/source/daml-script/index.rst new file mode 100644 index 000000000000..2ab6bb7debbf --- /dev/null +++ b/docs/source/daml-script/index.rst @@ -0,0 +1,191 @@ +.. Copyright (c) 2019 The DAML Authors. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +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 :doc:`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 :doc:`/tools/sandbox`). + +Usage +===== + +Our example for this tutorial consists of 2 templates. + +First, we have a template called ``Coin``: + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- COIN_TEMPLATE_BEGIN + :end-before: -- COIN_TEMPLATE_END + +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``: + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- COIN_PROPOSAL_TEMPLATE_BEGIN + :end-before: -- COIN_PROPOSAL_TEMPLATE_END + +``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``. + +.. literalinclude:: ./template-root/daml.yaml.template + :start-after: # script-dependencies-begin + :end-before: # script-dependencies-end + +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. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- DAML_SCRIPT_HEADER_BEGIN + :end-before: -- DAML_SCRIPT_HEADER_END + +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. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- LEDGER_PARTIES_BEGIN + :end-before: -- LEDGER_PARTIES_END + +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 ()``. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- INITIALIZE_SIGNATURE_BEGIN + :end-before: -- INITIALIZE_SIGNATURE_END + +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``. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- INITIALIZE_PROPOSAL_BEGIN + :end-before: -- INITIALIZE_PROPOSAL_END + +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``. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- INITIALIZE_ACCEPT_BEGIN + :end-before: -- INITIALIZE_ACCEPT_END + +Finally, we call ``pure ()`` on the last line of our script to match +the type ``Script ()``. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- INITIALIZE_PURE_BEGIN + :end-before: -- INITIALIZE_PURE_END + +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. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- TEST_SIGNATURE_BEGIN + :end-before: -- TEST_SIGNATURE_END + +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. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- TEST_ALLOCATE_BEGIN + :end-before: -- TEST_ALLOCATE_END + +We now call the ``initialize`` function that we defined before on the +parties that we have just allocated. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- TEST_INITIALIZE_BEGIN + :end-before: -- TEST_INITIALIZE_END + +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``. + +.. literalinclude:: ./template-root/src/ScriptExample.daml + :language: daml + :start-after: -- TEST_QUERIES_BEGIN + :end-before: -- TEST_QUERIES_END + +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 :doc:`/json-api/lf-value-specification`. + +.. literalinclude:: ./template-root/ledger-parties.json + :language: daml + +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. diff --git a/docs/source/daml-script/template-root/daml.yaml.template b/docs/source/daml-script/template-root/daml.yaml.template new file mode 100644 index 000000000000..347cf35ae9d2 --- /dev/null +++ b/docs/source/daml-script/template-root/daml.yaml.template @@ -0,0 +1,16 @@ +sdk-version: __VERSION__ +name: script-example +source: src +parties: + - Alice + - Bob + - Bank +version: 0.0.1 +# script-dependencies-begin +dependencies: + - daml-prim + - daml-stdlib + - daml-script +# script-dependencies-end +build-options: + - --target=1.7 diff --git a/docs/source/daml-script/template-root/ledger-parties.json b/docs/source/daml-script/template-root/ledger-parties.json new file mode 100644 index 000000000000..ea2fa96716a6 --- /dev/null +++ b/docs/source/daml-script/template-root/ledger-parties.json @@ -0,0 +1,5 @@ +{ + "alice": "Alice", + "bob": "Bob", + "bank": "Bank" +} diff --git a/docs/source/daml-script/template-root/src/ScriptExample.daml b/docs/source/daml-script/template-root/src/ScriptExample.daml new file mode 100644 index 000000000000..5488c85d7275 --- /dev/null +++ b/docs/source/daml-script/template-root/src/ScriptExample.daml @@ -0,0 +1,88 @@ +-- Copyright (c) 2019 The DAML Authors. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- DAML_SCRIPT_HEADER_BEGIN +{-# LANGUAGE ApplicativeDo #-} +daml 1.2 +module ScriptExample where +import Prelude hiding (submit, submitMustFail) +import Daml.Script +-- DAML_SCRIPT_HEADER_END + +import DA.Assert + +-- COIN_TEMPLATE_BEGIN +template Coin + with + issuer : Party + owner : Party + where + signatory issuer, owner +-- COIN_TEMPLATE_END + +-- COIN_PROPOSAL_TEMPLATE_BEGIN +template CoinProposal + with + coin : Coin + where + signatory coin.issuer + observer coin.owner + + choice Accept : ContractId Coin + controller coin.owner + do create coin +-- COIN_PROPOSAL_TEMPLATE_END + +-- LEDGER_PARTIES_BEGIN +data LedgerParties = LedgerParties with + bank : Party + alice : Party + bob : Party +-- LEDGER_PARTIES_END + +-- INITIALIZE_SIGNATURE_BEGIN +initialize : LedgerParties -> Script () +initialize parties = do +-- INITIALIZE_SIGNATURE_END +-- INITIALIZE_PROPOSAL_BEGIN + (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) +-- INITIALIZE_PROPOSAL_END + +-- INITIALIZE_ACCEPT_BEGIN + coinAlice <- submit parties.alice $ exerciseCmd coinProposalAlice Accept + coinBob <- submit parties.bob $ exerciseCmd coinProposalBob Accept +-- INITIALIZE_ACCEPT_END +-- INITIALIZE_PURE_BEGIN + pure () +-- INITIALIZE_PURE_END + +-- TEST_SIGNATURE_BEGIN +test : Script () +test = do +-- TEST_SIGNATURE_END +-- TEST_ALLOCATE_BEGIN + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + bank <- allocateParty "Bank" + let parties = LedgerParties bank alice bob +-- TEST_ALLOCATE_END + +-- TEST_INITIALIZE_BEGIN + initialize parties +-- TEST_INITIALIZE_END + +-- TEST_QUERIES_BEGIN + proposals <- query @CoinProposal bank + assertEq [CoinProposal (Coin bank bank)] proposals + + aliceCoins <- query @Coin alice + assertEq [Coin bank alice] aliceCoins + + bobCoins <- query @Coin bob + assertEq [Coin bank bob] bobCoins +-- TEST_QUERIES_END + diff --git a/docs/source/index.rst b/docs/source/index.rst index cc43ab4b186d..551c76ff5355 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -98,6 +98,7 @@ DAML SDK documentation migrate/index json-api/index DAML Triggers + DAML Script tools/visual .. toctree:: diff --git a/release/BUILD.bazel b/release/BUILD.bazel index 28e9f1acee3f..daaeb355f5dd 100644 --- a/release/BUILD.bazel +++ b/release/BUILD.bazel @@ -74,7 +74,9 @@ genrule( "//language-support/codegen-main:codegen-main_deploy.jar", "//templates:templates-tarball.tar.gz", "//triggers/daml:daml-trigger.dar", + "//daml-script/daml:daml-script.dar", "//triggers/runner:trigger-runner_deploy.jar", + "//daml-script/runner:script-runner_deploy.jar", ], outs = ["sdk-release-tarball.tar.gz"], cmd = """ @@ -99,6 +101,7 @@ genrule( mkdir -p $$OUT/daml-libs cp -t $$OUT/daml-libs $(location //triggers/daml:daml-trigger.dar) + cp -t $$OUT/daml-libs $(location //daml-script/daml:daml-script.dar) mkdir -p $$OUT/daml-helper tar xf $(location //daml-assistant/daml-helper:daml-helper-dist) --strip-components=1 -C $$OUT/daml-helper @@ -128,6 +131,9 @@ genrule( mkdir -p $$OUT/daml-trigger cp $(location //triggers/runner:trigger-runner_deploy.jar) $$OUT/daml-trigger/daml-trigger.jar + mkdir -p $$OUT/daml-script + cp $(location //daml-script/runner:script-runner_deploy.jar) $$OUT/daml-script/daml-script.jar + tar zcf $(location sdk-release-tarball.tar.gz) --format=ustar $$OUT """, visibility = ["//visibility:public"], diff --git a/release/sdk-config.yaml.tmpl b/release/sdk-config.yaml.tmpl index 076ea643f313..fae6fcfe05ad 100644 --- a/release/sdk-config.yaml.tmpl +++ b/release/sdk-config.yaml.tmpl @@ -69,3 +69,7 @@ commands: path: daml-helper/daml-helper args: ["run-jar", "daml-trigger/daml-trigger.jar"] desc: "Run a DAML trigger (experimental)" +- name: script + path: daml-helper/daml-helper + args: ["run-jar", "daml-script/daml-script.jar"] + desc: "Run a DAML trigger (experimental)" diff --git a/templates/BUILD.bazel b/templates/BUILD.bazel index 5b2ef0d2ac36..d71f3343fa09 100644 --- a/templates/BUILD.bazel +++ b/templates/BUILD.bazel @@ -14,6 +14,7 @@ genrule( "//docs:quickstart-java.tar.gz", "//docs:daml-intro-templates", "//docs:copy-trigger-template", + "//docs:script-example-template", "//language-support/scala/examples:quickstart-scala-dir", ], outs = ["templates-tarball.tar.gz"], @@ -47,6 +48,9 @@ genrule( mkdir -p $$OUT/copy-trigger tar xf $(location //docs:copy-trigger-template) -C $$OUT/copy-trigger + mkdir -p $$OUT/script-example + tar xf $(location //docs:script-example-template) -C $$OUT/script-example + tar zcf $(location :templates-tarball.tar.gz) templates-tarball """, visibility = ["//visibility:public"], diff --git a/unreleased.rst b/unreleased.rst index bc910b3de97a..83be1fbb7c1e 100644 --- a/unreleased.rst +++ b/unreleased.rst @@ -35,3 +35,5 @@ HEAD — ongoing This configuration is NOT recommended for production deployment. See issue #2782. - [Extractor] The app can now work against a Ledger API server that requires client authentication. See `issue #3157 `__. +- [DAML Script] This release contains a first version of an experimental DAML script + feature that provides a scenario-like API that is run against an actual ledger.