mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
f2e6705ed7
commit
656e456b78
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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) =>
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
: ()
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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")
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user