Add ExerciseByKey command to Ledger API (#1724)

Fixes #1366

Also adds support for the new command to the Java bindings and codegen
This commit is contained in:
Stefano Baghino 2019-06-19 11:11:52 +02:00 committed by mergify[bot]
parent f2e6705ed7
commit 656e456b78
24 changed files with 825 additions and 44 deletions

View File

@ -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,

View File

@ -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")

View File

@ -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

View File

@ -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) =>

View File

@ -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"

View File

@ -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
*

View File

@ -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

View File

@ -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)
^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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
}
}
}
}
}

View File

@ -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
: ()

View File

@ -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

View File

@ -21,6 +21,18 @@ Java Codegen
- Support generic types (including tuples) as contract keys in codegen.
See `#1728 <https://github.com/digital-asset/daml/issues/1728>`__.
Ledger API
~~~~~~~~~~
- A new command ``ExerciseByKey`` allows to exercise choices on active contracts referring to them by their key.
See `#1366 <https://github.com/digital-asset/daml/issues/1366>`__.
Java Bindings
~~~~~~~~~~~~~
- The addition of the ``ExerciseByKey`` to the Ledger API is reflected in the bindings.
See `#1366 <https://github.com/digital-asset/daml/issues/1366>`__.
Release Procedure
~~~~~~~~~~~~~~~~~
@ -86,9 +98,6 @@ Release Procedure
- Fixes to the release procedure.
See `#1725 <https://github.com/digital-asset/daml/issues/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.

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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(

View File

@ -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.<java.util.Map.Entry<String,$L>,String,Value>toMap(java.util.Map.Entry::getKey, $L)))",
"new $T($L.entrySet().stream().collect($T.<$T<String,$L>,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)
}
}

View File

@ -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()

View File

@ -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.

View File

@ -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")

View File

@ -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(

View File

@ -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))
}

View File

@ -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

View File

@ -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,