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:
Gerolf Seitz 2019-04-18 18:05:16 +02:00 committed by mergify[bot]
parent 0081fd6ea6
commit 7f8dbfeca0
16 changed files with 586 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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