Skip to content

Commit

Permalink
Expose a contract's agreement text on the Ledger API (digital-asset#1151
Browse files Browse the repository at this point in the history
)

* Added agreement_text field to the CreatedEvent in Ledger API.
* Changed java bindings + java codegen
* Changed utilities for scala codegen
* Made necessary changes in Sandbox to propagate the agreement text from ContractInst to the CreatedEvent
* Made changes to the navigator to show the agreement text in the contract details page when it is set and not empty

Fixes digital-asset#1110
  • Loading branch information
gerolf-da authored May 17, 2019
1 parent f48b7c7 commit c645348
Show file tree
Hide file tree
Showing 53 changed files with 419 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final case class CreateEvent[Cid, Val](
contractId: Cid,
templateId: Identifier,
argument: Val,
agreementText: String,
stakeholders: Set[Party],
witnesses: Set[Party])
extends Event[Nothing, Cid, Val] {
Expand Down Expand Up @@ -153,6 +154,7 @@ object Event {
nc.coid,
templateId,
nc.coinst.arg,
nc.coinst.agreementText,
stakeholders intersect disclosure(nodeId),
disclosure(nodeId))
evts += (nodeId -> evt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@ class EngineTest extends WordSpec with Matchers with BazelRunfiles {
(Some[Name]("giver"), ValueParty("Alice")),
(Some[Name]("receiver"), ValueParty("Clara")))
)),
"",
Set("Clara", "Alice"),
Set("Bob", "Clara", "Alice"),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class SemanticTester(
nextScenarioCoidToLedgerCoid(scenarioCreateNode.coid),
scenarioCreateNode.coinst.template,
scenarioCreateNode.coinst.arg.mapContractId(nextScenarioCoidToLedgerCoid),
scenarioCreateNode.coinst.agreementText,
scenarioCreateNode.stakeholders intersect scenarioWitnesses(scenarioNodeId),
scenarioWitnesses(scenarioNodeId),
)
Expand Down
7 changes: 4 additions & 3 deletions docs/source/app-dev/bindings-java/codegen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ To avoid possible name clashes in the generated Java sources, you should specify
^^^^^^^^^^^^^^^^^^^^^^^^^^
daml/project2.dar=com.example.daml.project2
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. _daml-codegen-java-decoder-class:

Generate the decoder utility class
----------------------------------

When reading transactions from the ledger, you typically want to convert a `CreatedEvent <https://docs.daml.com/app-dev/bindings-java/javadocs/com/daml/ledger/javaapi/data/CreatedEvent.html>`__ from the Ledger API to the corresponding generated ``Contract`` class. The Java codegen can optionally generate a decoder class based on the input DAR files that calls the ``fromIdAndRecord`` method of the respective generated ``Contract`` class (see :ref:`daml-codegen-java-templates`). The decoder class can do this for all templates in the input DAR files.
When reading transactions from the ledger, you typically want to convert a `CreatedEvent <https://docs.daml.com/app-dev/bindings-java/javadocs/com/daml/ledger/javaapi/data/CreatedEvent.html>`__ from the Ledger API to the corresponding generated ``Contract`` class. The Java codegen can optionally generate a decoder class based on the input DAR files that calls the ``fromCreatedEvent`` method of the respective generated ``Contract`` class (see :ref:`daml-codegen-java-templates`). The decoder class can do this for all templates in the input DAR files.

To generate such a decoder class, provide the command line parameter ``-d`` or ``--decoderClass`` with a fully qualified class name:

