Skip to content

Commit

Permalink
Add CreateAndExercise command to DAMLe
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gerolf-da committed Apr 18, 2019
1 parent f39b7a5 commit 4c77ae0
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 42 deletions.
Expand Up @@ -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
Expand All @@ -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
Expand Down
Expand Up @@ -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
Expand All @@ -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)]] = {
Expand Down
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -265,7 +265,7 @@ final class Engine {
)

case NodeExercises(
target,
coid,
template,
choice,
optLoc @ _,
Expand All @@ -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)
Expand Down
Expand Up @@ -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")
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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]])
Expand Down
Expand Up @@ -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

}

0 comments on commit 4c77ae0

Please sign in to comment.