mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-05 03:56:26 +03:00
Add CreateAndExercise command throughout the stack (#563)
* Add release notes entry * Add CreateAndExercise command to Java Bindings data layer * Add CreateAndExercise command to DAMLe The CreateAndExerciseCommand allows users to create a contract and exercise a choice on it within the same transaction. Users can use this method to implement "callable update functions" by creating a template that calls the update function in a choice body. Fixes #382. * Add CreateAndExercise command handling to the sandbox * Add CreateAndExercise command to the Ledger API
This commit is contained in:
parent
0081fd6ea6
commit
7f8dbfeca0
@ -8,9 +8,9 @@ import com.digitalasset.daml.lf.value.Value._
|
||||
|
||||
import com.digitalasset.daml.lf.data.Time
|
||||
|
||||
// --------------------------------
|
||||
// Accepted commads coming from API
|
||||
// --------------------------------
|
||||
// ---------------------------------
|
||||
// Accepted commands coming from API
|
||||
// ---------------------------------
|
||||
sealed trait Command extends Product with Serializable
|
||||
|
||||
/** Command for creating a contract
|
||||
@ -37,6 +37,23 @@ final case class ExerciseCommand(
|
||||
argument: VersionedValue[AbsoluteContractId])
|
||||
extends Command
|
||||
|
||||
/** Command for creating a contract and exercising a choice
|
||||
* on that existing contract within the same transaction
|
||||
*
|
||||
* @param templateId identifier of the original contract
|
||||
* @param createArgument value passed to the template
|
||||
* @param choiceId identifier choice
|
||||
* @param choiceArgument value passed for the choice
|
||||
* @param submitter party submitting the choice
|
||||
*/
|
||||
final case class CreateAndExerciseCommand(
|
||||
templateId: Identifier,
|
||||
createArgument: VersionedValue[AbsoluteContractId],
|
||||
choiceId: String,
|
||||
choiceArgument: VersionedValue[AbsoluteContractId],
|
||||
submitter: SimpleString)
|
||||
extends Command
|
||||
|
||||
/** Commands input adapted from ledger-api
|
||||
*
|
||||
* @param commands a batch of commands to be interpreted/executed
|
||||
|
@ -276,7 +276,7 @@ private[engine] class CommandPreprocessor(compiledPackages: ConcurrentCompiledPa
|
||||
|
||||
private[engine] def preprocessExercise(
|
||||
templateId: Identifier,
|
||||
contractId: AbsoluteContractId,
|
||||
contractId: ContractId,
|
||||
choiceId: ChoiceName,
|
||||
// actors are either the singleton set of submitter of an exercise command,
|
||||
// or the acting parties of an exercise node
|
||||
@ -302,30 +302,73 @@ private[engine] class CommandPreprocessor(compiledPackages: ConcurrentCompiledPa
|
||||
}
|
||||
)
|
||||
|
||||
private[engine] def buildUpdate(bindings: ImmArray[(Type, Expr)]): Expr = {
|
||||
bindings.length match {
|
||||
case 0 =>
|
||||
EUpdate(UpdatePure(TBuiltin(BTUnit), EPrimCon(PCUnit))) // do nothing if we have no commands
|
||||
case 1 =>
|
||||
bindings(0)._2
|
||||
case _ =>
|
||||
EUpdate(UpdateBlock(bindings.init.map {
|
||||
case (typ, e) => Binding(None, typ, e)
|
||||
}, bindings.last._2))
|
||||
}
|
||||
private[engine] def preprocessCreateAndExercise(
|
||||
templateId: ValueRef,
|
||||
createArgument: VersionedValue[AbsoluteContractId],
|
||||
choiceId: String,
|
||||
choiceArgument: VersionedValue[AbsoluteContractId],
|
||||
actors: Set[Party]): Result[(Type, SpeedyCommand)] = {
|
||||
Result.needDataType(
|
||||
compiledPackages,
|
||||
templateId,
|
||||
dataType => {
|
||||
// we rely on datatypes which are also templates to have _no_ parameters, according
|
||||
// to the DAML-LF spec.
|
||||
if (dataType.params.length > 0) {
|
||||
ResultError(Error(
|
||||
s"Unexpected type parameters ${dataType.params} for template $templateId. Template datatypes should never have parameters."))
|
||||
} else {
|
||||
val typ = TTyCon(templateId)
|
||||
translateValue(typ, createArgument).flatMap {
|
||||
createValue =>
|
||||
Result.needTemplate(
|
||||
compiledPackages,
|
||||
templateId,
|
||||
template => {
|
||||
template.choices.get(choiceId) 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 Some(choice) =>
|
||||
val choiceTyp = choice.argBinder._2
|
||||
val actingParties = ImmArray(actors.toSeq.map(actor => SValue.SParty(actor)))
|
||||
translateValue(choiceTyp, choiceArgument).map(
|
||||
choiceTyp -> SpeedyCommand
|
||||
.CreateAndExercise(templateId, createValue, choiceId, _, actingParties))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private[engine] def preprocessCommand(cmd: Command): Result[(Type, SpeedyCommand)] = cmd match {
|
||||
case CreateCommand(templateId, argument) =>
|
||||
preprocessCreate(templateId, argument)
|
||||
case ExerciseCommand(templateId, contractId, choiceId, submitter, argument) =>
|
||||
preprocessExercise(
|
||||
templateId,
|
||||
AbsoluteContractId(contractId),
|
||||
choiceId,
|
||||
Set(submitter),
|
||||
argument)
|
||||
}
|
||||
private[engine] def preprocessCommand(cmd: Command): Result[(Type, SpeedyCommand)] =
|
||||
cmd match {
|
||||
case CreateCommand(templateId, argument) =>
|
||||
preprocessCreate(templateId, argument)
|
||||
case ExerciseCommand(templateId, contractId, choiceId, submitter, argument) =>
|
||||
preprocessExercise(
|
||||
templateId,
|
||||
AbsoluteContractId(contractId),
|
||||
choiceId,
|
||||
Set(submitter),
|
||||
argument)
|
||||
case CreateAndExerciseCommand(
|
||||
templateId,
|
||||
createArgument,
|
||||
choiceId,
|
||||
choiceArgument,
|
||||
submitter) =>
|
||||
preprocessCreateAndExercise(
|
||||
templateId,
|
||||
createArgument,
|
||||
choiceId,
|
||||
choiceArgument,
|
||||
Set(submitter))
|
||||
}
|
||||
|
||||
private[engine] def preprocessCommands(
|
||||
cmds0: Commands): Result[ImmArray[(Type, SpeedyCommand)]] = {
|
||||
|
@ -85,7 +85,7 @@ final class Engine {
|
||||
* tx === tx' if tx and tx' are equivalent modulo a renaming of node and relative contract IDs
|
||||
*
|
||||
* In addition to the errors returned by `submit`, reinterpretation fails with a `ValidationError` whenever `nodes`
|
||||
* contain a relative contract ID, either as the target contract of an exercise or a fetch, or as an argument to a
|
||||
* contain a relative contract ID, either as the target contract of a fetch, or as an argument to a
|
||||
* create or an exercise choice.
|
||||
*/
|
||||
def reinterpret(
|
||||
@ -129,7 +129,7 @@ final class Engine {
|
||||
|
||||
/**
|
||||
* Post-commit validation
|
||||
* we damand that validatable transactions only contain AbsoluteContractIds in root nodes
|
||||
* we demand that validatable transactions only contain AbsoluteContractIds in root nodes
|
||||
*
|
||||
* @param tx a transaction to be validated
|
||||
* @param submitter party name if known who originally submitted the transaction
|
||||
@ -265,7 +265,7 @@ final class Engine {
|
||||
)
|
||||
|
||||
case NodeExercises(
|
||||
target,
|
||||
coid,
|
||||
template,
|
||||
choice,
|
||||
optLoc @ _,
|
||||
@ -277,11 +277,10 @@ final class Engine {
|
||||
controllers @ _,
|
||||
children @ _) =>
|
||||
val templateId = template
|
||||
asAbsoluteContractId(target).flatMap(
|
||||
acoid =>
|
||||
asValueWithAbsoluteContractIds(chosenVal).flatMap(absChosenVal =>
|
||||
commandPreprocessor
|
||||
.preprocessExercise(templateId, acoid, choice, actingParties, absChosenVal)))
|
||||
asValueWithAbsoluteContractIds(chosenVal).flatMap(
|
||||
absChosenVal =>
|
||||
commandPreprocessor
|
||||
.preprocessExercise(templateId, coid, choice, actingParties, absChosenVal))
|
||||
|
||||
case NodeFetch(coid, templateId, _, _, _, _) =>
|
||||
asAbsoluteContractId(coid)
|
||||
|
@ -214,6 +214,92 @@ class EngineTest extends WordSpec with Matchers {
|
||||
res shouldBe 'right
|
||||
}
|
||||
|
||||
"translate create-and-exercise commands argument including labels" in {
|
||||
val id = Identifier(basicTestsPkgId, "BasicTests:CallablePayout")
|
||||
val let = Time.Timestamp.now()
|
||||
val command =
|
||||
CreateAndExerciseCommand(
|
||||
id,
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(
|
||||
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
|
||||
ImmArray((Some("giver"), ValueParty(clara)), (Some("receiver"), ValueParty(clara))))),
|
||||
"Transfer",
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(None, ImmArray((Some("newReceiver"), ValueParty(clara))))),
|
||||
clara
|
||||
)
|
||||
|
||||
val res = commandTranslator
|
||||
.preprocessCommands(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContractForPayout, lookupPackage, lookupKey)
|
||||
res shouldBe 'right
|
||||
|
||||
}
|
||||
|
||||
"translate create-and-exercise commands argument without labels" in {
|
||||
val id = Identifier(basicTestsPkgId, "BasicTests:CallablePayout")
|
||||
val let = Time.Timestamp.now()
|
||||
val command =
|
||||
CreateAndExerciseCommand(
|
||||
id,
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(
|
||||
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
|
||||
ImmArray((None, ValueParty(clara)), (None, ValueParty(clara))))),
|
||||
"Transfer",
|
||||
assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueParty(clara))))),
|
||||
clara
|
||||
)
|
||||
|
||||
val res = commandTranslator
|
||||
.preprocessCommands(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
res shouldBe 'right
|
||||
}
|
||||
|
||||
"not translate create-and-exercise commands argument wrong label in create arguments" in {
|
||||
val id = Identifier(basicTestsPkgId, "BasicTests:CallablePayout")
|
||||
val let = Time.Timestamp.now()
|
||||
val command =
|
||||
CreateAndExerciseCommand(
|
||||
id,
|
||||
assertAsVersionedValue(ValueRecord(
|
||||
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
|
||||
ImmArray((None, ValueParty(clara)), (Some("this_is_not_the_one"), ValueParty(clara))))),
|
||||
"Transfer",
|
||||
assertAsVersionedValue(ValueRecord(None, ImmArray((None, ValueParty(clara))))),
|
||||
clara
|
||||
)
|
||||
|
||||
val res = commandTranslator
|
||||
.preprocessCommands(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
res shouldBe 'left
|
||||
}
|
||||
|
||||
"not translate create-and-exercise commands argument wrong label in choice arguments" in {
|
||||
val id = Identifier(basicTestsPkgId, "BasicTests:CallablePayout")
|
||||
val let = Time.Timestamp.now()
|
||||
val command =
|
||||
CreateAndExerciseCommand(
|
||||
id,
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(
|
||||
Some(Identifier(basicTestsPkgId, "BasicTests:CallablePayout")),
|
||||
ImmArray((None, ValueParty(clara)), (None, ValueParty(clara))))),
|
||||
"Transfer",
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(None, ImmArray((Some("this_is_not_the_one"), ValueParty(clara))))),
|
||||
clara
|
||||
)
|
||||
|
||||
val res = commandTranslator
|
||||
.preprocessCommands(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
res shouldBe 'left
|
||||
}
|
||||
|
||||
"translate Optional values" in {
|
||||
val (optionalPkgId, optionalPkg @ _, allOptionalPackages) =
|
||||
loadPackage("daml-lf/tests/Optional.dar")
|
||||
@ -316,14 +402,6 @@ class EngineTest extends WordSpec with Matchers {
|
||||
res.flatMap(r => engine.interpret(r, let).consume(lookupContract, lookupPackage, lookupKey))
|
||||
val Right(tx) = interpretResult
|
||||
|
||||
"be translated" in {
|
||||
val submitResult = engine
|
||||
.submit(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
interpretResult shouldBe 'right
|
||||
submitResult shouldBe interpretResult
|
||||
}
|
||||
|
||||
"reinterpret to the same result" in {
|
||||
val txRoots = tx.roots.map(id => tx.nodes.get(id).get).toSeq
|
||||
val reinterpretResult =
|
||||
@ -414,6 +492,55 @@ class EngineTest extends WordSpec with Matchers {
|
||||
}
|
||||
}
|
||||
|
||||
"create-and-exercise command" should {
|
||||
val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple")
|
||||
val hello = Identifier(basicTestsPkgId, "BasicTests:Hello")
|
||||
val let = Time.Timestamp.now()
|
||||
val command =
|
||||
CreateAndExerciseCommand(
|
||||
templateId,
|
||||
assertAsVersionedValue(
|
||||
ValueRecord(Some(templateId), ImmArray(Some("p") -> ValueParty(party)))),
|
||||
"Hello",
|
||||
assertAsVersionedValue(ValueRecord(Some(hello), ImmArray.empty)),
|
||||
party
|
||||
)
|
||||
|
||||
val res = commandTranslator
|
||||
.preprocessCommands(Commands(Seq(command), let, "test"))
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
res shouldBe 'right
|
||||
val interpretResult =
|
||||
res.flatMap(r => engine.interpret(r, let).consume(lookupContract, lookupPackage, lookupKey))
|
||||
val Right(tx) = interpretResult
|
||||
|
||||
"be translated" in {
|
||||
tx.roots should have length 2
|
||||
tx.nodes.keySet.toList should have length 2
|
||||
val ImmArray(create, exercise) = tx.roots.map(tx.nodes)
|
||||
create shouldBe a[NodeCreate[_, _]]
|
||||
exercise shouldBe a[NodeExercises[_, _, _]]
|
||||
}
|
||||
|
||||
"reinterpret to the same result" in {
|
||||
val txRoots = tx.roots.map(id => tx.nodes(id)).toSeq
|
||||
val reinterpretResult =
|
||||
engine.reinterpret(txRoots, let).consume(lookupContract, lookupPackage, lookupKey)
|
||||
(interpretResult |@| reinterpretResult)(_ isReplayedBy _) shouldBe Right(true)
|
||||
}
|
||||
|
||||
"be validated" in {
|
||||
val validated = engine
|
||||
.validate(tx, let)
|
||||
.consume(lookupContract, lookupPackage, lookupKey)
|
||||
validated match {
|
||||
case Left(e) =>
|
||||
fail(e.msg)
|
||||
case Right(()) => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"translate list value" should {
|
||||
"translate empty list" in {
|
||||
val list = ValueList(FrontStack.empty[Value[AbsoluteContractId]])
|
||||
|
@ -32,4 +32,12 @@ object Command {
|
||||
coid: SContractId
|
||||
) extends Command
|
||||
|
||||
final case class CreateAndExercise(
|
||||
templateId: Identifier,
|
||||
createArgument: SValue,
|
||||
choiceId: String,
|
||||
choiceArgument: SValue,
|
||||
submitter: ImmArray[SParty]
|
||||
) extends Command
|
||||
|
||||
}
|
||||
|
@ -1190,6 +1190,28 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
|
||||
SEApp(SEVal(makeChoiceRef(tmplId, choiceId), None), Array(actors, contractId, argument))
|
||||
}
|
||||
|
||||
private def compileCreateAndExercise(
|
||||
tmplId: Identifier,
|
||||
createArg: SValue,
|
||||
choiceId: ChoiceName,
|
||||
choiceArg: SValue,
|
||||
// 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
|
||||
actors: SExpr): SExpr = {
|
||||
|
||||
withEnv { _ =>
|
||||
SEAbs(1) {
|
||||
SELet(
|
||||
SEApp(compileCreate(tmplId, createArg), Array(SEVar(1))),
|
||||
SEApp(
|
||||
compileExercise(tmplId, SEVar(1), choiceId, actors, SEValue(choiceArg)),
|
||||
Array(SEVar(2)))
|
||||
) in SEVar(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def translateCommand(cmd: Command): SExpr = cmd match {
|
||||
case Command.Create(templateId, argument) =>
|
||||
compileCreate(templateId, argument)
|
||||
@ -1202,6 +1224,13 @@ final case class Compiler(packages: PackageId PartialFunction Package) {
|
||||
SEValue(argument))
|
||||
case Command.Fetch(templateId, coid) =>
|
||||
compileFetch(templateId, SEValue(coid))
|
||||
case Command.CreateAndExercise(templateId, createArg, choice, choiceArg, submitters) =>
|
||||
compileCreateAndExercise(
|
||||
templateId,
|
||||
createArg,
|
||||
choice,
|
||||
choiceArg,
|
||||
SEValue(SList(FrontStack(submitters))))
|
||||
}
|
||||
|
||||
private def translateCommands(bindings: ImmArray[Command]): SExpr = {
|
||||
|
@ -10,6 +10,10 @@ HEAD — ongoing
|
||||
--------------
|
||||
|
||||
- Addition of ``DA.Math`` library containing exponentiation, logarithms and trig functions
|
||||
- Add CreateAndExerciseCommand to Ledger API and DAMLe for creating a contract
|
||||
and exercising a choice on it within the same transaction. This can be used to
|
||||
implement "callable updates" (aka functions of type ``Update a`` that can be
|
||||
called from the Ledger API via a contract).
|
||||
|
||||
0.12.7 — 2019-04-17
|
||||
-------------------
|
||||
|
@ -0,0 +1,86 @@
|
||||
// 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 CreateAndExerciseCommand extends Command {
|
||||
private final Identifier templateId;
|
||||
|
||||
private final Record createArguments;
|
||||
|
||||
private final String choice;
|
||||
|
||||
private final Value choiceArgument;
|
||||
|
||||
public CreateAndExerciseCommand(@NonNull Identifier templateId, @NonNull Record createArguments, @NonNull String choice, @NonNull Value choiceArgument) {
|
||||
this.templateId = templateId;
|
||||
this.createArguments = createArguments;
|
||||
this.choice = choice;
|
||||
this.choiceArgument = choiceArgument;
|
||||
}
|
||||
|
||||
public static CreateAndExerciseCommand fromProto(CommandsOuterClass.CreateAndExerciseCommand command) {
|
||||
Identifier templateId = Identifier.fromProto(command.getTemplateId());
|
||||
Record createArguments = Record.fromProto(command.getCreateArguments());
|
||||
String choice = command.getChoice();
|
||||
Value choiceArgument = Value.fromProto(command.getChoiceArgument());
|
||||
return new CreateAndExerciseCommand(templateId, createArguments, choice, choiceArgument);
|
||||
}
|
||||
|
||||
public CommandsOuterClass.CreateAndExerciseCommand toProto() {
|
||||
return CommandsOuterClass.CreateAndExerciseCommand.newBuilder()
|
||||
.setTemplateId(this.templateId.toProto())
|
||||
.setCreateArguments(this.createArguments.toProtoRecord())
|
||||
.setChoice(this.choice)
|
||||
.setChoiceArgument(this.choiceArgument.toProto())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
Identifier getTemplateId() {
|
||||
return templateId;
|
||||
}
|
||||
|
||||
public Record getCreateArguments() {
|
||||
return createArguments;
|
||||
}
|
||||
|
||||
public String getChoice() {
|
||||
return choice;
|
||||
}
|
||||
|
||||
public Value getChoiceArgument() {
|
||||
return choiceArgument;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CreateAndExerciseCommand{" +
|
||||
"templateId=" + templateId +
|
||||
", createArguments=" + createArguments +
|
||||
", choice='" + choice + '\'' +
|
||||
", choiceArgument=" + choiceArgument +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CreateAndExerciseCommand that = (CreateAndExerciseCommand) o;
|
||||
return templateId.equals(that.templateId) &&
|
||||
createArguments.equals(that.createArguments) &&
|
||||
choice.equals(that.choice) &&
|
||||
choiceArgument.equals(that.choiceArgument);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(templateId, createArguments, choice, choiceArgument);
|
||||
}
|
||||
}
|
@ -58,6 +58,7 @@ message Command {
|
||||
oneof command {
|
||||
CreateCommand create = 1;
|
||||
ExerciseCommand exercise = 2;
|
||||
CreateAndExerciseCommand createAndExercise = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,3 +93,22 @@ message ExerciseCommand {
|
||||
// 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
|
||||
// Required
|
||||
Identifier template_id = 1;
|
||||
|
||||
// The arguments required for creating a contract from this template.
|
||||
// Required
|
||||
Record create_arguments = 2;
|
||||
|
||||
// The name of the choice the client wants to exercise.
|
||||
// Required
|
||||
string choice = 3;
|
||||
|
||||
// The argument for this choice.
|
||||
// Required
|
||||
Value choice_argument = 4;
|
||||
}
|
||||
|
@ -9,7 +9,12 @@ import com.digitalasset.ledger.api.domain
|
||||
import com.digitalasset.ledger.api.domain.Value.VariantValue
|
||||
import com.digitalasset.ledger.api.messages.command.submission
|
||||
import com.digitalasset.ledger.api.v1.command_submission_service.SubmitRequest
|
||||
import com.digitalasset.ledger.api.v1.commands.Command.Command.{Create, Empty, Exercise}
|
||||
import com.digitalasset.ledger.api.v1.commands.Command.Command.{
|
||||
Create,
|
||||
Empty,
|
||||
Exercise,
|
||||
CreateAndExercise
|
||||
}
|
||||
import com.digitalasset.ledger.api.v1.commands.{Command, Commands}
|
||||
import com.digitalasset.ledger.api.v1.value.Value.Sum
|
||||
import com.digitalasset.ledger.api.v1.value.{
|
||||
@ -96,6 +101,23 @@ class CommandSubmissionRequestValidator(ledgerId: String, identifierResolver: Id
|
||||
domain.ContractId(contractId),
|
||||
domain.Choice(choice),
|
||||
validatedValue)
|
||||
case ce: CreateAndExercise =>
|
||||
for {
|
||||
templateId <- requirePresence(ce.value.templateId, "template_id")
|
||||
validatedTemplateId <- identifierResolver.resolveIdentifier(templateId)
|
||||
createArguments <- requirePresence(ce.value.createArguments, "create_arguments")
|
||||
recordId <- validateOptionalIdentifier(createArguments.recordId)
|
||||
validatedRecordField <- validateRecordFields(createArguments.fields)
|
||||
choice <- requireNonEmptyString(ce.value.choice, "choice")
|
||||
value <- requirePresence(ce.value.choiceArgument, "value")
|
||||
validatedChoiceArgument <- validateValue(value)
|
||||
} yield
|
||||
domain.CreateAndExerciseCommand(
|
||||
validatedTemplateId,
|
||||
domain.Value.RecordValue(recordId, validatedRecordField),
|
||||
domain.Choice(choice),
|
||||
validatedChoiceArgument
|
||||
)
|
||||
case Empty => Left(missingField("command"))
|
||||
}
|
||||
|
||||
|
@ -12,12 +12,14 @@ import com.digitalasset.daml.lf.engine.{
|
||||
Commands => LfCommands,
|
||||
CreateCommand => LfCreateCommand,
|
||||
Error => LfError,
|
||||
ExerciseCommand => LfExerciseCommand
|
||||
ExerciseCommand => LfExerciseCommand,
|
||||
CreateAndExerciseCommand => LfCreateAndExerciseCommand
|
||||
}
|
||||
import com.digitalasset.daml.lf.lfpackage.Ast.Package
|
||||
import com.digitalasset.daml.lf.value.Value.AbsoluteContractId
|
||||
import com.digitalasset.daml.lf.value.{Value => Lf}
|
||||
import com.digitalasset.ledger.api.domain.{
|
||||
CreateAndExerciseCommand,
|
||||
CreateCommand,
|
||||
ExerciseCommand,
|
||||
Command => ApiCommand,
|
||||
@ -284,6 +286,7 @@ object ApiToLfEngine {
|
||||
val oldStyleTplId = apiCommand match {
|
||||
case e: ExerciseCommand => e.templateId
|
||||
case c: CreateCommand => c.templateId
|
||||
case ce: CreateAndExerciseCommand => ce.templateId
|
||||
}
|
||||
toLfIdentifier(packages0, oldStyleTplId) match {
|
||||
case Error(err) => Error(err)
|
||||
@ -304,6 +307,17 @@ object ApiToLfEngine {
|
||||
tplId,
|
||||
asVersionedValueOrThrow(arg),
|
||||
)
|
||||
def withCreateAndExerciseArgument(
|
||||
ce: CreateAndExerciseCommand,
|
||||
createArg: LfValue,
|
||||
choiceArg: LfValue): LfCreateAndExerciseCommand =
|
||||
LfCreateAndExerciseCommand(
|
||||
tplId,
|
||||
asVersionedValueOrThrow(createArg),
|
||||
ce.choice.unwrap,
|
||||
asVersionedValueOrThrow(choiceArg),
|
||||
SimpleString.assertFromString(cmd.submitter.underlyingString)
|
||||
)
|
||||
apiCommand match {
|
||||
case e: ExerciseCommand =>
|
||||
apiValueToLfValueWithPackages(packages1, e.choiceArgument) match {
|
||||
@ -339,6 +353,63 @@ object ApiToLfEngine {
|
||||
remainingCommands,
|
||||
processed :+ withCreateArgument(c, createArgument))
|
||||
}
|
||||
case ce: CreateAndExerciseCommand =>
|
||||
apiValueToLfValueWithPackages(packages1, ce.createArgument) match {
|
||||
case Error(err) => Error(err)
|
||||
case np: NeedPackage[(Packages, LfValue)] =>
|
||||
np.flatMap {
|
||||
case (packages2, createArgument) =>
|
||||
apiValueToLfValueWithPackages(packages2, ce.choiceArgument) match {
|
||||
case Error(err) => Error(err)
|
||||
case np: NeedPackage[(Packages, LfValue)] =>
|
||||
np.flatMap {
|
||||
case (packages3, choiceArgument) =>
|
||||
goResume(
|
||||
packages3,
|
||||
remainingCommands,
|
||||
processed :+ withCreateAndExerciseArgument(
|
||||
ce,
|
||||
createArgument,
|
||||
choiceArgument)
|
||||
)
|
||||
}
|
||||
case Done((packages3, choiceArgument)) =>
|
||||
goResume(
|
||||
packages3,
|
||||
remainingCommands,
|
||||
processed :+ withCreateAndExerciseArgument(
|
||||
ce,
|
||||
createArgument,
|
||||
choiceArgument)
|
||||
)
|
||||
}
|
||||
}
|
||||
case Done((packages2, createArgument)) =>
|
||||
apiValueToLfValueWithPackages(packages2, ce.choiceArgument) match {
|
||||
case Error(err) => Error(err)
|
||||
case np: NeedPackage[(Packages, LfValue)] =>
|
||||
np.flatMap {
|
||||
case (packages3, choiceArgument) =>
|
||||
goResume(
|
||||
packages3,
|
||||
remainingCommands,
|
||||
processed :+ withCreateAndExerciseArgument(
|
||||
ce,
|
||||
createArgument,
|
||||
choiceArgument)
|
||||
)
|
||||
}
|
||||
case Done((packages3, choiceArgument)) =>
|
||||
go(
|
||||
packages3,
|
||||
remainingCommands,
|
||||
processed :+ withCreateAndExerciseArgument(
|
||||
ce,
|
||||
createArgument,
|
||||
choiceArgument)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,16 @@ import com.digitalasset.daml.lf.engine.{
|
||||
Commands,
|
||||
CreateCommand,
|
||||
DeprecatedIdentifier,
|
||||
ExerciseCommand
|
||||
ExerciseCommand,
|
||||
CreateAndExerciseCommand
|
||||
}
|
||||
import com.digitalasset.daml.lf.value.{Value => Lf}
|
||||
import com.digitalasset.ledger.api.v1.commands.{
|
||||
Command => ApiCommand,
|
||||
Commands => ApiCommands,
|
||||
CreateCommand => ApiCreateCommand,
|
||||
ExerciseCommand => ApiExerciseCommand
|
||||
ExerciseCommand => ApiExerciseCommand,
|
||||
CreateAndExerciseCommand => ApiCreateAndExerciseCommand
|
||||
}
|
||||
import com.digitalasset.ledger.api.v1.value.{
|
||||
Optional,
|
||||
@ -191,6 +193,14 @@ object LfEngineToApi {
|
||||
contractId,
|
||||
choiceId,
|
||||
LfEngineToApi.lfValueToApiValue(verbose = true, argument.value).toOption)))
|
||||
case CreateAndExerciseCommand(templateId, createArgument, choiceId, choiceArgument, _) =>
|
||||
ApiCommand(
|
||||
ApiCommand.Command.CreateAndExercise(ApiCreateAndExerciseCommand(
|
||||
Some(toApiIdentifier(templateId)),
|
||||
LfEngineToApi.lfVersionedValueToApiRecord(verbose = true, createArgument).toOption,
|
||||
choiceId,
|
||||
LfEngineToApi.lfVersionedValueToApiValue(verbose = true, choiceArgument).toOption
|
||||
)))
|
||||
}
|
||||
|
||||
ApiCommands(
|
||||
|
@ -35,6 +35,14 @@ trait CommandPayloadValidations extends CommandValidations with ErrorFactories {
|
||||
_ <- requirePresence(ex.choiceArgument, "choice_argument")
|
||||
_ <- requirePresence(ex.templateId, "template_id")
|
||||
} yield ()
|
||||
|
||||
case Command.Command.CreateAndExercise(ce) =>
|
||||
for {
|
||||
_ <- requirePresence(ce.templateId, "template_id")
|
||||
_ <- requirePresence(ce.createArguments, "create_arguments")
|
||||
_ <- requireNonEmptyString(ce.choice, "choice")
|
||||
_ <- requirePresence(ce.choiceArgument, "choice_argument")
|
||||
} yield ()
|
||||
case _ => Left(missingField("command"))
|
||||
}
|
||||
}
|
||||
|
@ -302,4 +302,11 @@ object domain {
|
||||
choiceArgument: Value)
|
||||
extends Command
|
||||
|
||||
final case class CreateAndExerciseCommand(
|
||||
templateId: Ref.Identifier,
|
||||
createArgument: RecordValue,
|
||||
choice: Choice,
|
||||
choiceArgument: Value)
|
||||
extends Command
|
||||
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class CommandSubmissionServiceIT
|
||||
|
||||
private def client(channel: Channel) = CommandSubmissionServiceGrpc.stub(channel)
|
||||
|
||||
"Command Service" when {
|
||||
"Command Submission Service" when {
|
||||
|
||||
"commands arrive with extreme TTLs" should {
|
||||
|
||||
@ -45,7 +45,6 @@ class CommandSubmissionServiceIT
|
||||
succeed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package com.digitalasset.platform.apitesting
|
||||
import java.util.UUID
|
||||
|
||||
import akka.stream.scaladsl.Sink
|
||||
import com.digitalasset.ledger.api.testing.utils.MockMessages.{party, submitRequest, commandId}
|
||||
import com.digitalasset.ledger.api.testing.utils.{
|
||||
AkkaBeforeAndAfterAll,
|
||||
SuiteResourceManagementAroundEach,
|
||||
@ -13,13 +14,20 @@ import com.digitalasset.ledger.api.testing.utils.{
|
||||
}
|
||||
import com.digitalasset.ledger.api.v1.command_submission_service.SubmitRequest
|
||||
import com.digitalasset.ledger.api.v1.commands.Command.Command.Create
|
||||
import com.digitalasset.ledger.api.v1.commands.{Command, CreateCommand, ExerciseCommand}
|
||||
import com.digitalasset.ledger.api.v1.commands.{
|
||||
Command,
|
||||
CreateAndExerciseCommand,
|
||||
CreateCommand,
|
||||
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}
|
||||
import com.digitalasset.ledger.api.v1.ledger_offset.LedgerOffset
|
||||
import com.digitalasset.ledger.api.v1.transaction.TreeEvent.Kind
|
||||
import com.digitalasset.ledger.api.v1.transaction.{Transaction, TransactionTree}
|
||||
import com.digitalasset.ledger.api.v1.transaction_filter.{Filters, TransactionFilter}
|
||||
import com.digitalasset.ledger.api.v1.transaction_service.GetLedgerEndResponse
|
||||
import com.digitalasset.ledger.api.v1.value.Value.Sum
|
||||
import com.digitalasset.ledger.api.v1.value.Value.Sum.{Bool, ContractId, Text, Timestamp}
|
||||
import com.digitalasset.ledger.api.v1.value.{
|
||||
@ -736,6 +744,85 @@ abstract class CommandTransactionChecks
|
||||
}
|
||||
}
|
||||
}
|
||||
"client sends a CreateAndExerciseCommand" should {
|
||||
val validCreateAndExercise = CreateAndExerciseCommand(
|
||||
Some(templateIds.dummy),
|
||||
Some(Record(fields = List(RecordField(value = Some(Value(Value.Sum.Party(party))))))),
|
||||
"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(cmd: CreateAndExerciseCommand) = submitRequest
|
||||
.update(_.commands.commands := Seq[Command](Command(Command.Command.CreateAndExercise(cmd))))
|
||||
.update(_.commands.ledgerId := config.getLedgerId)
|
||||
|
||||
"process valid commands successfully" in allFixtures{ c =>
|
||||
val request = newRequest(validCreateAndExercise)
|
||||
|
||||
for {
|
||||
GetLedgerEndResponse(Some(currentEnd)) <- c.transactionClient.getLedgerEnd
|
||||
|
||||
_ <- submitSuccessfully(c, request)
|
||||
|
||||
txTree <- c.transactionClient
|
||||
.getTransactionTrees(currentEnd, None, partyFilter)
|
||||
.runWith(Sink.head)
|
||||
|
||||
flatTransaction <- c.transactionClient
|
||||
.getTransactions(currentEnd, None, partyFilter)
|
||||
.runWith(Sink.head)
|
||||
|
||||
} yield {
|
||||
flatTransaction.commandId shouldBe commandId
|
||||
// gerolf-da 2019-04-17: #575 takes care of whether we should even emit the flat transaction or not
|
||||
flatTransaction.events shouldBe empty
|
||||
|
||||
txTree.rootEventIds should have length 2
|
||||
txTree.commandId shouldBe commandId
|
||||
|
||||
val Seq(Kind.Created(createdEvent), Kind.Exercised(exercisedEvent)) =
|
||||
txTree.rootEventIds.map(txTree.eventsById(_).kind)
|
||||
|
||||
createdEvent.templateId shouldBe Some(templateIds.dummy)
|
||||
|
||||
exercisedEvent.choice shouldBe "DummyChoice1"
|
||||
exercisedEvent.contractId shouldBe createdEvent.contractId
|
||||
exercisedEvent.consuming shouldBe true
|
||||
exercisedEvent.contractCreatingEventId shouldBe createdEvent.eventId
|
||||
}
|
||||
}
|
||||
|
||||
"fail for invalid create arguments" in allFixtures{ implicit c =>
|
||||
val createAndExercise = validCreateAndExercise.copy(createArguments = Some(Record()))
|
||||
val request = newRequest(createAndExercise)
|
||||
|
||||
val response = submitCommand(c, request)
|
||||
response.map(_.getStatus should have('code (Code.INVALID_ARGUMENT.value)))
|
||||
}
|
||||
|
||||
"fail for invalid choice arguments" in allFixtures{ implicit c =>
|
||||
val createAndExercise =
|
||||
validCreateAndExercise.copy(choiceArgument = Some(Value(Value.Sum.Bool(false))))
|
||||
val request = newRequest(createAndExercise)
|
||||
.update(_.commands.commands := Seq[Command](Command(Command.Command.CreateAndExercise(createAndExercise))))
|
||||
|
||||
val response = submitCommand(c, request)
|
||||
response.map(_.getStatus should have('code (Code.INVALID_ARGUMENT.value)))
|
||||
}
|
||||
|
||||
"fail for an invalid choice" in allFixtures{ implicit c =>
|
||||
val createAndExercise = validCreateAndExercise.copy(choice = "DoesNotExist")
|
||||
|
||||
val request = newRequest(createAndExercise)
|
||||
.update(_.commands.commands := Seq[Command](Command(Command.Command.CreateAndExercise(createAndExercise))))
|
||||
|
||||
val response = submitCommand(c, request)
|
||||
response.map(_.getStatus should have('code (Code.INVALID_ARGUMENT.value)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def cid(commandId: String) = s"$commandId"
|
||||
|
Loading…
Reference in New Issue
Block a user