Skip to content

Commit

Permalink
Add ExerciseByKey command to Ledger API
Browse files Browse the repository at this point in the history
Fixes #1366

Also adds support for the new command to the Java bindings and codegen
  • Loading branch information
stefanobaghino-da committed Jun 13, 2019
1 parent 0394f47 commit 0644506
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,35 @@ 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],
Expand Down Expand Up @@ -419,6 +448,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._

Expand All @@ -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._

Expand Down Expand Up @@ -95,8 +95,15 @@ class EngineTest extends WordSpec with Matchers with BazelRunfiles {
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()
Expand Down Expand Up @@ -215,6 +222,91 @@ 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()
Expand Down Expand Up @@ -516,6 +608,75 @@ 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(lookupContractForPayout, lookupPackage, lookupKey)
res shouldBe 'right

"fail at submission" in {
val submitResult = engine
.submit(Commands(ImmArray(command), let, "test"))
.consume(lookupContract, 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(lookupContractForPayout, lookupPackage, lookupKey)
res shouldBe 'right
val result =
res.flatMap(r => engine.interpret(r, let).consume(lookupContract, lookupPackage, lookupKey))
val tx = result.right.value

"be translated" in {
val submitResult = engine
.submit(Commands(ImmArray(command), let, "test"))
.consume(lookupContract, 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(lookupContract, lookupPackage, lookupKey)
(result |@| 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(()) => ()
}
}
}

"create-and-exercise command" should {
val templateId = Identifier(basicTestsPkgId, "BasicTests:Simple")
val hello = Identifier(basicTestsPkgId, "BasicTests:Hello")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,27 @@ 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,
optActors: Option[SExpr],
argument: SExpr): SExpr = {
// Translates '<actor> does exerciseByKey SomeTemplate <key> SomeChoice with <params>' into:
// let coid = $fetchKey <key>
// in { SomeTemplate$SomeChoice <actor> coid <params> }
withEnv { _ =>
SEAbs(1) {
SELet(
/* coid = */ SBUFetchKey(tmplId)(
key,
SEVar(1) /* token */
)
) in compileExercise(tmplId, SEVar(2) /* coid */, choiceId, optActors, argument)
}
}
}

private def compileCreateAndExercise(
tmplId: Identifier,
createArg: SValue,
Expand Down Expand Up @@ -1187,6 +1208,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) =>
Expand Down
15 changes: 15 additions & 0 deletions daml-lf/tests/BasicTests.daml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
8 changes: 8 additions & 0 deletions docs/source/app-dev/code-snippets/Templates.daml
Original file line number Diff line number Diff line change
Expand Up @@ -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 owner

controller owner can
MyChoice
: ()
Expand Down
4 changes: 4 additions & 0 deletions docs/source/app-dev/grpc/daml-to-ledger-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 0644506

Please sign in to comment.