From 656e456b788a90bad1639a5c881b659d6e118236 Mon Sep 17 00:00:00 2001 From: Stefano Baghino <43749967+stefanobaghino-da@users.noreply.github.com> Date: Wed, 19 Jun 2019 11:11:52 +0200 Subject: [PATCH] Add ExerciseByKey command to Ledger API (#1724) Fixes #1366 Also adds support for the new command to the Java bindings and codegen --- .../daml/lf/engine/CommandPreprocessor.scala | 39 +++ .../daml/lf/engine/EngineTest.scala | 251 +++++++++++++++++- .../digitalasset/daml/lf/speedy/Command.scala | 8 + .../daml/lf/speedy/Compiler.scala | 26 ++ daml-lf/tests/BasicTests.daml | 15 ++ .../daml/lf/command/Command.scala | 16 ++ .../code-snippets/Templates.daml | 9 + docs/source/app-dev/bindings-java/codegen.rst | 6 + .../ExerciseByKeyMySimpleTemplate.payload | 27 ++ .../app-dev/code-snippets/Templates.daml | 8 + .../app-dev/grpc/daml-to-ledger-api.rst | 4 + docs/source/support/release-notes.rst | 15 +- .../com/daml/ledger/javaapi/data/Command.java | 4 + .../javaapi/data/ExerciseByKeyCommand.java | 91 +++++++ .../com/digitalasset/CodegenLedgerTest.scala | 21 ++ .../backend/java/inner/TemplateClass.scala | 97 ++++++- .../backend/java/inner/ToValueGenerator.scala | 35 +-- .../java/inner/VariantConstructorClass.scala | 4 +- .../digitalasset/ledger/api/v1/commands.proto | 22 ++ .../api/validation/CommandsValidator.scala | 22 +- .../participant/util/LfEngineToApi.scala | 9 + .../participant/util/ValueConversions.scala | 11 +- .../apitesting/CommandTransactionChecks.scala | 62 ++++- .../ledger/api/LedgerTestingHelpers.scala | 67 ++++- 24 files changed, 825 insertions(+), 44 deletions(-) create mode 100644 docs/source/app-dev/code-snippets/ExerciseByKeyMySimpleTemplate.payload create mode 100644 language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/ExerciseByKeyCommand.java diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/CommandPreprocessor.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/CommandPreprocessor.scala index b39ee4bf65..9d2f807a72 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/CommandPreprocessor.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/CommandPreprocessor.scala @@ -365,6 +365,37 @@ private[engine] class CommandPreprocessor(compiledPackages: ConcurrentCompiledPa } ) + private[engine] def preprocessExerciseByKey( + templateId: Identifier, + contractKey: VersionedValue[AbsoluteContractId], + choiceId: ChoiceName, + actors: Set[Party], + argument: VersionedValue[AbsoluteContractId]): Result[(Type, SpeedyCommand)] = + Result.needTemplate( + compiledPackages, + templateId, + template => { + (template.choices.get(choiceId), template.key) match { + case (None, _) => + val choicesNames: Seq[String] = template.choices.toList.map(_._1) + ResultError(Error( + s"Couldn't find requested choice $choiceId for template $templateId. Available choices: $choicesNames")) + case (_, None) => + ResultError( + Error(s"Impossible to exercise by key, no key is defined for template $templateId")) + case (Some(choice), Some(ck)) => + val (_, choiceType) = choice.argBinder + val actingParties = ImmArray(actors.map(SValue.SParty)) + for { + arg <- translateValue(choiceType, argument) + key <- translateValue(ck.typ, contractKey) + } yield + choiceType -> SpeedyCommand + .ExerciseByKey(templateId, key, choiceId, actingParties, arg) + } + } + ) + private[engine] def preprocessCreateAndExercise( templateId: ValueRef, createArgument: VersionedValue[AbsoluteContractId], @@ -419,6 +450,14 @@ private[engine] class CommandPreprocessor(compiledPackages: ConcurrentCompiledPa choiceId, Set(submitter), argument) + case ExerciseByKeyCommand(templateId, contractKey, choiceId, submitter, argument) => + preprocessExerciseByKey( + templateId, + contractKey, + choiceId, + Set(submitter), + argument + ) case CreateAndExerciseCommand( templateId, createArgument, diff --git a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala index c41d6aa8dd..a05f671517 100644 --- a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala +++ b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/EngineTest.scala @@ -20,7 +20,7 @@ import com.digitalasset.daml.lf.speedy.SValue import com.digitalasset.daml.lf.speedy.SValue._ import com.digitalasset.daml.lf.command._ import com.digitalasset.daml.lf.value.ValueVersions.assertAsVersionedValue -import org.scalatest.{Matchers, WordSpec} +import org.scalatest.{EitherValues, Matchers, WordSpec} import scalaz.std.either._ import scalaz.syntax.apply._ @@ -32,7 +32,7 @@ import scala.language.implicitConversions "org.wartremover.warts.Serializable", "org.wartremover.warts.Product" )) -class EngineTest extends WordSpec with Matchers with BazelRunfiles { +class EngineTest extends WordSpec with Matchers with EitherValues with BazelRunfiles { import EngineTest._ @@ -91,12 +91,40 @@ class EngineTest extends WordSpec with Matchers with BazelRunfiles { )) } + def lookupContractWithKey( + @deprecated("shut up unused arguments warning", "blah") id: AbsoluteContractId) + : Option[ContractInst[Tx.Value[AbsoluteContractId]]] = { + Some( + ContractInst( + TypeConName(basicTestsPkgId, "BasicTests:WithKey"), + assertAsVersionedValue( + ValueRecord( + Some(BasicTests_WithKey), + ImmArray( + (Some("p"), ValueParty("Alice")), + (Some("k"), ValueInt64(42)) + ))), + "" + )) + } + def lookupPackage(pkgId: PackageId): Option[Package] = { allPackages.get(pkgId) } - def lookupKey(@deprecated("", "") key: GlobalKey): Option[AbsoluteContractId] = - sys.error("TODO keys in EngineTest") + val BasicTests_WithKey = Identifier(basicTestsPkgId, "BasicTests:WithKey") + + def lookupKey(key: GlobalKey): Option[AbsoluteContractId] = + key match { + case GlobalKey( + BasicTests_WithKey, + Value.VersionedValue( + _, + ValueRecord(_, ImmArray((_, ValueParty("Alice")), (_, ValueInt64(42)))))) => + Some(AbsoluteContractId("1")) + case _ => + None + } // TODO make these two per-test, so that we make sure not to pollute the package cache and other possibly mutable stuff val engine = Engine() @@ -215,6 +243,97 @@ class EngineTest extends WordSpec with Matchers with BazelRunfiles { res shouldBe 'right } + "translate exercise-by-key commands with argument with labels" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(42))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((Some[Name]("n"), ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractForPayout, lookupPackage, lookupKey) + res shouldBe 'right + } + + "translate exercise-by-key commands with argument without labels" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(42))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractForPayout, lookupPackage, lookupKey) + res shouldBe 'right + } + + "not translate exercise-by-key commands with argument with wrong labels" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(42))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((Some[Name]("WRONG"), ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractForPayout, lookupPackage, lookupKey) + res.left.value.msg should startWith("Missing record label n for record") + } + + "not translate exercise-by-key commands if the template specifies no key" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:CallablePayout") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(42))))), + "Transfer", + "Bob", + assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueParty("Clara"))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractForPayout, lookupPackage, lookupKey) + res.left.value.msg should startWith( + "Impossible to exercise by key, no key is defined for template") + } + + "not translate exercise-by-key commands if the given key does not match the type specified in the template" in { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueInt64(42)), (None, ValueInt64(42))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractForPayout, lookupPackage, lookupKey) + res.left.value.msg should startWith("mismatching type") + } + "translate create-and-exercise commands argument including labels" in { val id = Identifier(basicTestsPkgId, "BasicTests:CallablePayout") val let = Time.Timestamp.now() @@ -516,6 +635,130 @@ class EngineTest extends WordSpec with Matchers with BazelRunfiles { } } + "exercise-by-key command with missing key" should { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(43))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + res shouldBe 'right + + "fail at submission" in { + val submitResult = engine + .submit(Commands(ImmArray(command), let, "test")) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + submitResult.left.value.msg should startWith("dependency error: couldn't find key") + } + } + + "exercise-by-key command with existing key" should { + val templateId = Identifier(basicTestsPkgId, "BasicTests:WithKey") + val let = Time.Timestamp.now() + val command = ExerciseByKeyCommand( + templateId, + assertAsVersionedValue( + ValueRecord(None, ImmArray((None, ValueParty("Alice")), (None, ValueInt64(42))))), + "SumToK", + "Alice", + assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueInt64(5))))) + ) + + val res = commandTranslator + .preprocessCommands(Commands(ImmArray(command), let, "test")) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + res shouldBe 'right + val result = + res.flatMap(r => + engine.interpret(r, let).consume(lookupContractWithKey, lookupPackage, lookupKey)) + val tx = result.right.value + + "be translated" in { + val submitResult = engine + .submit(Commands(ImmArray(command), let, "test")) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + submitResult shouldBe result + } + + "reinterpret to the same result" in { + val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq + val reinterpretResult = + engine.reinterpret(txRoots, let).consume(lookupContractWithKey, lookupPackage, lookupKey) + (result |@| reinterpretResult)(_ isReplayedBy _) shouldBe Right(true) + } + + "be validated" in { + val validated = engine + .validate(tx, let) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + validated match { + case Left(e) => + fail(e.msg) + case Right(()) => () + } + } + + "post-commit validation passes" in { + val validated = engine + .validatePartial( + tx.mapContractIdAndValue(makeAbsoluteContractId, makeValueWithAbsoluteContractId), + Some("Alice"), + let, + "Alice", + makeAbsoluteContractId, + makeValueWithAbsoluteContractId + ) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + validated match { + case Left(e) => + fail(e.msg) + case Right(()) => + () + } + } + + "post-commit validation fails with missing root node" in { + val validated = engine + .validatePartial( + tx.mapContractIdAndValue(makeAbsoluteContractId, makeValueWithAbsoluteContractId) + .copy(nodes = Map.empty), + Some("Alice"), + let, + "Alice", + makeAbsoluteContractId, + makeValueWithAbsoluteContractId + ) + .consume(lookupContractWithKey, lookupPackage, lookupKey) + validated match { + case Left(e) + if e.msg == "invalid transaction, root refers to non-existing node NodeId(0)" => + () + case _ => + fail("expected failing validation on missing node") + } + } + + "events are collected" in { + val Right(blindingInfo) = + Blinding.checkAuthorizationAndBlind(tx, Set("Alice")) + val events = Event.collectEvents(tx, blindingInfo.explicitDisclosure) + val partyEvents = events.events.values.toList.filter(_.witnesses contains "Alice") + partyEvents.size shouldBe 1 + partyEvents(0) match { + case _: ExerciseEvent[Tx.NodeId, ContractId, Tx.Value[ContractId]] => succeed + case _ => fail("expected exercise") + } + } + } + "create-and-exercise command" should { val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple") val hello = Identifier(basicTestsPkgId, "BasicTests:Hello") diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Command.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Command.scala index 78fc7ad71c..59cb123081 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Command.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Command.scala @@ -27,6 +27,14 @@ object Command { argument: SValue ) extends Command + final case class ExerciseByKey( + templateId: Identifier, + contractKey: SValue, + choiceId: ChoiceName, + submitter: ImmArray[SParty], + argument: SValue + ) extends Command + final case class Fetch( templateId: Identifier, coid: SContractId diff --git a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Compiler.scala b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Compiler.scala index df8a43a974..3e1723f1bc 100644 --- a/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Compiler.scala +++ b/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/Compiler.scala @@ -1155,6 +1155,25 @@ final case class Compiler(packages: PackageId PartialFunction Package) { SEApp(SEVal(ChoiceDefRef(tmplId, choiceId), None), Array(actors, contractId, argument)) } + private def compileExerciseByKey( + tmplId: Identifier, + key: SExpr, + choiceId: ChoiceName, + // actors are either the singleton set of submitter of an exercise command, + // or the acting parties of an exercise node + // of a transaction under reconstruction for validation + optActors: Option[SExpr], + argument: SExpr): SExpr = { + withEnv { _ => + SEAbs(1) { + SELet( + SBUFetchKey(tmplId)(key, SEVar(1)), + SEApp(compileExercise(tmplId, SEVar(1), choiceId, optActors, argument), Array(SEVar(2))) + ) in SEVar(1) + } + } + } + private def compileCreateAndExercise( tmplId: Identifier, createArg: SValue, @@ -1187,6 +1206,13 @@ final case class Compiler(packages: PackageId PartialFunction Package) { choiceId, Some(SEValue(SList(FrontStack(submitters)))), SEValue(argument)) + case Command.ExerciseByKey(templateId, contractKey, choiceId, submitters, argument) => + compileExerciseByKey( + templateId, + SEValue(contractKey), + choiceId, + Some(SEValue(SList(FrontStack(submitters)))), + SEValue(argument)) case Command.Fetch(templateId, coid) => compileFetch(templateId, SEValue(coid)) case Command.CreateAndExercise(templateId, createArg, choice, choiceArg, submitters) => diff --git a/daml-lf/tests/BasicTests.daml b/daml-lf/tests/BasicTests.daml index 5b1ec36688..ccda883234 100644 --- a/daml-lf/tests/BasicTests.daml +++ b/daml-lf/tests/BasicTests.daml @@ -320,6 +320,21 @@ template TwoParties World : Text do pure "world" +template WithKey + with p: Party + k: Int + where + signatory p + + key (p, k): (Party, Int) + maintainer key._1 + + controller p can + nonconsuming SumToK : Int + with + n : Int + do pure (n + k) + test_failedAuths = scenario do alice <- getParty "alice" bob <- getParty "bob" diff --git a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/command/Command.scala b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/command/Command.scala index 670b46425e..e48f97fccf 100644 --- a/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/command/Command.scala +++ b/daml-lf/transaction/src/main/scala/com/digitalasset/daml/lf/command/Command.scala @@ -36,6 +36,22 @@ final case class ExerciseCommand( argument: VersionedValue[AbsoluteContractId]) extends Command +/** Command for exercising a choice on an existing contract specified by its key + * + * @param templateId identifier of the original contract + * @param contractKey key of the contract on which the choice is exercised + * @param choiceId identifier choice + * @param submitter party submitting the choice + * @param argument value passed for the choice + */ +final case class ExerciseByKeyCommand( + templateId: Identifier, + contractKey: VersionedValue[AbsoluteContractId], + choiceId: ChoiceName, + submitter: Party, + argument: VersionedValue[AbsoluteContractId]) + extends Command + /** Command for creating a contract and exercising a choice * on that existing contract within the same transaction * diff --git a/docs/source/app-dev/bindings-java/code-snippets/Templates.daml b/docs/source/app-dev/bindings-java/code-snippets/Templates.daml index e0116aeeb3..c0a999dfcc 100644 --- a/docs/source/app-dev/bindings-java/code-snippets/Templates.daml +++ b/docs/source/app-dev/bindings-java/code-snippets/Templates.daml @@ -5,12 +5,21 @@ daml 1.2 module Com.Acme where +data BarKey = + BarKey + with + p : Party + t : Text + template Bar with owner: Party name: Text where signatory owner + + key BarKey owner name : BarKey + maintainer key.p controller owner can Bar_SomeChoice: Bool diff --git a/docs/source/app-dev/bindings-java/codegen.rst b/docs/source/app-dev/bindings-java/codegen.rst index 34fe9d595a..38d4401e57 100644 --- a/docs/source/app-dev/bindings-java/codegen.rst +++ b/docs/source/app-dev/bindings-java/codegen.rst @@ -283,6 +283,10 @@ A file is generated that defines three Java classes: public final String owner; public final String name; + public static ExerciseByKeyCommand exerciseByKeyBar_SomeChoice(BarKey key, Bar_SomeChoice arg) { /* ... */ } + + public static ExerciseByKeyCommand exerciseByKeyBar_SomeChoice(BarKey key, String aName) { /* ... */ } + public CreateAndExerciseCommand createAndExerciseBar_SomeChoice(Bar_SomeChoice arg) { /* ... */ } public CreateAndExerciseCommand createAndExerciseBar_SomeChoice(String aName) { /* ... */ } @@ -305,6 +309,8 @@ A file is generated that defines three Java classes: } } +Note that the static methods returning an ``ExerciseByKeyCommand`` will only be generated for templates that define a key. + Variants (a.k.a sum types) ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/app-dev/code-snippets/ExerciseByKeyMySimpleTemplate.payload b/docs/source/app-dev/code-snippets/ExerciseByKeyMySimpleTemplate.payload new file mode 100644 index 0000000000..9c10293df0 --- /dev/null +++ b/docs/source/app-dev/code-snippets/ExerciseByKeyMySimpleTemplate.payload @@ -0,0 +1,27 @@ +{ // ExerciseByKeyCommand + template_id { // Identifier + package_id: "some-hash" + name: "Templates.MySimpleTemplate" + } + contract_key { // Value + record { // Record + fields { // RecordField + label: "party" + value { // Value + party: "Alice" + } + } + } + } + choice: "MyChoice" + choice_argument { // Value + record { // Record + fields { // RecordField + label: "parameter" + value { // Value + int64: 42 + } + } + } + } +} diff --git a/docs/source/app-dev/code-snippets/Templates.daml b/docs/source/app-dev/code-snippets/Templates.daml index 63ddb6eca3..97cd886b0e 100644 --- a/docs/source/app-dev/code-snippets/Templates.daml +++ b/docs/source/app-dev/code-snippets/Templates.daml @@ -4,12 +4,20 @@ daml 1.2 module Templates where +data MySimpleTemplateKey = + MySimpleTemplateKey + with + party: Party + template MySimpleTemplate with owner: Party where signatory owner + key MySimpleTemplateKey owner: MySimpleTemplateKey + maintainer key.party + controller owner can MyChoice : () diff --git a/docs/source/app-dev/grpc/daml-to-ledger-api.rst b/docs/source/app-dev/grpc/daml-to-ledger-api.rst index 9d10e8d68c..ea62d3383f 100644 --- a/docs/source/app-dev/grpc/daml-to-ledger-api.rst +++ b/docs/source/app-dev/grpc/daml-to-ledger-api.rst @@ -104,3 +104,7 @@ Exercising a choice A choice is exercised by sending an :ref:`com.digitalasset.ledger.api.v1.exercisecommand`. Taking the same contract template again, exercising the choice ``MyChoice`` would result in a command similar to the following: .. literalinclude:: ../code-snippets/ExerciseMySimpleTemplate.payload + +If the template specifies a key, the :ref:`com.digitalasset.ledger.api.v1.exercisebykeycommand` can be used. It works in a similar way as :ref:`com.digitalasset.ledger.api.v1.exercisecommand`, but instead of specifying the contract identifier you have to provide its key. The example above could be rewritten as follows: + +.. literalinclude:: ../code-snippets/ExerciseByKeyMySimpleTemplate.payload diff --git a/docs/source/support/release-notes.rst b/docs/source/support/release-notes.rst index f58bffaf9e..3e970f4949 100644 --- a/docs/source/support/release-notes.rst +++ b/docs/source/support/release-notes.rst @@ -21,6 +21,18 @@ Java Codegen - Support generic types (including tuples) as contract keys in codegen. See `#1728 `__. +Ledger API +~~~~~~~~~~ + +- A new command ``ExerciseByKey`` allows to exercise choices on active contracts referring to them by their key. + See `#1366 `__. + +Java Bindings +~~~~~~~~~~~~~ + +- The addition of the ``ExerciseByKey`` to the Ledger API is reflected in the bindings. + See `#1366 `__. + Release Procedure ~~~~~~~~~~~~~~~~~ @@ -86,9 +98,6 @@ Release Procedure - Fixes to the release procedure. See `#1725 `__ -Java Bindings -~~~~~~~~~~~~~ - - The changes for Java Bindings listed for SDK 0.13.1 now only apply to SDK 0.13.2 and later. This is due to the partial failure of the release procedure. diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/Command.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/Command.java index ac9e313a4b..535fdfda3e 100644 --- a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/Command.java +++ b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/Command.java @@ -19,6 +19,8 @@ public abstract class Command { return ExerciseCommand.fromProto(command.getExercise()); case CREATEANDEXERCISE: return CreateAndExerciseCommand.fromProto(command.getCreateAndExercise()); + case EXERCISEBYKEY: + return ExerciseByKeyCommand.fromProto(command.getExerciseByKey()); case COMMAND_NOT_SET: default: throw new ProtoCommandUnknown(command); @@ -33,6 +35,8 @@ public abstract class Command { builder.setExercise(((ExerciseCommand) this).toProto()); } else if (this instanceof CreateAndExerciseCommand) { builder.setCreateAndExercise(((CreateAndExerciseCommand) this).toProto()); + } else if (this instanceof ExerciseByKeyCommand) { + builder.setExerciseByKey(((ExerciseByKeyCommand) this).toProto()); } else { throw new CommandUnknown(this); } diff --git a/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/ExerciseByKeyCommand.java b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/ExerciseByKeyCommand.java new file mode 100644 index 0000000000..c66a1601c1 --- /dev/null +++ b/language-support/java/bindings/src/main/java/com/daml/ledger/javaapi/data/ExerciseByKeyCommand.java @@ -0,0 +1,91 @@ +// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.ledger.javaapi.data; + +import com.digitalasset.ledger.api.v1.CommandsOuterClass; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Objects; + +public class ExerciseByKeyCommand extends Command { + + private final Identifier templateId; + + private final Value contractKey; + + private final String choice; + + private final Value choiceArgument; + + public ExerciseByKeyCommand(@NonNull Identifier templateId, @NonNull Value contractKey, @NonNull String choice, @NonNull Value choiceArgument) { + this.templateId = templateId; + this.contractKey = contractKey; + this.choice = choice; + this.choiceArgument = choiceArgument; + } + + public static ExerciseByKeyCommand fromProto(CommandsOuterClass.ExerciseByKeyCommand command) { + Identifier templateId = Identifier.fromProto(command.getTemplateId()); + Value contractKey = Value.fromProto(command.getContractKey()); + String choice = command.getChoice(); + Value choiceArgument = Value.fromProto(command.getChoiceArgument()); + return new ExerciseByKeyCommand(templateId, contractKey, choice, choiceArgument); + } + + public CommandsOuterClass.ExerciseByKeyCommand toProto() { + return CommandsOuterClass.ExerciseByKeyCommand.newBuilder() + .setTemplateId(this.templateId.toProto()) + .setContractKey(this.contractKey.toProto()) + .setChoice(this.choice) + .setChoiceArgument(this.choiceArgument.toProto()) + .build(); + } + + @NonNull + @Override + public Identifier getTemplateId() { + return templateId; + } + + @NonNull + public Value getContractKey() { + return contractKey; + } + + @NonNull + public String getChoice() { + return choice; + } + + @NonNull + public Value getChoiceArgument() { + return choiceArgument; + } + + @Override + public String toString() { + return "ExerciseByKeyCommand{" + + "templateId=" + templateId + + ", contractKey='" + contractKey + '\'' + + ", choice='" + choice + '\'' + + ", choiceArgument=" + choiceArgument + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExerciseByKeyCommand that = (ExerciseByKeyCommand) o; + return Objects.equals(templateId, that.templateId) && + Objects.equals(contractKey, that.contractKey) && + Objects.equals(choice, that.choice) && + Objects.equals(choiceArgument, that.choiceArgument); + } + + @Override + public int hashCode() { + return Objects.hash(templateId, contractKey, choice, choiceArgument); + } +} diff --git a/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/CodegenLedgerTest.scala b/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/CodegenLedgerTest.scala index 541a410879..ddcf16e05c 100644 --- a/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/CodegenLedgerTest.scala +++ b/language-support/java/codegen/src/ledger-tests/scala/com/digitalasset/CodegenLedgerTest.scala @@ -226,4 +226,25 @@ class CodegenLedgerTest extends FlatSpec with Matchers with BazelRunfiles { wolpertinger.key.get.owner shouldEqual "Alice" wolpertinger.key.get.age shouldEqual java.math.BigDecimal.valueOf(17.42) } + + it should "be able to exercise by key" in withClient { client => + sendCmd(client, glookofly.create(), sruquito.create()) + + // We'll exercise by key, no need to get the handles + val glookoflyContract :: sruquitoContract :: Nil = readActiveContracts(client) + + val tob = Instant.now().`with`(ChronoField.NANO_OF_SECOND, 0) + val reproduceByKeyCmd = + Wolpertinger.exerciseByKeyReproduce(glookoflyContract.key.get, sruquitoContract.id, tob) + sendCmd(client, reproduceByKeyCmd) + + val wolpertingers = readActiveContracts(client) + wolpertingers should have length 2 + + val sruq :: glookosruq :: Nil = wolpertingers + + sruq.data.name shouldEqual sruquito.name + glookosruq.data.name shouldEqual s"${glookofly.name}-${sruquito.name}" + glookosruq.data.timeOfBirth shouldEqual tob + } } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala index 09ddfc5a18..3734666b56 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/TemplateClass.scala @@ -36,6 +36,14 @@ private[inner] object TemplateClass extends StrictLogging { .superclass(classOf[javaapi.data.Template]) .addField(generateTemplateIdField(typeWithContext)) .addMethod(generateCreateMethod(className)) + .addMethods( + generateStaticExerciseByKeyMethods( + className, + template.choices, + template.key, + typeWithContext.interface.typeDecls, + typeWithContext.packageId, + packagePrefixes)) .addMethods( generateCreateAndExerciseMethods( className, @@ -324,6 +332,94 @@ private[inner] object TemplateClass extends StrictLogging { } else None } + private def generateStaticExerciseByKeyMethods( + templateClassName: ClassName, + choices: Map[ChoiceName, TemplateChoice[Type]], + maybeKey: Option[Type], + typeDeclarations: Map[QualifiedName, InterfaceType], + packageId: PackageId, + packagePrefixes: Map[PackageId, String]) = + maybeKey.fold(java.util.Collections.emptyList[MethodSpec]()) { key => + val methods = for ((choiceName, choice) <- choices.toList) yield { + val raw = generateStaticExerciseByKeyMethod( + choiceName, + choice, + key, + templateClassName, + packagePrefixes) + val flattened = for (record <- choice.param + .fold(getRecord(_, typeDeclarations, packageId), _ => None, _ => None)) yield { + generateFlattenedStaticExerciseByKeyMethod( + choiceName, + choice, + key, + templateClassName, + getFieldsWithTypes(record.fields, packagePrefixes), + packagePrefixes) + } + raw :: flattened.toList + } + methods.flatten.asJava + } + + private def generateStaticExerciseByKeyMethod( + choiceName: ChoiceName, + choice: TemplateChoice[Type], + key: Type, + templateClassName: ClassName, + packagePrefixes: Map[PackageId, String]): MethodSpec = { + val exerciseByKeyBuilder = MethodSpec + .methodBuilder(s"exerciseByKey${choiceName.capitalize}") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(classOf[javaapi.data.ExerciseByKeyCommand]) + val keyJavaType = toJavaTypeName(key, packagePrefixes) + exerciseByKeyBuilder.addParameter(keyJavaType, "key") + val choiceJavaType = toJavaTypeName(choice.param, packagePrefixes) + exerciseByKeyBuilder.addParameter(choiceJavaType, "arg") + val choiceArgument = choice.param match { + case TypeCon(_, _) => "arg.toValue()" + case TypePrim(_, _) => "arg" + case TypeVar(_) => "arg" + } + exerciseByKeyBuilder.addStatement( + "return new $T($T.TEMPLATE_ID, $L, $S, $L)", + classOf[javaapi.data.ExerciseByKeyCommand], + templateClassName, + ToValueGenerator + .generateToValueConverter(key, CodeBlock.of("key"), newNameGenerator, packagePrefixes), + choiceName, + choiceArgument + ) + exerciseByKeyBuilder.build() + } + + private def generateFlattenedStaticExerciseByKeyMethod( + choiceName: ChoiceName, + choice: TemplateChoice[Type], + key: Type, + templateClassName: ClassName, + fields: Fields, + packagePrefixes: Map[PackageId, String]): MethodSpec = { + val methodName = s"exerciseByKey${choiceName.capitalize}" + val exerciseByKeyBuilder = MethodSpec + .methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(classOf[javaapi.data.ExerciseByKeyCommand]) + val keyJavaType = toJavaTypeName(key, packagePrefixes) + exerciseByKeyBuilder.addParameter(keyJavaType, "key") + val choiceJavaType = toJavaTypeName(choice.param, packagePrefixes) + for (FieldInfo(_, _, javaName, javaType) <- fields) { + exerciseByKeyBuilder.addParameter(javaType, javaName) + } + exerciseByKeyBuilder.addStatement( + "return $T.$L(key, new $T($L))", + templateClassName, + methodName, + choiceJavaType, + generateArgumentList(fields.map(_.javaName))) + exerciseByKeyBuilder.build() + } + private def generateCreateAndExerciseMethods( templateClassName: ClassName, choices: Map[ChoiceName, TemplateChoice[com.digitalasset.daml.lf.iface.Type]], @@ -344,7 +440,6 @@ private[inner] object TemplateClass extends StrictLogging { createAndExerciseChoiceMethod :: splatted.toList } methods.flatten.asJava - } private def generateCreateAndExerciseMethod( diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/ToValueGenerator.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/ToValueGenerator.scala index b74294b5f6..2eaad86a94 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/ToValueGenerator.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/ToValueGenerator.scala @@ -55,7 +55,6 @@ object ToValueGenerator { new Integer(fields.length)) for (FieldInfo(damlName, damlType, javaName, _) <- fields) { - val anonNameGen = newNameGenerator toValueMethod.addStatement( "fields.add(new $T($S, $L))", classOf[javaapi.data.Record.Field], @@ -63,7 +62,7 @@ object ToValueGenerator { generateToValueConverter( damlType, CodeBlock.of("this.$L", javaName), - () => anonNameGen.next(), + newNameGenerator, packagePrefixes) ) } @@ -74,7 +73,7 @@ object ToValueGenerator { def generateToValueConverter( damlType: Type, accessor: CodeBlock, - args: () => String, + args: Iterator[String], packagePrefixes: Map[PackageId, String]): CodeBlock = { damlType match { case TypeVar(tvName) => @@ -91,7 +90,7 @@ object ToValueGenerator { case TypePrim(PrimTypeUnit, _) => CodeBlock.of("$T.getInstance()", classOf[javaapi.data.Unit]) case TypePrim(PrimTypeList, ImmArraySeq(param)) => - val arg = args() + val arg = args.next() val extractor = CodeBlock.of( "$L -> $L", arg, @@ -106,7 +105,7 @@ object ToValueGenerator { ) case TypePrim(PrimTypeOptional, ImmArraySeq(param)) => - val arg = args() + val arg = args.next() val wrapped = generateToValueConverter(param, CodeBlock.of("$L", arg), args, packagePrefixes) val extractor = CodeBlock.of("$L -> $L", arg, wrapped) @@ -120,27 +119,29 @@ object ToValueGenerator { ) case TypePrim(PrimTypeMap, ImmArraySeq(param)) => - val arg = args() + val arg = args.next() val extractor = CodeBlock.of( "$L -> $L", arg, generateToValueConverter(param, CodeBlock.of("$L.getValue()", arg), args, packagePrefixes) ) CodeBlock.of( - "new $T($L.entrySet().stream().collect($T.,String,Value>toMap(java.util.Map.Entry::getKey, $L)))", + "new $T($L.entrySet().stream().collect($T.<$T,String,Value>toMap($T::getKey, $L)))", apiMap, accessor, classOf[Collectors], + classOf[java.util.Map.Entry[_, _]], toJavaTypeName(param, packagePrefixes), + classOf[java.util.Map.Entry[_, _]], extractor ) case TypePrim(PrimTypeContractId, _) | TypeCon(_, Seq()) => CodeBlock.of("$L.toValue()", accessor) - case TypeCon(constructor, typeParameters) => + case TypeCon(_, typeParameters) => val extractorParams = typeParameters.map { ta => - val arg = args() + val arg = args.next() val wrapped = generateToValueConverter(ta, CodeBlock.of("$L", arg), args, packagePrefixes) val extractor = CodeBlock.of("$L -> $L", arg, wrapped) extractor @@ -153,20 +154,4 @@ object ToValueGenerator { } } - private def initBuilder(method: MethodSpec, codeBlock: CodeBlock): MethodSpec.Builder = - if (method.isConstructor) { - MethodSpec - .constructorBuilder() - .addStatement("this($L)", codeBlock) - } else if (method.returnType == TypeName.VOID) { - MethodSpec - .methodBuilder(method.name) - .addStatement("$L($L)", method.name, codeBlock) - } else { - MethodSpec - .methodBuilder(method.name) - .returns(method.returnType) - .addStatement("return $L($L)", method.name, codeBlock) - } - } diff --git a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantConstructorClass.scala b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantConstructorClass.scala index 844ce391dc..f24686c6b8 100644 --- a/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantConstructorClass.scala +++ b/language-support/java/codegen/src/main/scala/com/digitalasset/daml/lf/codegen/backend/java/inner/VariantConstructorClass.scala @@ -62,8 +62,6 @@ object VariantConstructorClass extends StrictLogging { body: Type, fieldName: String, packagePrefixes: Map[PackageId, String]) = { - val anonNameGen = newNameGenerator - val extractorParameters = ToValueExtractorParameters.generate(typeArgs) MethodSpec @@ -79,7 +77,7 @@ object VariantConstructorClass extends StrictLogging { .generateToValueConverter( body, CodeBlock.of("this.$L", fieldName), - () => anonNameGen.next(), + newNameGenerator, packagePrefixes) ) .build() diff --git a/ledger-api/grpc-definitions/com/digitalasset/ledger/api/v1/commands.proto b/ledger-api/grpc-definitions/com/digitalasset/ledger/api/v1/commands.proto index 631a90a60a..cceb08bd73 100644 --- a/ledger-api/grpc-definitions/com/digitalasset/ledger/api/v1/commands.proto +++ b/ledger-api/grpc-definitions/com/digitalasset/ledger/api/v1/commands.proto @@ -63,6 +63,7 @@ message Command { oneof command { CreateCommand create = 1; ExerciseCommand exercise = 2; + ExerciseByKeyCommand exerciseByKey = 4; CreateAndExerciseCommand createAndExercise = 3; } } @@ -101,6 +102,27 @@ message ExerciseCommand { Value choice_argument = 4; } +// Exercise a choice on an existing contract specified by its key. +message ExerciseByKeyCommand { + + // The template of contract the client wants to exercise. + // Required + Identifier template_id = 1; + + // The key of the contract the client wants to exercise upon. + // Required + Value contract_key = 2; + + // The name of the choice the client wants to exercise. + // Must be a valid NameString (as described in ``value.proto``) + // Required + string choice = 3; + + // The argument for this choice. + // Required + Value choice_argument = 4; +} + // Create a contract and exercise a choice on it in the same transaction. message CreateAndExerciseCommand { // The template of the contract the client wants to create. diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/ledger/api/validation/CommandsValidator.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/ledger/api/validation/CommandsValidator.scala index 91e2e5e883..4f15e9f44c 100644 --- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/ledger/api/validation/CommandsValidator.scala +++ b/ledger/ledger-api-common/src/main/scala/com/digitalasset/ledger/api/validation/CommandsValidator.scala @@ -12,7 +12,8 @@ import com.digitalasset.ledger.api.v1.commands.Command.Command.{ Create => ProtoCreate, CreateAndExercise => ProtoCreateAndExercise, Empty => ProtoEmpty, - Exercise => ProtoExercise + Exercise => ProtoExercise, + ExerciseByKey => ProtoExerciseByKey } import com.digitalasset.ledger.api.v1.commands.{Command => ProtoCommand, Commands => ProtoCommands} import com.digitalasset.ledger.api.v1.value.Value.Sum @@ -114,6 +115,25 @@ final class CommandsValidator(ledgerId: LedgerId, identifierResolver: Identifier choiceId = choice, submitter = submitter, argument = asVersionedValueOrThrow(validatedValue)) + + case ek: ProtoExerciseByKey => + for { + templateId <- requirePresence(ek.value.templateId, "template_id") + validatedTemplateId <- identifierResolver.resolveIdentifier(templateId) + contractKey <- requirePresence(ek.value.contractKey, "contract_key") + validatedContractKey <- validateValue(contractKey) + choice <- requireName(ek.value.choice, "choice") + value <- requirePresence(ek.value.choiceArgument, "value") + validatedValue <- validateValue(value) + } yield + ExerciseByKeyCommand( + templateId = validatedTemplateId, + contractKey = asVersionedValueOrThrow(validatedContractKey), + choiceId = choice, + submitter = submitter, + argument = asVersionedValueOrThrow(validatedValue) + ) + case ce: ProtoCreateAndExercise => for { templateId <- requirePresence(ce.value.templateId, "template_id") diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala index e0ad091cab..f8793ef6f1 100644 --- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala +++ b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/LfEngineToApi.scala @@ -16,6 +16,7 @@ import com.digitalasset.ledger.api.v1.commands.{ Commands => ApiCommands, CreateCommand => ApiCreateCommand, ExerciseCommand => ApiExerciseCommand, + ExerciseByKeyCommand => ApiExerciseByKeyCommand, CreateAndExerciseCommand => ApiCreateAndExerciseCommand } import com.digitalasset.ledger.api.v1.value.{ @@ -186,6 +187,14 @@ object LfEngineToApi { contractId, choiceId, LfEngineToApi.lfValueToApiValue(verbose = true, argument.value).toOption))) + case ExerciseByKeyCommand(templateId, contractKey, choiceId, _, argument) => + ApiCommand( + ApiCommand.Command.ExerciseByKey(ApiExerciseByKeyCommand( + Some(toApiIdentifier(templateId)), + LfEngineToApi.lfValueToApiValue(verbose = true, contractKey.value).toOption, + choiceId, + LfEngineToApi.lfValueToApiValue(verbose = true, argument.value).toOption + ))) case CreateAndExerciseCommand(templateId, createArgument, choiceId, choiceArgument, _) => ApiCommand( ApiCommand.Command.CreateAndExercise(ApiCreateAndExerciseCommand( diff --git a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/ValueConversions.scala b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/ValueConversions.scala index 9ce6e141ee..45fa95a2b3 100644 --- a/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/ValueConversions.scala +++ b/ledger/ledger-api-common/src/main/scala/com/digitalasset/platform/participant/util/ValueConversions.scala @@ -6,7 +6,12 @@ package com.digitalasset.platform.participant.util import java.time.Instant import java.util.concurrent.TimeUnit -import com.digitalasset.ledger.api.v1.commands.{Command, CreateCommand, ExerciseCommand} +import com.digitalasset.ledger.api.v1.commands.{ + Command, + CreateCommand, + ExerciseByKeyCommand, + ExerciseCommand +} import com.digitalasset.ledger.api.v1.value.Value.Sum import com.digitalasset.ledger.api.v1.value.Value.Sum.{ ContractId, @@ -80,6 +85,10 @@ object ValueConversions { def wrap = Command(Command.Command.Exercise(exercise)) } + implicit class ExerciseByKeyCommands(val exerciseByKey: ExerciseByKeyCommand) extends AnyVal { + def wrap = Command(Command.Command.ExerciseByKey(exerciseByKey)) + } + implicit class CreateCommands(val create: CreateCommand) extends AnyVal { def wrap = Command(Command.Command.Create(create)) } diff --git a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/apitesting/CommandTransactionChecks.scala b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/apitesting/CommandTransactionChecks.scala index bbca608d68..984948ffc8 100644 --- a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/apitesting/CommandTransactionChecks.scala +++ b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/apitesting/CommandTransactionChecks.scala @@ -653,8 +653,6 @@ abstract class CommandTransactionChecks val key = "some-key" val alice = "Alice" val bob = "Bob" - def textKeyValue(p: String, k: String, disclosedTo: List[String]): Value = - Value(Value.Sum.Record(textKeyRecord(p, k, disclosedTo))) def textKeyKey(p: String, k: String): Value = Value(Value.Sum.Record(Record(fields = List(RecordField(value = p.asParty), RecordField(value = s"$keyPrefix-$k".asText))))) for { @@ -775,7 +773,7 @@ abstract class CommandTransactionChecks cid1.contractId, "TextKeyChoice", emptyRecordValue) - lookupAfterConsume <- ctx.testingHelpers.simpleExercise( + _ <- ctx.testingHelpers.simpleExercise( "CK-test-alice-lookup-after-consume", alice, templateIds.textKeyOperations, @@ -848,7 +846,63 @@ abstract class CommandTransactionChecks succeed } } + + "handle exercise by key" in allFixtures { ctx => + val keyPrefix = UUID.randomUUID.toString + def textKeyRecord(p: String, k: String, disclosedTo: List[String]): Record = + Record( + fields = + List( + RecordField(value = p.asParty), + RecordField(value = s"$keyPrefix-$k".asText), + RecordField(value = disclosedTo.map(_.asParty).asList))) + val key = "some-key" + val alice = "Alice" + val bob = "Bob" + def textKeyKey(p: String, k: String): Value = + Value(Value.Sum.Record(Record(fields = List(RecordField(value = p.asParty), RecordField(value = s"$keyPrefix-$k".asText))))) + for { + _ <- ctx.testingHelpers.failingExerciseByKey( + "EK-test-alice-exercise-before-create", + alice, + templateIds.textKey, + textKeyKey(alice, key), + "TextKeyChoice", + emptyRecordValue, + Code.INVALID_ARGUMENT, + "couldn't find key" + ) + _ <- ctx.testingHelpers.simpleCreate( + "EK-test-cid1", + alice, + templateIds.textKey, + textKeyRecord(alice, key, List(bob)) + ) + // now we exercise by key, thus archiving it, and then verify + // that we cannot look it up anymore + _ <- ctx.testingHelpers.simpleExerciseByKey( + "EK-test-alice-exercise", + alice, + templateIds.textKey, + textKeyKey(alice, key), + "TextKeyChoice", + emptyRecordValue) + _ <- ctx.testingHelpers.failingExerciseByKey( + "EK-test-alice-exercise-consumed", + alice, + templateIds.textKey, + textKeyKey(alice, key), + "TextKeyChoice", + emptyRecordValue, + Code.INVALID_ARGUMENT, + "couldn't find key" + ) + } yield { + succeed + } + } } + "client sends a CreateAndExerciseCommand" should { val validCreateAndExercise = CreateAndExerciseCommand( Some(templateIds.dummy), @@ -856,8 +910,6 @@ abstract class CommandTransactionChecks "DummyChoice1", Some(Value(Value.Sum.Record(Record()))) ) - val ledgerEnd = - LedgerOffset(LedgerOffset.Value.Boundary(LedgerOffset.LedgerBoundary.LEDGER_END)) val partyFilter = TransactionFilter(Map(party -> Filters(None))) def newRequest(context: LedgerContext, cmd: CreateAndExerciseCommand) = submitRequest diff --git a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/tests/integration/ledger/api/LedgerTestingHelpers.scala b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/tests/integration/ledger/api/LedgerTestingHelpers.scala index 300746152e..bdd75ab4dd 100644 --- a/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/tests/integration/ledger/api/LedgerTestingHelpers.scala +++ b/ledger/ledger-api-integration-tests/src/test/lib/scala/com/digitalasset/platform/tests/integration/ledger/api/LedgerTestingHelpers.scala @@ -11,7 +11,11 @@ import com.digitalasset.ledger.api.v1.command_service.{ SubmitAndWaitRequest } import com.digitalasset.ledger.api.v1.command_submission_service.SubmitRequest -import com.digitalasset.ledger.api.v1.commands.{CreateCommand, ExerciseCommand} +import com.digitalasset.ledger.api.v1.commands.{ + CreateCommand, + ExerciseByKeyCommand, + ExerciseCommand +} import com.digitalasset.ledger.api.v1.completion.Completion import com.digitalasset.ledger.api.v1.event.Event.Event.{Archived, Created} import com.digitalasset.ledger.api.v1.event.{ArchivedEvent, CreatedEvent, Event, ExercisedEvent} @@ -428,6 +432,28 @@ class LedgerTestingHelpers( ) } + // Exercise a choice by key and return all resulting create events. + def simpleExerciseByKeyWithListener( + commandId: String, + submitter: String, + listener: String, + template: Identifier, + contractKey: Value, + choice: String, + arg: Value + ): Future[TransactionTree] = { + submitAndListenForSingleTreeResultOfCommand( + submitRequestWithId(commandId) + .update( + _.commands.commands := + List(ExerciseByKeyCommand(Some(template), Some(contractKey), choice, Some(arg)).wrap), + _.commands.party := submitter + ), + TransactionFilter(Map(listener -> Filters.defaultInstance)), + false + ) + } + def simpleCreateWithListenerForTransactions( commandId: String, submitter: String, @@ -563,6 +589,23 @@ class LedgerTestingHelpers( ): Future[TransactionTree] = simpleExerciseWithListener(commandId, submitter, submitter, template, contractId, choice, arg) + def simpleExerciseByKey( + commandId: String, + submitter: String, + template: Identifier, + contractKey: Value, + choice: String, + arg: Value + ): Future[TransactionTree] = + simpleExerciseByKeyWithListener( + commandId, + submitter, + submitter, + template, + contractKey, + choice, + arg) + // Exercise a choice that is supposed to fail. def failingExercise( commandId: String, @@ -585,6 +628,28 @@ class LedgerTestingHelpers( pattern ) + // Exercise a choice by key that is supposed to fail. + def failingExerciseByKey( + commandId: String, + submitter: String, + template: Identifier, + contractKey: Value, + choice: String, + arg: Value, + code: Code, + pattern: String + ): Future[Assertion] = + assertCommandFailsWithCode( + submitRequestWithId(commandId) + .update( + _.commands.commands := + List(ExerciseByKeyCommand(Some(template), contractKey, choice, Some(arg)).wrap), + _.commands.party := submitter + ), + code, + pattern + ) + def listenForCompletionAsApplication( applicationId: String, requestingParty: String,