Expand Down Expand Up @@ -254,7 +255,7 @@ The Java codegen generates three classes for a DAML template:
.. TODO: refer to another section explaining exactly that, when we have it.
**TemplateName.Contract**
Represents an actual contract on the ledger. It contains a field for the contract ID (of type ``TemplateName.ContractId``) and a field for the template data (of type ``TemplateName``). With the static method ``TemplateName.Contract.fromIdAndRecord``, you can deserialize a `CreatedEvent <https://docs.daml.com/app-dev/bindings-java/javadocs/com/daml/ledger/javaapi/data/CreatedEvent.html>`__ to an instance of ``TemplateName.Contract``.
Represents an actual contract on the ledger. It contains a field for the contract ID (of type ``TemplateName.ContractId``) and a field for the template data (of type ``TemplateName``). With the static method ``TemplateName.Contract.fromCreatedEvent``, you can deserialize a `CreatedEvent <https://docs.daml.com/app-dev/bindings-java/javadocs/com/daml/ledger/javaapi/data/CreatedEvent.html>`__ to an instance of ``TemplateName.Contract``.


.. literalinclude:: ./code-snippets/Templates.daml
Expand Down Expand Up @@ -300,7 +301,7 @@ A file is generated that defines three Java classes:
public final ContractId id;
public final Bar data;
public static Contract fromIdAndRecord(String contractId, Record record) { /* ... */ }
public static Contract fromCreatedEvent(CreatedEvent event) { /* ... */ }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public static void main(String[] args) {
.blockingForEach(response -> {
response.getOffset().ifPresent(offset -> acsOffset.set(new LedgerOffset.Absolute(offset)));
response.getCreatedEvents().stream()
.map(e -> Iou.Contract.fromIdAndRecord(e.getContractId(), e.getArguments()))
.map(Iou.Contract::fromCreatedEvent)
.forEach(contract -> {
long id = idCounter.getAndIncrement();
contracts.put(id, contract.data);
Expand All @@ -83,7 +83,7 @@ public static void main(String[] args) {
if (event instanceof CreatedEvent) {
CreatedEvent createdEvent = (CreatedEvent) event;
long id = idCounter.getAndIncrement();
Iou.Contract contract = Iou.Contract.fromIdAndRecord(createdEvent.getContractId(), createdEvent.getArguments());
Iou.Contract contract = Iou.Contract.fromCreatedEvent(createdEvent);
contracts.put(id, contract.data);
idMap.put(id, contract.id);
} else if (event instanceof ArchivedEvent) {
Expand Down
74 changes: 73 additions & 1 deletion docs/source/support/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Ledger API

- If you check for the presence of :ref:`com.digitalasset.ledger.api.v1.ExercisedEvent` when handling a :ref:`com.digitalasset.ledger.api.v1.Transaction`, you have to remove this code now.

- Added the :ref:`agreement text <daml-ref-agreements>` as a new field ``agreement_text`` to the ``CreatedEvent`` message. This means you now have access to the agreement text of contracts via the Ledger API.
The type of this field is ``google.protobuf.StringValue`` to properly reflect the optionality on the wire for full backwards compatibility.
See Google's `wrappers.proto <https://github.com/protocolbuffers/protobuf/blob/b4f193788c9f0f05d7e0879ea96cd738630e5d51/src/google/protobuf/wrappers.proto#L31-L34>`__ for more information about ``StringValue``.

See `#1110 <https://github.com/digital-asset/daml/issues/1110>`__ for details.


Java Bindings
~~~~~~~~~~~~~

Expand Down Expand Up @@ -83,17 +90,82 @@ Java Bindings

See `issue #1092 <https://github.com/digital-asset/daml/issues/1092>`__ for details.

- Added :ref:`agreement text <daml-ref-agreements>` of contracts: `#1110 <https://github.com/digital-asset/daml/issues/1110>`__

- **Java Bindings**

- Added field ``Optional<String> agreementText`` to ``data.CreatedEvent``, to reflect the change in Ledger API.

- **Java Codegen**

- Added generated field ``Optional<String> TemplateName.Contract#agreementText``.
- Added generated static method ``TemplateName.Contract.fromCreatedEvent(CreatedEvent)``.
This is the preferred method to use for converting a ``CreatedEvent`` into a ``Contract``.
- Added generated static method ``TemplateName.Contract.fromIdAndRecord(String, Record, Optional<String>)``.
This method is useful for setting up tests, when you want to convert a ``Record`` into a contract without having to create a ``CreatedEvent`` first.
- Deprecated generated static method ``TemplateName.Contract.fromIdAndRecord(String, Record)`` in favor of the new static methods in the generated ``Contract`` classes.
- Changed the generated :ref:`decoder utility class <daml-codegen-java-decoder-class>` to use the new ``fromCreatedEvent`` method.
- **BREAKING** Changed the return type of the ``getDecoder`` method in the generated decoder utility class from ``Optional<BiFunction<String, Record, Contract>>`` to ``Optional<Function<CreatedEvent, Contract>>``.

How to migrate:

- If you are manually constructing instances of ``data.CreatedEvent`` (for example, for testing), you need to add an ``Optional<String>`` value as constructor parameter for the ``agreementText`` field.
- You should change all calls to ``Contract.fromIdAndRecord`` to ``Contract.fromCreatedEvent``.

.. code-block:: java
// BEFORE
CreatedEvent event = ...;
Iou.Contract contract = Iou.Contract.fromIdAndRecord(event.getContractId(), event.getArguments()));
// AFTER
CreatedEvent event = ...;
Iou.Contract contract = Iou.Contract.fromCreatedEvent(event);
- Pass the ``data.CreatedEvent`` directly to the function returned by the decoder's ``getDecoder`` method.
If you are using the decoder utility class method ``fromCreatedEvent``, you don't need to change anything.

.. code-block:: java
CreatedEvent event = ...;
// BEFORE
Optional<BiFunction<String, Record, Contract>> decoder = MyDecoderUtility.getDecoder(MyTemplate.TEMPLATE_ID);
if (decoder.isPresent()) {
return decoder.get().apply(event.getContractId(), event.getArguments();
}
// AFTER
Optional<Function<CreatedEvent, Contract>> decoder = MyDecoderUtility.getDecoder(MyTemplate.TEMPLATE_ID);
if (decoder.isPresent()) {
return decoder.get().apply(event);
}
Scala Bindings
~~~~~~~~~~~~~~
- **BREAKING** You can now access the :ref:`agreement text <daml-ref-agreements>` of a contract with the new field ``Contract#agreementText: Option[String]``.
How to migrate:
- If you are pattern matching on ``com.digitalasset.ledger.client.binding.Contract``, you need to add a match clause for the added field.
- If you are constructing ``com.digitalasset.ledger.client.binding.Contract`` values, for example for tests, you need to add a constructor parameter for the agreement text.
Ledger
~~~~~~

- Renamed ``--jdbcurl`` to ``--sql-backend-jdbcurl``. Left ``--jdbcurl`` in place for backwards compat.
- Fixed issue when loading scenarios making use of ``pass`` into the sandbox, see
`#1079 <https://github.com/digital-asset/daml/pull/1079>`_.
- Fixed issue when loading scenarios that involve contract divulgence, see
`#1166 <https://github.com/digital-asset/daml/issues/1166>`_.
- Contract visibility is now properly checked when looking up contracts in the SQL backend, see
`#784 <https://github.com/digital-asset/daml/issues/784>`_.
- The sandbox now exposes the :ref:`agreement text <daml-ref-agreements>` of contracts in :ref:`CreatedEvents <com.digitalasset.ledger.api.v1.CreatedEvent>`. See `#1110 <https://github.com/digital-asset/daml/issues/1110>`__
Navigator
~~~~~~~~~
- Non-empty :ref:`agreement texts <daml-ref-agreements>` are now shown on the contract page above the section ``Contract details``, see `#1110 <https://github.com/digital-asset/daml/issues/1110>`__
.. _release-0-12-17:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ class BotTest extends FlatSpec with Matchers with DataLayerHelpers {
s"eid_$id",
templateId,
s"cid_$id",
new Record(List.empty[Record.Field].asJava))
new Record(List.empty[Record.Field].asJava),
Optional.empty())

def archive(event: CreatedEvent): ArchivedEvent =
new ArchivedEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package com.daml.ledger.rxjava.grpc.helpers

import java.time.Instant
import java.util
import java.util.Collections
import java.util.{Collections, Optional}

import com.daml.ledger.javaapi.data
import com.daml.ledger.testkit.services.TransactionServiceImpl
Expand Down Expand Up @@ -183,13 +183,27 @@ object TransactionGenerator {
val createdEventGen: Gen[(Created, data.CreatedEvent)] = for {
eventId <- nonEmptyId
contractId <- nonEmptyId
agreementText <- Gen.option(Gen.asciiStr)
(scalaTemplateId, javaTemplateId) <- identifierGen
(scalaRecord, javaRecord) <- Gen.sized(recordGen)
parties <- Gen.listOf(nonEmptyId)
} yield
(
Created(CreatedEvent(eventId, contractId, Some(scalaTemplateId), Some(scalaRecord), parties)),
new data.CreatedEvent(parties.asJava, eventId, javaTemplateId, contractId, javaRecord)
Created(
CreatedEvent(
eventId,
contractId,
Some(scalaTemplateId),
Some(scalaRecord),
parties,
agreementText = agreementText)),
new data.CreatedEvent(
parties.asJava,
eventId,
javaTemplateId,
contractId,
javaRecord,
agreementText.map(Optional.of[String]).getOrElse(Optional.empty()))
)

val archivedEventGen: Gen[(Archived, data.ArchivedEvent)] = for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
package com.daml.ledger.javaapi.data;

import com.digitalasset.ledger.api.v1.EventOuterClass;
import com.google.protobuf.StringValue;
import org.checkerframework.checker.nullness.qual.NonNull;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

public final class CreatedEvent implements Event, TreeEvent {

Expand All @@ -21,12 +23,15 @@ public final class CreatedEvent implements Event, TreeEvent {

private final Record arguments;

public CreatedEvent(@NonNull List<@NonNull String> witnessParties, @NonNull String eventId, @NonNull Identifier templateId, @NonNull String contractId, @NonNull Record arguments) {
private final Optional<String> agreementText;

public CreatedEvent(@NonNull List<@NonNull String> witnessParties, @NonNull String eventId, @NonNull Identifier templateId, @NonNull String contractId, @NonNull Record arguments, Optional<String> agreementText) {
this.witnessParties = witnessParties;
this.eventId = eventId;
this.templateId = templateId;
this.contractId = contractId;
this.arguments = arguments;
this.agreementText = agreementText;
}

@NonNull
Expand Down Expand Up @@ -58,6 +63,11 @@ public Record getArguments() {
return arguments;
}

@NonNull
public Optional<String> getAgreementText() {
return agreementText;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -67,13 +77,14 @@ public boolean equals(Object o) {
Objects.equals(eventId, that.eventId) &&
Objects.equals(templateId, that.templateId) &&
Objects.equals(contractId, that.contractId) &&
Objects.equals(arguments, that.arguments);
Objects.equals(arguments, that.arguments) &&
Objects.equals(agreementText, that.agreementText);
}

@Override
public int hashCode() {

return Objects.hash(witnessParties, eventId, templateId, contractId, arguments);
return Objects.hash(witnessParties, eventId, templateId, contractId, arguments, agreementText);
}

@Override
Expand All @@ -84,17 +95,19 @@ public String toString() {
", templateId=" + templateId +
", contractId='" + contractId + '\'' +
", arguments=" + arguments +
", agreementText='" + agreementText + '\'' +
'}';
}

public EventOuterClass.@NonNull CreatedEvent toProto() {
return EventOuterClass.CreatedEvent.newBuilder()
EventOuterClass.CreatedEvent.Builder builder = EventOuterClass.CreatedEvent.newBuilder()
.setContractId(getContractId())
.setCreateArguments(getArguments().toProtoRecord())
.setEventId(getEventId())
.setTemplateId(getTemplateId().toProto())
.addAllWitnessParties(this.getWitnessParties())
.build();
.addAllWitnessParties(this.getWitnessParties());
agreementText.ifPresent(a -> builder.setAgreementText(StringValue.of(a)));
return builder.build();
}

public static CreatedEvent fromProto(EventOuterClass.CreatedEvent createdEvent) {
Expand All @@ -103,7 +116,8 @@ public static CreatedEvent fromProto(EventOuterClass.CreatedEvent createdEvent)
createdEvent.getEventId(),
Identifier.fromProto(createdEvent.getTemplateId()),
createdEvent.getContractId(),
Record.fromProto(createdEvent.getCreateArguments()));
Record.fromProto(createdEvent.getCreateArguments()),
createdEvent.hasAgreementText() ? Optional.of(createdEvent.getAgreementText().getValue()) : Optional.empty());

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@

package com.digitalasset;

import com.daml.ledger.javaapi.data.CreateAndExerciseCommand;
import com.daml.ledger.javaapi.data.CreateCommand;
import com.daml.ledger.javaapi.data.ExerciseCommand;
import com.daml.ledger.javaapi.data.*;
import org.junit.jupiter.api.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import tests.template1.SimpleTemplate;
import tests.template1.TestTemplate_Int;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.util.Collections;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;

@RunWith(JUnitPlatform.class)
public class TemplateMethodTest {
Expand All @@ -21,6 +22,8 @@ public class TemplateMethodTest {
// at compilation time in case any of the methods are generated differently
// or not at all

private static Record simpleTemplateRecord = new Record(new Record.Field(new Party("Bob")));

@Test
void templateHasCreateMethods() {
CreateCommand fromStatic = SimpleTemplate.create("Bob");
Expand Down Expand Up @@ -50,5 +53,34 @@ void templateHasCreateAndExerciseMethods() {

assertNotNull(fromSplatted, "CreateAndExerciseCommand from splatted choice was null");
assertNotNull(fromRecord, "CreateAndExerciseCommand from record choice was null");
assertEquals(fromRecord, fromSplatted, "CreateAndExerciseCommands from both methods are not the same");
}

@Test
void contractHasDeprecatedFromIdAndRecord() {
SimpleTemplate.Contract contract = SimpleTemplate.Contract.fromIdAndRecord("SomeId", simpleTemplateRecord);
assertFalse(contract.agreementText.isPresent(), "Field agreementText should not be present");
}

@Test
void contractHasFromIdAndRecord() {
SimpleTemplate.Contract emptyAgreement = SimpleTemplate.Contract.fromIdAndRecord("SomeId", simpleTemplateRecord, Optional.empty());
assertFalse(emptyAgreement.agreementText.isPresent(), "Field agreementText should not be present");

SimpleTemplate.Contract nonEmptyAgreement = SimpleTemplate.Contract.fromIdAndRecord("SomeId", simpleTemplateRecord, Optional.of("I agree"));
assertTrue(nonEmptyAgreement.agreementText.isPresent(), "Field agreementText should be present");
assertEquals(nonEmptyAgreement.agreementText, Optional.of("I agree"), "Unexpected agreementText");
}

@Test
void contractHasFromCreatedEvent() {
CreatedEvent agreementEvent = new CreatedEvent(Collections.emptyList(), "eventId", SimpleTemplate.TEMPLATE_ID, "cid", simpleTemplateRecord, Optional.of("I agree"));
CreatedEvent noAgreementEvent = new CreatedEvent(Collections.emptyList(), "eventId", SimpleTemplate.TEMPLATE_ID, "cid", simpleTemplateRecord, Optional.empty());

SimpleTemplate.Contract withAgreement = SimpleTemplate.Contract.fromCreatedEvent(agreementEvent);
assertTrue(withAgreement.agreementText.isPresent(), "AgreementText was not present");

SimpleTemplate.Contract withoutAgreement = SimpleTemplate.Contract.fromCreatedEvent(noAgreementEvent);
assertFalse(withoutAgreement.agreementText.isPresent(), "AgreementText was present");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ template Wolpertinger
where
signatory owner

agreement name <> " has " <> show wings <> " wings and is " <> show age <> " years old."

controller owner can
Reproduce : ContractId Wolpertinger
with mateId : ContractId Wolpertinger
Expand Down
Loading

0 comments on commit c645348

Please sign in to comment.