mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-11 04:45:57 +03:00
rewrite trigger docs to follow gsg (#10509)
* rewrite trigger docs to follow gsg Per #10419 point 4, I've rewritten the Triggers section to build upon the Getting Started Guide instead of inventing its own example. Compared to #10395, this has a lot more explanations as this page must now serve the dual purpose of being a possible "next step" from the GSG and being the main reference page for triggers. It's also lost the "next steps" section, which I think is a bit of a shame, but it doesn't really make sense here. There's also no easy way for people not interested in the GSG to follow along; should we expose the "completed GSG" as a tempate? CHANGELOG_BEGIN CHANGELOG_END * keep copy-trigger as a template * fix copy-trigger project name * make up gsg-trigger template * remove awkward sentence, fix existing typo * update code to use when{,Some} * add to * swap emitCommands and getCommandsInFlight * typo * insist on state-correction perspective * fix copy-trigger tests * add back copy-trigger to whitelist * add gsg-trigger to whitelist
This commit is contained in:
parent
aec601f929
commit
f8c0a35940
@ -354,7 +354,7 @@ argWhitelist = S.fromList
|
|||||||
, "install", "latest", "project"
|
, "install", "latest", "project"
|
||||||
, "uninstall"
|
, "uninstall"
|
||||||
, "studio", "never", "always", "published"
|
, "studio", "never", "always", "published"
|
||||||
, "new", "skeleton", "empty-skeleton", "quickstart-java", "quickstart-scala", "copy-trigger"
|
, "new", "skeleton", "empty-skeleton", "quickstart-java", "quickstart-scala", "copy-trigger", "gsg-trigger"
|
||||||
, "daml-intro-1", "daml-intro-2", "daml-intro-3", "daml-intro-4"
|
, "daml-intro-1", "daml-intro-2", "daml-intro-3", "daml-intro-4"
|
||||||
, "daml-intro-5", "daml-intro-6", "daml-intro-7", "script-example"
|
, "daml-intro-5", "daml-intro-6", "daml-intro-7", "script-example"
|
||||||
, "migrate"
|
, "migrate"
|
||||||
|
@ -256,13 +256,13 @@ packagingTests tmpDir =
|
|||||||
let projDir = tmpDir </> "copy-trigger1"
|
let projDir = tmpDir </> "copy-trigger1"
|
||||||
callCommandSilent $ unwords ["daml", "new", projDir, "--template=copy-trigger"]
|
callCommandSilent $ unwords ["daml", "new", projDir, "--template=copy-trigger"]
|
||||||
callCommandSilentIn projDir "daml build"
|
callCommandSilentIn projDir "daml build"
|
||||||
let dar = projDir </> ".daml" </> "dist" </> "copy-trigger-0.0.1.dar"
|
let dar = projDir </> ".daml" </> "dist" </> "copy-trigger1-0.0.1.dar"
|
||||||
assertFileExists dar
|
assertFileExists dar
|
||||||
, testCase "Build copy trigger with LF version 1.dev" $ do
|
, testCase "Build copy trigger with LF version 1.dev" $ do
|
||||||
let projDir = tmpDir </> "copy-trigger2"
|
let projDir = tmpDir </> "copy-trigger2"
|
||||||
callCommandSilent $ unwords ["daml", "new", projDir, "--template=copy-trigger"]
|
callCommandSilent $ unwords ["daml", "new", projDir, "--template=copy-trigger"]
|
||||||
callCommandSilentIn projDir "daml build --target 1.dev"
|
callCommandSilentIn projDir "daml build --target 1.dev"
|
||||||
let dar = projDir </> ".daml" </> "dist" </> "copy-trigger-0.0.1.dar"
|
let dar = projDir </> ".daml" </> "dist" </> "copy-trigger2-0.0.1.dar"
|
||||||
assertFileExists dar
|
assertFileExists dar
|
||||||
, testCase "Build trigger with extra dependency" $ do
|
, testCase "Build trigger with extra dependency" $ do
|
||||||
let myDepDir = tmpDir </> "mydep"
|
let myDepDir = tmpDir </> "mydep"
|
||||||
@ -790,6 +790,7 @@ templateTests = testGroup "templates" $
|
|||||||
-- this should be good enough.
|
-- this should be good enough.
|
||||||
where templateNames =
|
where templateNames =
|
||||||
[ "copy-trigger"
|
[ "copy-trigger"
|
||||||
|
, "gsg-trigger"
|
||||||
-- daml-intro-1 - daml-intro-6 are not full projects.
|
-- daml-intro-1 - daml-intro-6 are not full projects.
|
||||||
, "daml-intro-7"
|
, "daml-intro-7"
|
||||||
, "daml-patterns"
|
, "daml-patterns"
|
||||||
|
@ -241,6 +241,11 @@ genrule(
|
|||||||
mkdir $$CODE_DIR/testing
|
mkdir $$CODE_DIR/testing
|
||||||
cp $(location //templates:create-daml-app-test-resources/index.test.ts) $$CODE_DIR/testing
|
cp $(location //templates:create-daml-app-test-resources/index.test.ts) $$CODE_DIR/testing
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
TEMPLATES_DIR=$$PWD/build/docs/source/_templates
|
||||||
|
mkdir -p $$TEMPLATES_DIR
|
||||||
|
tar -zxf $(location //templates:templates-tarball) -C $$TEMPLATES_DIR --strip-components=1
|
||||||
|
|
||||||
if ! docs/scripts/check-closing-quotes.sh . docs/scripts/check-closing-quotes.sh.allow; then
|
if ! docs/scripts/check-closing-quotes.sh . docs/scripts/check-closing-quotes.sh.allow; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@ -630,16 +635,6 @@ pkg_tar(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
)
|
)
|
||||||
|
|
||||||
pkg_tar(
|
|
||||||
name = "copy-trigger-template",
|
|
||||||
srcs = glob(
|
|
||||||
["source/triggers/template-root/**"],
|
|
||||||
exclude = ["**/*~"],
|
|
||||||
),
|
|
||||||
strip_prefix = "source/triggers/template-root",
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
)
|
|
||||||
|
|
||||||
pkg_tar(
|
pkg_tar(
|
||||||
name = "script-example-template",
|
name = "script-example-template",
|
||||||
srcs = glob(
|
srcs = glob(
|
||||||
@ -651,6 +646,5 @@ pkg_tar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
exports_files([
|
exports_files([
|
||||||
"source/triggers/template-root/src/CopyTrigger.daml",
|
|
||||||
"source/daml-script/template-root/src/ScriptExample.daml",
|
"source/daml-script/template-root/src/ScriptExample.daml",
|
||||||
])
|
])
|
||||||
|
@ -8,12 +8,14 @@ set -eou pipefail
|
|||||||
SCRIPT_DIR=$PWD/$(dirname "$0")
|
SCRIPT_DIR=$PWD/$(dirname "$0")
|
||||||
cd $SCRIPT_DIR
|
cd $SCRIPT_DIR
|
||||||
BUILD_DIR=$(cd ..; pwd)/build
|
BUILD_DIR=$(cd ..; pwd)/build
|
||||||
|
BAZEL_BIN="$SCRIPT_DIR/../../bazel-bin"
|
||||||
|
|
||||||
trap cleanup 1 2 3 6
|
trap cleanup 1 2 3 6
|
||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
{
|
{
|
||||||
echo "Caught Signal ... cleaning up."
|
echo "Caught Signal ... cleaning up."
|
||||||
|
rm -rf $TEMPLATES_DIR/
|
||||||
rm -rf $BUILD_DIR
|
rm -rf $BUILD_DIR
|
||||||
cd $SCRIPT_DIR
|
cd $SCRIPT_DIR
|
||||||
rm -rf ../source/getting-started/code
|
rm -rf ../source/getting-started/code
|
||||||
@ -44,6 +46,12 @@ bazel build //templates:create-daml-app-docs
|
|||||||
mkdir -p $BUILD_DIR/source/getting-started/code/templates-tarball
|
mkdir -p $BUILD_DIR/source/getting-started/code/templates-tarball
|
||||||
tar -zxf ../../bazel-bin/templates/create-daml-app-docs.tar.gz -C $BUILD_DIR/source/getting-started/code/templates-tarball/
|
tar -zxf ../../bazel-bin/templates/create-daml-app-docs.tar.gz -C $BUILD_DIR/source/getting-started/code/templates-tarball/
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
bazel build //templates:templates-tarball
|
||||||
|
TEMPLATES_DIR=$BUILD_DIR/source/_templates
|
||||||
|
mkdir -p $TEMPLATES_DIR
|
||||||
|
tar -zxf $BAZEL_BIN/templates/templates-tarball.tar.gz -C $TEMPLATES_DIR --strip-components=1
|
||||||
|
|
||||||
for arg in "$@"
|
for arg in "$@"
|
||||||
do
|
do
|
||||||
if [ "$arg" = "--pdf" ]; then
|
if [ "$arg" = "--pdf" ]; then
|
||||||
|
@ -10,7 +10,7 @@ Trigger Service
|
|||||||
./authorization
|
./authorization
|
||||||
./auth0_example
|
./auth0_example
|
||||||
|
|
||||||
The :ref:`running-a-daml-trigger` documentation shows a simple method using the ``daml trigger`` command to arrange for the execution of a single trigger. Using this method, a dedicated process is launched to host the trigger.
|
The :ref:`running-a-no-op-trigger` section shows a simple method using the ``daml trigger`` command to arrange for the execution of a single trigger. Using this method, a dedicated process is launched to host the trigger.
|
||||||
|
|
||||||
Complex workflows can require running many triggers for many parties and at a certain point, use of ``daml trigger`` with its process per trigger model becomes unwieldy. The Trigger Service provides the means to host multiple triggers for multiple parties running against a common ledger in a single process and provides a convenient interface for starting, stopping and monitoring them.
|
Complex workflows can require running many triggers for many parties and at a certain point, use of ``daml trigger`` with its process per trigger model becomes unwieldy. The Trigger Service provides the means to host multiple triggers for multiple parties running against a common ledger in a single process and provides a convenient interface for starting, stopping and monitoring them.
|
||||||
|
|
||||||
|
@ -14,81 +14,89 @@ and the UI, Daml applications often need to automate certain
|
|||||||
interactions with the ledger. This is commonly done in the form of a
|
interactions with the ledger. This is commonly done in the form of a
|
||||||
ledger client that listens to the transaction stream of the ledger and
|
ledger client that listens to the transaction stream of the ledger and
|
||||||
when certain conditions are met, e.g., when a template of a given type
|
when certain conditions are met, e.g., when a template of a given type
|
||||||
has been created, the client sends commands to the ledger, e.g., it
|
has been created, the client sends commands to the ledger to
|
||||||
creates a template of another type.
|
create a template of another type.
|
||||||
|
|
||||||
It is possible to write these clients in a language of your choice,
|
It is possible to write these clients in a language of your choice,
|
||||||
e.g., JavaScript, using the HTTP JSON API. However, that introduces an
|
such as JavaScript, using the HTTP JSON API. However, that introduces an
|
||||||
additional layer of friction: You now need to translate between the
|
additional layer of friction: you now need to translate between the
|
||||||
template and choice types in Daml and a representation of those Daml
|
template and choice types in Daml and a representation of those Daml
|
||||||
types in the language you are using for your client. Daml triggers
|
types in the language you are using for your client. Daml triggers
|
||||||
address this problem by allowing you to write certain kinds of
|
address this problem by allowing you to write certain kinds of
|
||||||
automation directly in Daml reusing all the Daml types and logic that
|
automation directly in Daml, reusing all the Daml types and logic that
|
||||||
you have already defined. Note that while the logic for Daml triggers
|
you have already defined. Note that, while the logic for Daml triggers
|
||||||
is written in Daml, they act like any other ledger client: They are
|
is written in Daml, they act like any other ledger client: they are
|
||||||
executed separately from the ledger, they do not need to be uploaded
|
executed separately from the ledger, they do not need to be uploaded
|
||||||
to the ledger and they do not allow you to do anything that any other
|
to the ledger and they do not allow you to do anything that any other
|
||||||
ledger client could not do.
|
ledger client could not do.
|
||||||
|
|
||||||
Usage
|
|
||||||
=====
|
|
||||||
|
|
||||||
Our example for this tutorial consists of 3 templates.
|
If you don't want to follow along, but still want to get the final code
|
||||||
|
for this section to play with, you can get it by running::
|
||||||
|
|
||||||
First, we have a template called ``Original``:
|
daml new --template-name=gsg-trigger create-daml-app
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
How To Think About Triggers
|
||||||
:language: daml
|
===========================
|
||||||
:start-after: -- ORIGINAL_TEMPLATE_BEGIN
|
|
||||||
:end-before: -- ORIGINAL_TEMPLATE_END
|
|
||||||
|
|
||||||
This template has an ``owner``, a ``name`` that identifies it and some
|
It is tempting to think of Daml Triggers as snippets of code that "react to
|
||||||
``textdata`` that we just represent as ``Text`` to keep things simple. We
|
ledger events". However, this is not the best way to think about them; while it
|
||||||
have also added a contract key to ensure that each owner can only have
|
will work in some cases, in many corner cases that line of thought will lead to
|
||||||
one ``Original`` with a given ``name``.
|
subtle errors.
|
||||||
|
|
||||||
Second, we have a template called ``Subscriber``:
|
Instead, you should think of, and write, your triggers from the perspective of
|
||||||
|
"correcting the current ACS" to match some predefined expectations. Trigger
|
||||||
|
rules should be a combination of checking those expectations on the current ACS
|
||||||
|
and applyin corrective actions to bring back the ACS in line with its expected
|
||||||
|
state.
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
The "trigger" part is best thought of as an optimization: rather than check the
|
||||||
:language: daml
|
ACS constantly, we only apply our rules when something happens that we believe
|
||||||
:start-after: -- SUBSCRIBER_TEMPLATE_BEGIN
|
_may_ lead to the state of the ledger diverging from our expectations.
|
||||||
:end-before: -- SUBSCRIBER_TEMPLATE_END
|
|
||||||
|
|
||||||
This template allows the ``subscriber`` to subscribe to ``Original`` s where ``subscribedTo`` is the ``owner``.
|
Sample Trigger
|
||||||
For each of these ``Original`` s, our Daml trigger should then automatically create an instance of
|
==============
|
||||||
third template called ``Copy``:
|
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
Our example for this tutorial builds upon the Getting Started Guide,
|
||||||
:language: daml
|
specifically picking up right after the :doc:`/getting-started/first-feature`
|
||||||
:start-after: -- COPY_TEMPLATE_BEGIN
|
section.
|
||||||
:end-before: -- COPY_TEMPLATE_END
|
|
||||||
|
|
||||||
Our trigger should also ensure that the ``Copy`` contracts stay in sync with changes on the ledger. That means
|
We assume that our requirements are to build a chatbot that reponds to every
|
||||||
that we need to archive ``Copy`` contracts if there is more than one for the same ``Original``, we need to archive
|
message with:
|
||||||
``Copy`` contracts if the corresponding ``Original`` has been archived and we need to archive
|
|
||||||
all ``Copy`` s for a given subscriber if the corresponding ``Subscriber`` contract has been archived.
|
|
||||||
|
|
||||||
Implementing a Daml Trigger
|
"Please, tell me more about that."
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Having defined what our Daml trigger is supposed to do, we can now
|
That should fool anyone and pass the Turing test, easily.
|
||||||
move on to its implementation. A Daml trigger is a regular Daml
|
|
||||||
project that you can build using ``daml build``. To get access to the
|
|
||||||
API used to build a trigger, you need to add the ``daml-triggers``
|
|
||||||
library to the ``dependencies`` field in ``daml.yaml``.
|
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/daml.yaml.template
|
As explained above, while the layman description may be "responds to every
|
||||||
:start-after: # trigger-dependencies-begin
|
message", our technical description is better phrased as "ensure that, at all
|
||||||
:end-before: # trigger-dependencies-end
|
times, the last message we can see has been sent by us; if that is not the
|
||||||
|
case, the corrective action is to send a response to the last message we can
|
||||||
|
see".
|
||||||
|
|
||||||
|
Daml Trigger Basics
|
||||||
|
===================
|
||||||
|
|
||||||
|
A Daml trigger is a regular Daml project that you can build using ``daml
|
||||||
|
build``. To get access to the API used to build a trigger, you need to add the
|
||||||
|
``daml-trigger`` library to the ``dependencies`` field in ``daml.yaml``:
|
||||||
|
|
||||||
|
.. literalinclude:: /_templates/gsg-trigger/daml.yaml.template
|
||||||
|
:language: yaml
|
||||||
|
:start-after: # trigger-dependencies-begin
|
||||||
|
:end-before: # trigger-dependencies-end
|
||||||
|
|
||||||
|
**Note**: In the specific case of the Getting Started Guide, this is already
|
||||||
|
included as part of the ``create-daml-app`` template.
|
||||||
|
|
||||||
In addition to that you also need to import the ``Daml.Trigger``
|
In addition to that you also need to import the ``Daml.Trigger``
|
||||||
module.
|
module in your own code.
|
||||||
|
|
||||||
Daml triggers automatically track the active contract set (ACS), i.e., the set of contracts
|
Daml triggers automatically track the active contract set (ACS), i.e., the set of contracts
|
||||||
that have been created and have not been archived, and the
|
that have been created and have not been archived, and the
|
||||||
commands in flight for you. In addition to that, they allow you to
|
commands in flight for you. In addition to that, they allow you to
|
||||||
have user-defined state that is updated based on new transactions and
|
have user-defined state that is updated based on new transactions and
|
||||||
command completions. For our copy trigger, the ACS is sufficient, so
|
command completions. For our chatbot trigger, the ACS is sufficient, so
|
||||||
we will simply use ``()`` as the type of the user defined state.
|
we will simply use ``()`` as the type of the user defined state.
|
||||||
|
|
||||||
To create a trigger you need to define a value of type ``Trigger s`` where ``s`` is the type of your user-defined state:
|
To create a trigger you need to define a value of type ``Trigger s`` where ``s`` is the type of your user-defined state:
|
||||||
@ -103,6 +111,10 @@ To create a trigger you need to define a value of type ``Trigger s`` where ``s``
|
|||||||
, heartbeat : Optional RelTime
|
, heartbeat : Optional RelTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
To clarify, this is the definition in the ``Daml.Trigger`` library, reproduced
|
||||||
|
here for illustration purposes. This is not something you need to add to your
|
||||||
|
own code.
|
||||||
|
|
||||||
The ``initialize`` function is called on startup and allows you to
|
The ``initialize`` function is called on startup and allows you to
|
||||||
initialize your user-defined state based on querying the active contract
|
initialize your user-defined state based on querying the active contract
|
||||||
set.
|
set.
|
||||||
@ -125,151 +137,265 @@ Like ``Scenario`` or ``Update``, you can use ``do`` notation and
|
|||||||
We can specify the templates that our trigger will operate
|
We can specify the templates that our trigger will operate
|
||||||
on. In our case, we will simply specify ``AllInDar`` which means that
|
on. In our case, we will simply specify ``AllInDar`` which means that
|
||||||
the trigger will receive events for all template types defined in the
|
the trigger will receive events for all template types defined in the
|
||||||
DAR. It is also possible to specify an explicit list of templates,
|
DAR. It is also possible to specify an explicit list of templates. For example,
|
||||||
e.g., ``RegisteredTemplates [registeredTemplate @Original, registeredTemplate @Subscriber, registeredTemplate @Copy]``.
|
to specify just the ``Message`` template, one would write:
|
||||||
|
|
||||||
|
.. code-block:: daml
|
||||||
|
|
||||||
|
...
|
||||||
|
registeredTemplates = RegisteredTemplates [registeredTemplate @Message],
|
||||||
|
...
|
||||||
|
|
||||||
This is mainly useful for performance reasons if your DAR contains many templates that are not relevant for your trigger.
|
This is mainly useful for performance reasons if your DAR contains many templates that are not relevant for your trigger.
|
||||||
|
|
||||||
Finally, you can specify an optional heartbeat interval at which the trigger
|
Finally, you can specify an optional heartbeat interval at which the trigger
|
||||||
will be sent a ``MHeartbeat`` message. This is useful if you want to ensure
|
will be sent a ``MHeartbeat`` message. This is useful if you want to ensure
|
||||||
that the trigger is executed at a certain rate to issue timed commands.
|
that the trigger is executed at a certain rate to issue timed commands. We will
|
||||||
|
not be using heartbeats in this example.
|
||||||
|
|
||||||
For our Daml trigger, the definition looks as follows:
|
.. _running-a-no-op-trigger:
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
Running a No-Op Trigger
|
||||||
:language: daml
|
=======================
|
||||||
:start-after: -- TRIGGER_BEGIN
|
|
||||||
:end-before: -- TRIGGER_END
|
|
||||||
|
|
||||||
Now we can move on to the most complex part of our Daml trigger, the implementation of ``copyRule``.
|
To implement a no-op trigger, one could write the following in a separate
|
||||||
First let’s take a look at the signature:
|
``daml/ChatBot.daml`` file:
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
.. literalinclude:: /_templates/gsg-trigger/daml/NoOp.daml
|
||||||
:language: daml
|
:language: daml
|
||||||
:start-after: -- RULE_SIGNATURE_BEGIN
|
|
||||||
:end-before: -- RULE_SIGNATURE_END
|
|
||||||
|
|
||||||
We will need the party and the ACS to get the ``Original`` contracts
|
In the context of the Getting Started app, if you write the above file, then
|
||||||
where we are the owner, the ``Subscriber`` contracts where we are in
|
run ``daml start`` and ``npm start`` as usual, and then set up the trigger
|
||||||
the ``subscribedTo`` field and the ``Copy`` contracts where we are the
|
with:
|
||||||
``owner`` of the corresponding ``Original``.
|
|
||||||
|
|
||||||
The commands in flight, retrievable with ``getCommandsInFlight``, will
|
.. code-block:: bash
|
||||||
be useful to avoid sending the same command multiple times if
|
|
||||||
``copyRule`` is run multiple times before we get the corresponding
|
|
||||||
transaction. Note that Daml triggers are expected to be designed such
|
|
||||||
that they can cope with this, e.g., after a restart or a crash where the
|
|
||||||
commands in flight do not contain commands in flight from before the
|
|
||||||
restart, so this is an optimization rather than something required for
|
|
||||||
them to function correctly.
|
|
||||||
|
|
||||||
First, we get all ``Subscriber``, ``Original`` and ``Copy`` contracts
|
daml trigger --dar .daml/dist/create-daml-app-0.1.0.dar \
|
||||||
from the ACS. For that, the Daml trigger API provides a ``query``
|
--trigger-name NoOp:noOp \
|
||||||
function that will return a list of all contracts of a given template.
|
--ledger-host localhost \
|
||||||
|
--ledger-port 6865 \
|
||||||
|
--ledger-party "bob"
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
and then play with the app as ``alice`` and ``bob`` just like you did for
|
||||||
:language: daml
|
:doc:`/getting-started/first-feature`, you should see the trigger command
|
||||||
:start-after: -- ACS_QUERY_BEGIN
|
printing a line for each interaction, containing the message ``triggered`` as
|
||||||
:end-before: -- ACS_QUERY_END
|
well as other debug information.
|
||||||
|
|
||||||
Now, we can filter those contracts to the ones where we are the
|
Diversion: Updating ``Message``
|
||||||
``owner`` as described before.
|
===============================
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
Before we can make our Trigger more useful, we need to think a bit more about
|
||||||
:language: daml
|
what it is supposed to do. For example, we don't want to respond to ``bob``'s
|
||||||
:start-after: -- ACS_FILTER_BEGIN
|
own messages. We also do not want to send messages when we have not received
|
||||||
:end-before: -- ACS_FILTER_END
|
any.
|
||||||
|
|
||||||
We also need a list of all parties that have subscribed to us.
|
In order to start with something reasonably simple, we're going to set the rule as
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
*if* the last message we can see was *not* sent by ``bob``, *then* we'll send
|
||||||
:language: daml
|
``"Please, tell me more about that."`` to whoever sent the last message we
|
||||||
:start-after: -- SUBSCRIBING_PARTIES_BEGIN
|
can see.
|
||||||
:end-before: -- SUBSCRIBING_PARTIES_END
|
|
||||||
|
|
||||||
As we have mentioned before, we only want to keep one ``Copy`` per
|
This raises the question of how we can determine which message is the last one,
|
||||||
``Original`` and ``Subscriber`` and archive all others. Therefore, we
|
given the current structure of a message. In order to solve that, we need to
|
||||||
group identical ``Copy`` contracts and keep the first of each group
|
add a ``Time`` field to ``Message``, which can be done by editing the
|
||||||
while archiving the others.
|
``Message`` template in ``daml/User.daml`` to look like:
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
.. literalinclude:: /_templates/gsg-trigger/daml/User.daml
|
||||||
:language: daml
|
:language: daml
|
||||||
:start-after: -- GROUP_COPIES_BEGIN
|
:start-after: -- MESSAGE_BEGIN
|
||||||
:end-before: -- GROUP_COPIES_END
|
:end-before: -- MESSAGE_END
|
||||||
|
|
||||||
In addition to duplicate copies, we also need to archive copies where
|
This should result in Daml Studio reporting an error in the ``SendMessage``
|
||||||
the corresponding ``Original`` or ``Subscriber`` no longer exists.
|
choice, as it now needs to set the ``receivedAt`` field. Here is the updated
|
||||||
|
code for ``SendMessage``:
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
.. literalinclude:: /_templates/gsg-trigger/daml/User.daml
|
||||||
:language: daml
|
:language: daml
|
||||||
:start-after: -- ARCHIVE_COPIES_BEGIN
|
:start-after: -- SEND_BEGIN
|
||||||
:end-before: -- ARCHIVE_COPIES_END
|
:end-before: -- SEND_END
|
||||||
|
|
||||||
To send the corresponding archive commands to the ledger, we iterate
|
The ``getTime`` action (`doc </daml/stdlib/Prelude.html#function-da-internal-lf-gettime-99334>`__)
|
||||||
over ``archiveCopies`` using ``forA`` and call the ``emitCommands``
|
returns the time at which the command was received by the sandbox. In more
|
||||||
function. Each call to ``emitCommands`` takes a list of commands which
|
sensitive applications, this may not be sufficiently reliable, as transactions
|
||||||
will be submitted as a single transaction. The actual commands can be
|
may be processed in parallel (so "received at" timestamp order may not match
|
||||||
created using ``exerciseCmd`` and ``createCmd``. In addition to that,
|
actual transaction order), and in distributed cases dishonest participants may
|
||||||
we also pass in a list of contract ids. Those contracts will be marked
|
fudge this value. It's good enough for this example, though.
|
||||||
pending and not be included in the result of ``query`` until
|
|
||||||
the commands have either been committed to the ledger or the command
|
|
||||||
submission failed.
|
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
Now that we have a field to sort on, and thus a way to identify the *latest*
|
||||||
:language: daml
|
message, we can turn our attention back to our trigger code.
|
||||||
:start-after: -- ARCHIVE_COMMAND_BEGIN
|
|
||||||
:end-before: -- ARCHIVE_COMMAND_END
|
|
||||||
|
|
||||||
Finally, we also need to create copies that do not already exists. We
|
AutoReply
|
||||||
want to avoid creating copies for which there is already a command in
|
=========
|
||||||
flight. The Daml Trigger API provides a ``dedupCreate`` helper for this
|
|
||||||
which only sends the commands if it is not already in flight.
|
|
||||||
|
|
||||||
.. literalinclude:: ./template-root/src/CopyTrigger.daml
|
Open up the trigger code again (``daml/ChatBot.daml``), and change it to:
|
||||||
:language: daml
|
|
||||||
:start-after: -- CREATE_COPIES_BEGIN
|
|
||||||
:end-before: -- CREATE_COPIES_END
|
|
||||||
|
|
||||||
.. _running-a-daml-trigger:
|
.. literalinclude:: /_templates/gsg-trigger/daml/ChatBot.daml
|
||||||
|
:language: daml
|
||||||
|
|
||||||
Running a Daml Trigger
|
Refresh ``daml start`` by pressing ``r`` (followed by ``Enter`` on Windows) in
|
||||||
----------------------
|
its terminal, then start the trigger with:
|
||||||
|
|
||||||
To try this example out, you can replicate it using
|
.. code-block:: bash
|
||||||
``daml new copy-trigger --template copy-trigger``. You first have to build the trigger like
|
|
||||||
you would build a regular Daml project using ``daml build``.
|
|
||||||
Then start the sandbox and navigator using ``daml start``.
|
|
||||||
|
|
||||||
Now we are ready to run the trigger using ``daml trigger``:
|
daml trigger --dar .daml/dist/create-daml-app-0.1.0.dar \
|
||||||
|
--trigger-name ChatBot:autoReply \
|
||||||
|
--ledger-host localhost \
|
||||||
|
--ledger-port 6865 \
|
||||||
|
--ledger-party "bob"
|
||||||
|
|
||||||
.. code-block:: sh
|
Play a bit with ``alice`` and ``bob`` in your browser, to get a feel for how
|
||||||
|
the trigger works. Watch both the messages in-browser and the debug statements
|
||||||
|
printed by the trigger runner.
|
||||||
|
|
||||||
daml trigger --dar .daml/dist/copy-trigger-0.0.1.dar --trigger-name CopyTrigger:copyTrigger --ledger-host localhost --ledger-port 6865 --ledger-party Alice
|
Let's walk through the ``rule`` code line-by-line:
|
||||||
|
|
||||||
The first argument specifies the ``.dar`` file that we have just
|
- We use the ``query`` function to get all of the ``Message`` templates visible
|
||||||
built. The second argument specifies the identifier of the trigger
|
to the current party (``p``; in our case this will be ``bob``). Per the
|
||||||
using the syntax ``ModuleName:identifier``. Finally, we need to
|
`documentation </triggers/api/Daml-Trigger.html#function-daml-trigger-query-2759>`_,
|
||||||
specify the ledger host, port, the party that our trigger is executed
|
this returns a list of tuples (contract id, payload), which we store as
|
||||||
as, and the time mode of the ledger which is the sandbox default, i.e,
|
``message_contracts``.
|
||||||
static time.
|
- We then `map </daml/stdlib/Prelude.html#function-ghc-base-map-40302>`_ the
|
||||||
|
`snd </daml/stdlib/Prelude.html#function-da-internal-prelude-snd-86578>`_
|
||||||
|
function on the result to get only the payloads, i.e. the actual data of the
|
||||||
|
messages we can see.
|
||||||
|
- We print, as a ``debug`` message, the number of messages we can see.
|
||||||
|
- On the next line, get the message with the highest ``receivedAt`` field
|
||||||
|
(`maximumOn </daml/stdlib/DA-List-Total.html#function-da-list-total-maximumon-67732>`_).
|
||||||
|
- We then print another debug message, this time printing the message our code
|
||||||
|
has identified as "the last message visible to the current party". If you run
|
||||||
|
this, you'll see that ``lastMessage`` is actually a ``Optional Message``. This
|
||||||
|
is because the `maximumOn </daml/stdlib/DA-List-Total.html#function-da-list-total-maximumon-67732>`_
|
||||||
|
function will return the element from a list for which the given functions
|
||||||
|
produces the highest value *if* the list has at least one element, but it
|
||||||
|
needs to still do something sensible if the list is empty; in this case, it
|
||||||
|
would return ``None``.
|
||||||
|
- When ``lastMessage`` is ``Some m``
|
||||||
|
(`whenSome </daml/stdlib/DA-Optional.html#function-da-optional-whensome-23804>`_),
|
||||||
|
we execute the given function. Otherwise, ``lastMessage`` is ``None`` and we
|
||||||
|
implicitly do nothing.
|
||||||
|
- Next, we need to check whether the message has been sent *to* or *by* the
|
||||||
|
party running the trigger (with the current Daml model, it has to be one or
|
||||||
|
the other, as messages are only visible to the sender and receiver).
|
||||||
|
`when </daml/stdlib/DA-Action.html#function-da-action-when-53144>`_ the
|
||||||
|
expression ``m.receiver == p`` is ``True``, we then our expectations of the
|
||||||
|
ledger state are wrong and we need to correct it. Otherwise, the state
|
||||||
|
matches our rule and we don't need to do anything.
|
||||||
|
- At this point we know the state is "wrong", per our expectations, and start
|
||||||
|
engaging in correcting actions. For this trigger, this means sendinga message
|
||||||
|
to the sender of the last message. In order to do that, we need to find the
|
||||||
|
``User`` contract for the sender. We start by getting the list of all
|
||||||
|
``User`` contracts we know about, which will be all users who
|
||||||
|
follow the party running the trigger (and that party's own ``User``
|
||||||
|
contract). As for ``Message`` contracts earlier, the result of ``query
|
||||||
|
@User`` is going to be a list of tuples with (contract id, payload). The big
|
||||||
|
difference is that this time we actually want to keep the contract ids, as
|
||||||
|
that is what we'll use to send a message back.
|
||||||
|
- We print the list of users we just fetched, as a debug message.
|
||||||
|
- We create a function to identify the user we are looking for.
|
||||||
|
- We get the user contract by applying our ``isSender`` function as a
|
||||||
|
`filter </daml/stdlib/Prelude.html#function-da-internal-prelude-filter-27394>`_
|
||||||
|
on the list of users, and then taking the
|
||||||
|
`head <daml/stdlib/DA-List-Total.html#function-da-list-total-head-74336>`_
|
||||||
|
of that list, i.e. its first element.
|
||||||
|
- Just like ``maximumOn``, ``head`` will return an ``Optional a``, so the next
|
||||||
|
step is to check whether we have actually found the relevant ``User``
|
||||||
|
contract. In most cases we should find it, but remember that users can send
|
||||||
|
us a message if *we* follow *them*, whereas we can only answer if *they*
|
||||||
|
follow *us*.
|
||||||
|
- If we did find some ``User`` contract to reply to, we extract the
|
||||||
|
corresponding contract id (first element of the tuple, ``sender``) and
|
||||||
|
discard the payload (second element, ``_``), and we
|
||||||
|
`exercise <triggers/api/Daml-Trigger.html#function-daml-trigger-dedupexercise-37617>`_
|
||||||
|
the ``SendMessage`` choice, passing in the current party ``p`` as the sender.
|
||||||
|
See below for additional information on what that ``dedup`` in the name of the
|
||||||
|
command means.
|
||||||
|
|
||||||
Now open Navigator at http://localhost:7500/.
|
Command Deduplication
|
||||||
|
=====================
|
||||||
|
|
||||||
First, login as ``Alice`` and create an ``Original`` contract with
|
Daml Triggers react to many things, and it's usually important to make sure
|
||||||
``party`` set to ``Alice``. Now, logout and login as ``Bob`` and
|
that the same command is not sent mutiple times.
|
||||||
create a ``Subscriber`` contract with ``subscriber`` set to ``Bob``
|
|
||||||
and ``subscribedTo`` set to ``Alice``. After a short delay you should
|
For example, in our ``autoReply`` chatbot above, the rule will be triggered not
|
||||||
now see a ``Copy`` contract corresponding to the ``Original`` that you
|
only when we receive a message, but also when we send one, as well as when we
|
||||||
have created as ``Alice``. Once you archive the ``Subscriber``
|
follow a user or get followed by a user, and when we stop following a user or a
|
||||||
contract, you can see that the ``Copy`` contract will also be
|
user stops following us.
|
||||||
archived.
|
|
||||||
|
It's easy to imagine a sequence of events that would make a naive trigger
|
||||||
|
implementation send too many messages. For example:
|
||||||
|
|
||||||
|
- ``alice`` sends ``"hi"``, so the trigger runs and sends an ``exercise`` command.
|
||||||
|
- _Before_ the ``exercise`` command is fully processed, ``carol`` follows
|
||||||
|
``bob``, which triggers the rule again. The state of all the ``Message``
|
||||||
|
contracts ``bob`` can see has not changed, so the rule might send the
|
||||||
|
response to ``alice`` again.
|
||||||
|
|
||||||
|
We obviously don't want that to happen, as it would likely prevent us from
|
||||||
|
passing that Turing test we were after.
|
||||||
|
|
||||||
|
Triggers offer a few features to help users manage that. Possibly the simplest
|
||||||
|
one is the ``dedup*`` family of ledger operations. When using those, the
|
||||||
|
trigger runner will keep track of the commands currently sent and prevent
|
||||||
|
sending the exact same command again. In the above example, the trigger would
|
||||||
|
see that, when ``carol`` follows ``bob`` and the rule runs ``dedupExercise``,
|
||||||
|
there is already an Exercise command in flight with the exact same value, in
|
||||||
|
this case same message, same sender and same receiver.
|
||||||
|
|
||||||
|
Note that, if instead the in-between event is ``alice`` following ``carol``,
|
||||||
|
this simple deduplication mechanism might not work as expected: because the
|
||||||
|
``User`` contract ID for ``alice`` would have changed, the new command is not
|
||||||
|
the same as the in-flight one and thus a second ``SendMessage`` exercise would
|
||||||
|
be sent to the ledger.
|
||||||
|
|
||||||
|
Similarly, if ``alice`` sends a second message quickly after the first one,
|
||||||
|
this deduplication would prevent it, because the "response" does not have any
|
||||||
|
reference to which message it's responding to. This may or may not be what we
|
||||||
|
want.
|
||||||
|
|
||||||
|
If this simple deduplication is not suited to your use-case, you have two other
|
||||||
|
tools at your disposal. The first one is the second argument to the
|
||||||
|
``emitCommands`` action
|
||||||
|
(`doc <https://docs.daml.com/triggers/api/Daml-Trigger.html#function-daml-trigger-emitcommands-10563>`__),
|
||||||
|
which is a list of contract IDs. These IDs will be filtered out of any ACS
|
||||||
|
``query`` made by this trigger until the commands submitted as part of the same
|
||||||
|
``emitCommands`` call have completed. If your trigger is based on seeing
|
||||||
|
certain contracts, this can be a simple, effective way to prevent triggering it
|
||||||
|
multiple times.
|
||||||
|
|
||||||
|
The last tool you have at your disposal is the ``getCommandsInflight`` action
|
||||||
|
(`doc <https://docs.daml.com/triggers/api/Daml-Trigger.html#function-daml-trigger-getcommandsinflight-32524>`__),
|
||||||
|
which returns all of the commands this instance of the trigger runner has sent
|
||||||
|
and that have not yet been resolved (i.e. either committed or failed). You can
|
||||||
|
then build your own logic based on this list, the ACS, and possibly your own
|
||||||
|
trigger state.
|
||||||
|
|
||||||
|
Finally, do keep in mind that all of these mechanisms rely on internal state
|
||||||
|
from the trigger runner, which keeps track of which commands it has sent and
|
||||||
|
for which it's not seen a completion. They will all fail to deduplicate if that
|
||||||
|
internal state is lost, e.g. if the trigger runner is shut down and a new one
|
||||||
|
is started. As such, these deduplication mechanisms should be seen as an
|
||||||
|
optimization rather than a requirement for correctness. The Daml model should
|
||||||
|
be designed such that duplicated commands are either rejected (e.g. using keys
|
||||||
|
or relying on changing contract IDs) or benign.
|
||||||
|
|
||||||
|
Authentication
|
||||||
|
==============
|
||||||
|
|
||||||
When using Daml triggers against a Ledger with authentication, you can
|
When using Daml triggers against a Ledger with authentication, you can
|
||||||
pass ``--access-token-file token.jwt`` to ``daml trigger`` which will
|
pass ``--access-token-file token.jwt`` to ``daml trigger`` which will
|
||||||
read the token from the file ``token.jwt``.
|
read the token from the file ``token.jwt``.
|
||||||
|
|
||||||
|
If you plan to run more than one trigger at a time, or trigers for more than
|
||||||
|
one party at a time, you may be interested in the
|
||||||
|
:doc:`/tools/trigger-service/index`.
|
||||||
|
|
||||||
When not to use Daml triggers
|
When not to use Daml triggers
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
Daml triggers deliberately only allow you to express automation that
|
||||||
|
listens for ledger events and reacts to them by sending commands to
|
||||||
|
the ledger.
|
||||||
|
|
||||||
Daml Triggers are not suited for automation that needs to interact
|
Daml Triggers are not suited for automation that needs to interact
|
||||||
with services or data outside of the ledger. For those cases, you can
|
with services or data outside of the ledger. For those cases, you can
|
||||||
write a ledger client using the
|
write a ledger client using the
|
||||||
@ -277,7 +403,3 @@ write a ledger client using the
|
|||||||
running against the HTTP JSON API or the
|
running against the HTTP JSON API or the
|
||||||
:doc:`Java bindings</app-dev/bindings-java/index>` running against the
|
:doc:`Java bindings</app-dev/bindings-java/index>` running against the
|
||||||
gRPC Ledger API.
|
gRPC Ledger API.
|
||||||
|
|
||||||
Daml triggers deliberately only allow you to express automation that
|
|
||||||
listens for ledger events and reacts to them by sending commands to
|
|
||||||
the ledger.
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
load("@os_info//:os_info.bzl", "is_windows")
|
load("@os_info//:os_info.bzl", "is_windows")
|
||||||
|
|
||||||
exports_files(glob(["create-daml-app-test-resources/*"]))
|
exports_files(glob(["create-daml-app-test-resources/*"]) + ["copy-trigger/src/CopyTrigger.daml"])
|
||||||
|
|
||||||
# Split out into a separate rule so we can cheaply include this in the
|
# Split out into a separate rule so we can cheaply include this in the
|
||||||
# live-preview.
|
# live-preview.
|
||||||
@ -49,13 +49,14 @@ genrule(
|
|||||||
"create-daml-app/**",
|
"create-daml-app/**",
|
||||||
"quickstart-java/**",
|
"quickstart-java/**",
|
||||||
"quickstart-scala/**",
|
"quickstart-scala/**",
|
||||||
|
"copy-trigger/**",
|
||||||
|
"gsg-trigger.patch",
|
||||||
],
|
],
|
||||||
exclude = ["**/NO_AUTO_COPYRIGHT"],
|
exclude = ["**/NO_AUTO_COPYRIGHT"],
|
||||||
) + [
|
) + [
|
||||||
"//docs:quickstart-java.tar.gz",
|
"//docs:quickstart-java.tar.gz",
|
||||||
"//docs:daml-intro-templates",
|
"//docs:daml-intro-templates",
|
||||||
"//docs:daml-patterns",
|
"//docs:daml-patterns",
|
||||||
"//docs:copy-trigger-template",
|
|
||||||
"//docs:script-example-template",
|
"//docs:script-example-template",
|
||||||
"//language-support/scala/examples:quickstart-scala.tar",
|
"//language-support/scala/examples:quickstart-scala.tar",
|
||||||
],
|
],
|
||||||
@ -64,8 +65,18 @@ genrule(
|
|||||||
SRC=templates
|
SRC=templates
|
||||||
OUT=templates-tarball
|
OUT=templates-tarball
|
||||||
|
|
||||||
|
PATCH_TOOL=$$PWD/$(location @patch_dev_env//:patch)
|
||||||
|
cp -rL $$SRC/create-daml-app $$SRC/gsg-trigger
|
||||||
|
"$$PATCH_TOOL" -d $$SRC/gsg-trigger -p1 < $$SRC/gsg-trigger.patch
|
||||||
|
|
||||||
# templates in templates dir
|
# templates in templates dir
|
||||||
for d in skeleton empty-skeleton create-daml-app quickstart-scala quickstart-java; do
|
for d in skeleton \
|
||||||
|
empty-skeleton \
|
||||||
|
create-daml-app \
|
||||||
|
quickstart-scala \
|
||||||
|
quickstart-java \
|
||||||
|
copy-trigger \
|
||||||
|
gsg-trigger; do
|
||||||
mkdir -p $$OUT/$$d
|
mkdir -p $$OUT/$$d
|
||||||
cp -rL $$SRC/$$d/* $$OUT/$$d/
|
cp -rL $$SRC/$$d/* $$OUT/$$d/
|
||||||
for f in gitattributes gitignore dlint.yaml; do
|
for f in gitattributes gitignore dlint.yaml; do
|
||||||
@ -88,9 +99,6 @@ genrule(
|
|||||||
# daml intro templates
|
# daml intro templates
|
||||||
tar xf $(location //docs:daml-intro-templates) -C $$OUT
|
tar xf $(location //docs:daml-intro-templates) -C $$OUT
|
||||||
|
|
||||||
mkdir -p $$OUT/copy-trigger
|
|
||||||
tar xf $(location //docs:copy-trigger-template) -C $$OUT/copy-trigger
|
|
||||||
|
|
||||||
mkdir -p $$OUT/script-example
|
mkdir -p $$OUT/script-example
|
||||||
tar xf $(location //docs:script-example-template) -C $$OUT/script-example
|
tar xf $(location //docs:script-example-template) -C $$OUT/script-example
|
||||||
|
|
||||||
@ -99,6 +107,9 @@ genrule(
|
|||||||
|
|
||||||
$(execpath //bazel_tools/sh:mktgz) $@ templates-tarball
|
$(execpath //bazel_tools/sh:mktgz) $@ templates-tarball
|
||||||
""",
|
""",
|
||||||
tools = ["//bazel_tools/sh:mktgz"],
|
tools = [
|
||||||
|
"//bazel_tools/sh:mktgz",
|
||||||
|
"@patch_dev_env//:patch",
|
||||||
|
],
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
sdk-version: __VERSION__
|
sdk-version: __VERSION__
|
||||||
name: copy-trigger
|
name: __PROJECT_NAME__
|
||||||
source: src
|
source: src
|
||||||
parties:
|
parties:
|
||||||
- Alice
|
- Alice
|
94
templates/gsg-trigger.patch
Normal file
94
templates/gsg-trigger.patch
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
diff -Naur create-daml-app/daml/ChatBot.daml gsg-trigger/daml/ChatBot.daml
|
||||||
|
--- create-daml-app/daml/ChatBot.daml 1970-01-01 01:00:00.000000000 +0100
|
||||||
|
+++ gsg-trigger/daml/ChatBot.daml 2021-08-16 13:05:51.000000000 +0200
|
||||||
|
@@ -0,0 +1,29 @@
|
||||||
|
+module ChatBot where
|
||||||
|
+
|
||||||
|
+import qualified Daml.Trigger as T
|
||||||
|
+import qualified User
|
||||||
|
+import qualified DA.List.Total as List
|
||||||
|
+import DA.Action (when)
|
||||||
|
+import DA.Optional (whenSome)
|
||||||
|
+
|
||||||
|
+autoReply : T.Trigger ()
|
||||||
|
+autoReply = T.Trigger
|
||||||
|
+ { initialize = pure ()
|
||||||
|
+ , updateState = \_ -> pure ()
|
||||||
|
+ , rule = \p -> do
|
||||||
|
+ message_contracts <- T.query @User.Message
|
||||||
|
+ let messages = map snd message_contracts
|
||||||
|
+ debug $ "Messages so far: " <> show (length messages)
|
||||||
|
+ let lastMessage = List.maximumOn (.receivedAt) messages
|
||||||
|
+ debug $ "Last message: " <> show lastMessage
|
||||||
|
+ whenSome lastMessage $ \m ->
|
||||||
|
+ when (m.receiver == p) $ do
|
||||||
|
+ users <- T.query @User.User
|
||||||
|
+ debug users
|
||||||
|
+ let isSender = (\user -> user.username == m.sender)
|
||||||
|
+ let replyTo = List.head $ filter (\(_, user) -> isSender user) users
|
||||||
|
+ whenSome replyTo $ \(sender, _) ->
|
||||||
|
+ T.dedupExercise sender (User.SendMessage p "Please, tell me more about that.")
|
||||||
|
+ , registeredTemplates = T.AllInDar
|
||||||
|
+ , heartbeat = None
|
||||||
|
+ }
|
||||||
|
diff -Naur create-daml-app/daml/NoOp.daml gsg-trigger/daml/NoOp.daml
|
||||||
|
--- create-daml-app/daml/NoOp.daml 1970-01-01 01:00:00.000000000 +0100
|
||||||
|
+++ gsg-trigger/daml/NoOp.daml 2021-08-16 13:02:52.000000000 +0200
|
||||||
|
@@ -0,0 +1,13 @@
|
||||||
|
+module NoOp where
|
||||||
|
+
|
||||||
|
+import qualified Daml.Trigger as T
|
||||||
|
+
|
||||||
|
+noOp : T.Trigger ()
|
||||||
|
+noOp = T.Trigger with
|
||||||
|
+ initialize = pure ()
|
||||||
|
+ updateState = \_ -> pure ()
|
||||||
|
+ rule = \_ -> do
|
||||||
|
+ debug "triggered"
|
||||||
|
+ pure ()
|
||||||
|
+ registeredTemplates = T.AllInDar
|
||||||
|
+ heartbeat = None
|
||||||
|
diff -Naur create-daml-app/daml/User.daml gsg-trigger/daml/User.daml
|
||||||
|
--- create-daml-app/daml/User.daml 2021-08-12 14:36:31.000000000 +0200
|
||||||
|
+++ gsg-trigger/daml/User.daml 2021-08-16 13:02:52.000000000 +0200
|
||||||
|
@@ -25,3 +25,24 @@
|
||||||
|
archive self
|
||||||
|
create this with following = userToFollow :: following
|
||||||
|
-- FOLLOW_END
|
||||||
|
+ -- SEND_BEGIN
|
||||||
|
+ -- New definition for SendMessage
|
||||||
|
+ nonconsuming choice SendMessage: ContractId Message with
|
||||||
|
+ sender: Party
|
||||||
|
+ content: Text
|
||||||
|
+ controller sender
|
||||||
|
+ do
|
||||||
|
+ assertMsg "Designated user must follow you back to send a message" (elem sender following)
|
||||||
|
+ now <- getTime
|
||||||
|
+ create Message with sender, receiver = username, content, receivedAt = now
|
||||||
|
+ -- SEND_END
|
||||||
|
+
|
||||||
|
+-- MESSAGE_BEGIN
|
||||||
|
+template Message with
|
||||||
|
+ sender: Party
|
||||||
|
+ receiver: Party
|
||||||
|
+ content: Text
|
||||||
|
+ receivedAt: Time
|
||||||
|
+ where
|
||||||
|
+ signatory sender, receiver
|
||||||
|
+-- MESSAGE_END
|
||||||
|
diff -Naur create-daml-app/daml.yaml.template gsg-trigger/daml.yaml.template
|
||||||
|
--- create-daml-app/daml.yaml.template 2021-05-12 10:23:33.000000000 +0200
|
||||||
|
+++ gsg-trigger/daml.yaml.template 2021-08-16 13:02:52.000000000 +0200
|
||||||
|
@@ -6,10 +6,12 @@
|
||||||
|
- Alice
|
||||||
|
- Bob
|
||||||
|
- Charlie
|
||||||
|
+# trigger-dependencies-begin
|
||||||
|
dependencies:
|
||||||
|
- daml-prim
|
||||||
|
- daml-stdlib
|
||||||
|
- daml-trigger
|
||||||
|
+# trigger-dependencies-end
|
||||||
|
sandbox-options:
|
||||||
|
- --wall-clock-time
|
||||||
|
- --ledgerid=__PROJECT_NAME__-sandbox
|
@ -26,7 +26,8 @@ DAML_LF_VERSIONS = [
|
|||||||
glob(["**/*.daml"]) + [
|
glob(["**/*.daml"]) + [
|
||||||
"//triggers/daml:daml-trigger%s.dar" % suffix,
|
"//triggers/daml:daml-trigger%s.dar" % suffix,
|
||||||
"//daml-script/daml:daml-script%s.dar" % suffix,
|
"//daml-script/daml:daml-script%s.dar" % suffix,
|
||||||
"//docs:source/triggers/template-root/src/CopyTrigger.daml",
|
] + [
|
||||||
|
"//templates:copy-trigger/src/CopyTrigger.daml",
|
||||||
],
|
],
|
||||||
outs = ["acs%s.dar" % suffix],
|
outs = ["acs%s.dar" % suffix],
|
||||||
cmd = """
|
cmd = """
|
||||||
@ -44,7 +45,7 @@ DAML_LF_VERSIONS = [
|
|||||||
cp -L $(location :daml/TemplateIdFilter.daml) $$TMP_DIR/daml
|
cp -L $(location :daml/TemplateIdFilter.daml) $$TMP_DIR/daml
|
||||||
cp -L $(location :daml/Time.daml) $$TMP_DIR/daml
|
cp -L $(location :daml/Time.daml) $$TMP_DIR/daml
|
||||||
cp -L $(location :daml/Heartbeat.daml) $$TMP_DIR/daml
|
cp -L $(location :daml/Heartbeat.daml) $$TMP_DIR/daml
|
||||||
cp -L $(location //docs:source/triggers/template-root/src/CopyTrigger.daml) $$TMP_DIR/daml
|
cp -L $(location //templates:copy-trigger/src/CopyTrigger.daml) $$TMP_DIR/daml
|
||||||
cp -L $(location //triggers/daml:daml-trigger{suffix}.dar) $$TMP_DIR/daml-trigger.dar
|
cp -L $(location //triggers/daml:daml-trigger{suffix}.dar) $$TMP_DIR/daml-trigger.dar
|
||||||
cp -L $(location //daml-script/daml:daml-script{suffix}.dar) $$TMP_DIR/daml-script.dar
|
cp -L $(location //daml-script/daml:daml-script{suffix}.dar) $$TMP_DIR/daml-script.dar
|
||||||
cat << EOF > $$TMP_DIR/daml.yaml
|
cat << EOF > $$TMP_DIR/daml.yaml
|
||||||
|
Loading…
Reference in New Issue
Block a user