Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exercise by key #4049

Merged
merged 8 commits into from
Jan 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions docs/source/json-api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,10 +402,10 @@ POST ``/command/exercise``

Exercise a choice on a contract.

``"contractId": "#124:0"`` is the value from the create output.
Exercise by ContractId Request
------------------------------

Request
-------
``"contractId": "#124:0"`` is the value from the create output.

application/json body:

Expand All @@ -420,6 +420,21 @@ application/json body:
}
}

Exercise by Contract Key Request
--------------------------------

application/json body:

.. code-block:: json

{
"templateId": "Account:Account",
"key": ["Alice", "abc123"],
"choice": "Archive",
"argument": {}
}


Response
--------

Expand Down
3 changes: 3 additions & 0 deletions language-support/java/bindings/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ da_scala_library(
"src/test/**/*Test.scala",
],
),
visibility = [
"//ledger-service/http-json:__subpackages__",
],
deps = [
":bindings-java",
"@maven//:com_google_protobuf_protobuf_java",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,20 @@ class Ledger {
* Exercise a choice on a contract identified by its contract key.
*/
async exerciseByKey<T extends object, C, R, K>(choice: Choice<T, C, R, K>, key: K extends undefined ? never : K, argument: C): Promise<[R, Event<object>[]]> {
const contract = await this.lookupByKey(choice.template(), key);
if (contract === null) {
throw Error(`exerciseByKey: no contract with key ${JSON.stringify(key)} for template ${choice.template().templateId}`);
}
return this.exercise(choice, contract.contractId, argument);
const payload = {
templateId: choice.template().templateId,
key,
choice: choice.choiceName,
argument,
};
const json = await this.submit('command/exercise', payload);
// Decode the server response into a tuple.
const responseDecoder: jtv.Decoder<{exerciseResult: R; contracts: Event<object>[]}> = jtv.object({
exerciseResult: choice.resultDecoder(),
contracts: jtv.array(decodeEventUnknown),
});
const {exerciseResult, contracts} = jtv.Result.withException(responseDecoder.run(json));
return [exerciseResult, contracts];
}

/**
Expand Down
1 change: 1 addition & 0 deletions ledger-service/http-json/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ da_scala_test(
"//daml-lf/interface",
"//daml-lf/transaction",
"//daml-lf/transaction-scalacheck",
"//language-support/java/bindings:bindings-java-tests-lib",
"//language-support/scala/bindings-akka",
"//ledger-api/rs-grpc-bridge",
"//ledger-service/db-backend",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,16 @@ class CommandService(
private def liftET[A](fa: Future[A]): EitherT[Future, Error, A] = EitherT.rightT(fa)

@SuppressWarnings(Array("org.wartremover.warts.Any"))
def exercise(jwt: Jwt, jwtPayload: JwtPayload, input: ExerciseCommand[lav1.value.Value])
def exercise(
jwt: Jwt,
jwtPayload: JwtPayload,
input: ExerciseCommand[lav1.value.Value, (domain.TemplateId.RequiredPkg, domain.ContractId)])
: Future[Error \/ ExerciseResponse[lav1.value.Value]] = {

val command = exerciseCommand(input)
val request = submitAndWaitRequest(jwtPayload, input.meta, command)

val et: EitherT[Future, Error, ExerciseResponse[lav1.value.Value]] = for {
command <- EitherT.either(exerciseCommand(input))
request = submitAndWaitRequest(jwtPayload, input.meta, command)
response <- liftET(logResult('exercise, submitAndWaitForTransactionTree(jwt, request)))
exerciseResult <- EitherT.either(exerciseResult(response))
contracts <- EitherT.either(contracts(response))
Expand All @@ -76,11 +80,13 @@ class CommandService(
def exerciseWithResult(
jwt: Jwt,
jwtPayload: JwtPayload,
input: ExerciseCommand[lav1.value.Value]): Future[Error \/ lav1.value.Value] = {
input: ExerciseCommand[lav1.value.Value, (domain.TemplateId.RequiredPkg, domain.ContractId)])
: Future[Error \/ lav1.value.Value] = {

val command = exerciseCommand(input)
val request = submitAndWaitRequest(jwtPayload, input.meta, command)

val et: EitherT[Future, Error, lav1.value.Value] = for {
command <- EitherT.either(exerciseCommand(input))
request = submitAndWaitRequest(jwtPayload, input.meta, command)
response <- liftET(
logResult('exerciseWithResult, submitAndWaitForTransactionTree(jwt, request)))
result <- EitherT.either(exerciseResult(response))
Expand All @@ -106,12 +112,14 @@ class CommandService(
}

private def exerciseCommand(
input: ExerciseCommand[lav1.value.Value]): Error \/ lav1.commands.Command.Command.Exercise =
resolveTemplateId(input.templateId)
.toRightDisjunction(
Error('exerciseCommand, ErrorMessages.cannotResolveTemplateId(input.templateId)))
.map(tpId =>
Commands.exercise(refApiIdentifier(tpId), input.contractId, input.choice, input.argument))
input: ExerciseCommand[lav1.value.Value, (domain.TemplateId.RequiredPkg, domain.ContractId)])
: lav1.commands.Command.Command.Exercise = {
Commands.exercise(
templateId = refApiIdentifier(input.reference._1),
contractId = input.reference._2,
choice = input.choice,
argument = input.argument)
}

private def submitAndWaitRequest(
jwtPayload: JwtPayload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,20 @@ class ContractsService(
)
}

def lookup(jwt: Jwt, jwtPayload: JwtPayload, contractLocator: domain.ContractLocator[ApiValue])
def resolve(jwt: Jwt, jwtPayload: JwtPayload, contractLocator: domain.ContractLocator[LfValue])
: Future[Option[(domain.TemplateId.RequiredPkg, domain.ContractId)]] =
contractLocator match {
case domain.EnrichedContractKey(templateId, key) =>
findByContractKey(jwt, jwtPayload.party, templateId, key).map(_.map(a =>
(a.templateId, a.contractId)))
case domain.EnrichedContractId(Some(templateId), contractId) =>
Future.successful(resolveTemplateId(templateId).map(a => (a, contractId)))
case domain.EnrichedContractId(None, contractId) =>
findByContractId(jwt, jwtPayload.party, None, contractId).map(_.map(a =>
(a.templateId, a.contractId)))
}

def lookup(jwt: Jwt, jwtPayload: JwtPayload, contractLocator: domain.ContractLocator[LfValue])
: Future[Option[domain.ActiveContract[LfValue]]] =
contractLocator match {
case domain.EnrichedContractKey(templateId, contractKey) =>
Expand All @@ -66,14 +79,12 @@ class ContractsService(
jwt: Jwt,
party: lar.Party,
templateId: TemplateId.OptionalPkg,
contractKey: api.value.Value): Future[Option[domain.ActiveContract[LfValue]]] =
contractKey: LfValue): Future[Option[domain.ActiveContract[LfValue]]] =
for {

lfKey <- toFuture(apiValueToLfValue(contractKey)): Future[LfValue]

resolvedTemplateId <- toFuture(resolveTemplateId(templateId)): Future[TemplateId.RequiredPkg]

predicate = isContractKey(lfKey) _
predicate = isContractKey(contractKey) _

errorOrAc <- searchInMemoryOneTpId(jwt, party, resolvedTemplateId, Map.empty)
.collect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import com.digitalasset.http.ContractsService.SearchResult
import com.digitalasset.http.EndpointsCompanion._
import com.digitalasset.http.Statement.discard
import com.digitalasset.http.domain.JwtPayload
import com.digitalasset.http.json.{DomainJsonDecoder, DomainJsonEncoder, ResponseFormats, SprayJson}
import com.digitalasset.http.util.FutureUtil.{either, eitherT}
import com.digitalasset.http.json._
import com.digitalasset.http.util.FutureUtil.{either, eitherT, rightT}
import com.digitalasset.http.util.{ApiValueToLfValueConverter, FutureUtil}
import com.digitalasset.jwt.domain.Jwt
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
Expand Down Expand Up @@ -84,12 +84,20 @@ class Endpoints(

cmd <- either(
decoder
.decodeV[domain.ExerciseCommand](reqBody)
.decodeExerciseCommand(reqBody)
.leftMap(e => InvalidUserInput(e.shows))
): ET[domain.ExerciseCommand[ApiValue]]
): ET[domain.ExerciseCommand[LfValue, domain.ContractLocator[LfValue]]]

resolvedRef <- eitherT(
resolveReference(jwt, jwtPayload, cmd.reference)
): ET[(domain.TemplateId.RequiredPkg, domain.ContractId)]

apiArg <- either(lfValueToApiValue(cmd.argument)): ET[ApiValue]

resolvedCmd = cmd.copy(argument = apiArg, reference = resolvedRef)

apiResp <- eitherT(
handleFutureFailure(commandService.exercise(jwt, jwtPayload, cmd))
handleFutureFailure(commandService.exercise(jwt, jwtPayload, resolvedCmd))
): ET[domain.ExerciseResponse[ApiValue]]

lfResp <- either(apiResp.traverse(apiValueToLfValue)): ET[domain.ExerciseResponse[LfValue]]
Expand Down Expand Up @@ -138,12 +146,11 @@ class Endpoints(

_ = logger.debug(s"/contracts/lookup reqBody: $reqBody")

// TODO(Leo): decode to domain.ContractLocator[LfValue], findByContractKey converts it to LfValue
cl <- either(
decoder
.decodeContractLocator(reqBody)
.leftMap(e => InvalidUserInput(e.shows))
): ET[domain.ContractLocator[ApiValue]]
): ET[domain.ContractLocator[LfValue]]

_ = logger.debug(s"/contracts/lookup cl: $cl")

Expand Down Expand Up @@ -205,7 +212,7 @@ class Endpoints(
case req @ HttpRequest(GET, Uri.Path("/parties"), _, _, _) =>
val et: ET[JsValue] = for {
_ <- FutureUtil.eitherT(input(req)): ET[(Jwt, JwtPayload, String)]
ps <- FutureUtil.rightT(partiesService.allParties()): ET[List[domain.PartyDetails]]
ps <- rightT(partiesService.allParties()): ET[List[domain.PartyDetails]]
jsVal <- either(SprayJson.encode(ps)).leftMap(e => ServerError(e.shows)): ET[JsValue]
} yield jsVal

Expand All @@ -219,6 +226,9 @@ class Endpoints(
\/.fromTryCatchNonFatal(LfValueCodec.apiValueToJsValue(a)).leftMap(e =>
ServerError(e.description))

private def lfValueToApiValue(a: LfValue): Error \/ ApiValue =
JsValueToApiValueConverter.lfValueToApiValue(a).leftMap(e => ServerError(e.shows))

private def collectActiveContracts(
predicates: Map[domain.TemplateId.RequiredPkg, LfValue => Boolean]): PartialFunction[
Error \/ domain.ActiveContract[LfValue],
Expand Down Expand Up @@ -300,6 +310,14 @@ class Endpoints(
}
.toRightDisjunction(Unauthorized("missing Authorization header with OAuth 2.0 Bearer Token"))

private def resolveReference(
jwt: Jwt,
jwtPayload: JwtPayload,
reference: domain.ContractLocator[LfValue])
: Future[Error \/ (domain.TemplateId.RequiredPkg, domain.ContractId)] =
contractsService
.resolve(jwt, jwtPayload, reference)
.map(_.toRightDisjunction(InvalidUserInput(ErrorMessages.cannotResolveTemplateId(reference))))
}

object Endpoints {
Expand All @@ -309,7 +327,4 @@ object Endpoints {
private type ApiValue = lav1.value.Value

private type LfValue = lf.value.Value[lf.value.Value.AbsoluteContractId]

private type ActiveContractStream[A] = Source[A, NotUsed]

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
package com.digitalasset.http

object ErrorMessages {
def cannotResolveTemplateId[A](t: domain.TemplateId[A]): String =
def cannotResolveTemplateId(t: domain.TemplateId[_]): String =
s"Cannot resolve template ID, given: ${t.toString}"

def cannotResolveTemplateId(a: domain.ContractLocator[_]): String =
s"Cannot resolve templateId, given: $a"
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ object HttpService extends StrictLogging {
packageService.resolveKeyType,
jsValueToApiValueConverter.jsObjectToApiRecord,
jsValueToApiValueConverter.jsValueToApiValue,
jsValueToApiValueConverter.jsValueToLfValue,
)

(encoder, decoder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package com.digitalasset.http
import java.time.Instant

import akka.http.scaladsl.model.{StatusCode, StatusCodes}
import com.digitalasset.daml.lf
import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.daml.lf.iface
import com.digitalasset.http.util.ClientUtil.boxedRecord
Expand Down Expand Up @@ -35,6 +36,8 @@ object domain {
}
}

type LfValue = lf.value.Value[lf.value.Value.AbsoluteContractId]

case class JwtPayload(ledgerId: lar.LedgerId, applicationId: lar.ApplicationId, party: Party)

case class TemplateId[+PkgId](packageId: PkgId, moduleName: String, entityName: String)
Expand Down Expand Up @@ -74,7 +77,7 @@ object domain {
case class PartyDetails(party: Party, displayName: Option[String], isLocal: Boolean)

final case class CommandMeta(
commandId: Option[lar.CommandId],
commandId: Option[CommandId],
ledgerEffectiveTime: Option[Instant],
maximumRecordTime: Option[Instant])

Expand All @@ -83,10 +86,9 @@ object domain {
argument: LfV,
meta: Option[CommandMeta])

final case class ExerciseCommand[+LfV](
templateId: TemplateId.OptionalPkg,
contractId: lar.ContractId,
choice: lar.Choice,
final case class ExerciseCommand[+LfV, +Ref](
reference: Ref,
choice: domain.Choice,
argument: LfV,
meta: Option[CommandMeta])

Expand Down Expand Up @@ -128,6 +130,10 @@ object domain {
type ContractId = lar.ContractId
val ContractId = lar.ContractId

type CommandIdTag = lar.CommandIdTag
type CommandId = lar.CommandId
val CommandId = lar.CommandId

type PartyTag = lar.PartyTag
type Party = lar.Party
val Party = lar.Party
Expand Down Expand Up @@ -387,17 +393,33 @@ object domain {
}

object ExerciseCommand {
implicit val traverseInstance: Traverse[ExerciseCommand] = new Traverse[ExerciseCommand] {
override def traverseImpl[G[_]: Applicative, A, B](fa: ExerciseCommand[A])(
f: A => G[B]): G[ExerciseCommand[B]] = f(fa.argument).map(a => fa.copy(argument = a))
implicit val bitraverseInstance: Bitraverse[ExerciseCommand] = new Bitraverse[ExerciseCommand] {
override def bitraverseImpl[G[_]: Applicative, A, B, C, D](
fab: ExerciseCommand[A, B])(f: A => G[C], g: B => G[D]): G[ExerciseCommand[C, D]] = {
import scalaz.syntax.applicative._
^(f(fab.argument), g(fab.reference))((c, d) => fab.copy(argument = c, reference = d))
}
}

implicit val hasTemplateId: HasTemplateId[ExerciseCommand] =
new HasTemplateId[ExerciseCommand] {
override def templateId(fa: ExerciseCommand[_]): TemplateId.OptionalPkg = fa.templateId
implicit val leftTraverseInstance: Traverse[ExerciseCommand[+?, Nothing]] =
bitraverseInstance.leftTraverse

implicit val hasTemplateId =
new HasTemplateId[ExerciseCommand[+?, domain.ContractLocator[_]]] {

override def templateId(
fab: ExerciseCommand[_, domain.ContractLocator[_]]): TemplateId.OptionalPkg = {
fab.reference match {
case EnrichedContractKey(templateId, _) => templateId
case EnrichedContractId(Some(templateId), _) => templateId
case EnrichedContractId(None, _) =>
throw new IllegalArgumentException(
"Please specify templateId, optional templateId is not supported yet!")
}
}

override def lfType(
fa: ExerciseCommand[_],
fa: ExerciseCommand[_, domain.ContractLocator[_]],
templateId: TemplateId.RequiredPkg,
f: PackageService.ResolveTemplateRecordType,
g: PackageService.ResolveChoiceRecordType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ class ApiValueToJsValueConverter(apiToLf: ApiValueToLfValueConverter.ApiValueToL
a.fields.toList.traverse(convertField).map(fs => JsObject(fs.toMap))
}

@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def convertRecord(
record: List[lav1.value.RecordField]): JsonError \/ List[(String, JsValue)] = {
record.traverse(convertField)
}

private def convertField(field: lav1.value.RecordField): JsonError \/ (String, JsValue) =
field.value match {
case None => \/-(field.label -> JsObject.empty)
Expand Down
Loading